「HDU 5677」 ztr loves substring - Manacher + 多重背包

版权声明:禁止商业用途,如需转载请注明原文出处:https://hyp1231.github.io 或者 https://blog.csdn.net/hyp1231/article/details/81061081

建议访问原文出处,获得更佳浏览体验。
原文出处:https://hyp1231.github.io/2018/07/10/20180710-hdu5677/

题意

给出 N 个字符串,所有字符串的连续回文子串构成一个多重集合 S
S 中是否存在恰好 K 个串,使它们的长度之和为 L

链接

HDU5677 ztr loves substring

题解

只与每个串的长度有关,故应先预处理出每个长度的回文串的数量。

使用 Manacher 算法计算出以串的各个位置为中心的最大回文串长度。若以 s i 为中心的最大回文串的长度为 l (不妨假设它为偶数),考虑逐步去掉两侧字符就可以造出更多回文子串。因此长度为 2 , 4 , , l 的子串的计数器分别加一(若 l 为奇数,同理)。这样我们就得到了每个长度的串的数量,

考虑选出 K 个串使它们长度之和为 L 。即选出 K 个物品,填满容量为 L 的背包,每种物品的个数已知,是多重背包问题。设计 d p [ i ] [ j ] 表示当前已经选了 i 个串,放在容量为 j 的背包中的最大容量。记放入新串后数量为 n u m ,当前背包容量为 p ,新增串个数为 m u l ,新增子串长 i ,则状态转移方程为:
d p [ n u m ] [ p ] = M A X ( d p [ n u m ] [ p ] , d p [ n u m m u l ] [ p m u l i ] + m u l i )

Hint 1: 转移要求在 n u m = m u l (第一次添加串)或 d p [ n u m m u l ] [ p m u l i ] 不为 0 时才转移(说明存在上一个状态)。
Hint 2: 使用二进制优化加速多重背包。

记长度为 i 的回文子串的数量为 W i 。读入和 N 次 Manacher 是 O ( N L ) 的,多重背包是 O ( ( i = 1 L log W i ) K L ) 。总时间复杂度 O ( ( i = 1 L log W i ) K L + N L )

代码

#include <cstdio>
#include <algorithm>
#include <cstring>

const int N = 128;
const char mid_c = '$';
const char side_c = '@';

int n, k, l;
char s[N][N];
char s2[N << 1];
int len, r[N][N << 1];

void prepare(char s1[]) {
    len = 0;
    s2[len++] = side_c;
    s2[len++] = mid_c;

    int n = strlen(s1);
    for (int i = 0; i < n; ++i) {
        s2[len++] = s1[i];
        s2[len++] = mid_c;
    }
    s2[len + 1] = '\0';
}

void Manacher(char s1[], int id) {
    prepare(s1);
    memset(r[id], 0, sizeof(r[id]));
    int mid = 0, p = 0;
    for (int i = 0; i < len; ++i) {
        int x;
        if (p < i)x = 1;
        else x = std::min(p - i, r[id][2 * mid - i]);
        while (s2[i - x] == s2[i + x]) ++x;
        if (i + x > p) {
            p = i + x;
            mid = i;
        }
        r[id][i] = x;
    }
}

int cnt[N], dp[N][N];
// dp[i][j] 代表 i 个子串拼接成的串的最大长度(不超过 j)

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        memset(cnt, 0, sizeof(cnt));
        memset(dp, 0, sizeof(dp));
        scanf("%d%d%d", &n, &k, &l);
        for (int i = 0; i < n; ++i) {
            scanf("%s", s[i]);
        }
        for (int i = 0; i < n; ++i) {
            Manacher(s[i], i);
            for (int j = 0; j < len; ++j) {
                int tmp = r[i][j] - 1;
                if (tmp <= 0) continue;
                while (tmp > 0) {           // 最长子串逐步去掉侧边字母也是子串
                    ++cnt[tmp];
                    tmp -= 2;
                }
            }
        }
        for (int i = 0; i <= l; ++i) {      // 枚举子串长度
            int sum = cnt[i];               // 长度为 i 的子串数量
            for (int j = 1; sum; j <<= 1) { // 二进制优化
                int mul = std::min(sum, j);
                for (int p = l; p >= mul * i; --p)      // 枚举长度
                    for (int num = mul; num <= k; ++num)// 枚举已选字符串个数
                        if (num == mul || dp[num - mul][p - mul * i] > 0)
                            // 如果本串是第一次被选,或存在选了 p - mul * i 个串的方案
                            dp[num][p] = std::max(dp[num][p], 
                                                  dp[num - mul][p - mul * i] + mul * i);
                sum -= mul;
            }
        }
        printf("%s\n", (dp[k][l] == l ? "True" : "False"));
    }

    return 0;
}

猜你喜欢

转载自blog.csdn.net/hyp1231/article/details/81061081