把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

P3269 [JLOI2016] 字符串覆盖 分析

题目概述

给一个串 \(S\),和 \(n\) 个子串 \(T_i\),然后问你用这 \(n\) 个子串覆盖 \(S\),最多和最少能覆盖多少个单位长度。

\(n\leq 4,|S|,|T_i|\leq 10^4\)

分析

先搞一下每个子串在 \(S\) 上的每个匹配位置,求出前缀最后匹配和后缀最前匹配。

然后我们想最小值时就是尽量覆盖,最大值尽量不覆盖,那么我们先全排列搞一下这个顺序然后直接利用记忆化搜索即可。

思路还是比较清晰的。

代码(AI 注释)

#include <bits/stdc++.h>
using namespace std;

// ---------- 辅助函数 ----------
void chmax(int &x, int y) { if (x < y) x = y; }
void chmin(int &x, int y) { if (x > y) x = y; }

// ---------- 常量与全局变量 ----------
#define N 5              // 子串个数最多为4,开5防越界
#define L 10005          // 母串最大长度

char s[L];               // 母串(1-indexed)
char q[1003];            // 当前匹配的子串模板(1-indexed)
int pre[N][L];           // pre[i][x]:子串i在位置x及之前最后一次出现的起点
int nxtArr[N][L];        // nxtArr[i][x]:子串i在位置x及之后第一次出现的起点
int len[N];              // len[i]:子串i的长度
int n;                   // 子串个数

int id[N];               // 排列数组,用于枚举顺序
int *pr[N], *nex[N];     // 当前排列下指向 pre 和 nxtArr 的指针
int curLen[N];           // 当前排列下各子串的长度(拷贝 len)

int failArr[L];          // KMP失败函数
bool mark[L];            // 标记母串中某个位置是否为某个子串的出现起点

// ---------- KMP预处理 ----------
// 对当前模板串 q 计算 fail 数组
void initFail() {
    int m = strlen(q + 1);
    failArr[1] = 0;
    for (int i = 2, j = 0; i <= m; ++i) {
        while (j && q[j + 1] != q[i]) j = failArr[j];
        if (q[j + 1] == q[i]) ++j;
        failArr[i] = j;
    }
}

// 使用 KMP 将模板 q 在母串 s 中匹配,填充 preArr 和 nxtArr
void ins(int *preArr, int *nxtArr) {
    int m = strlen(q + 1);
    int l = strlen(s + 1);
    int j = 0;
    // KMP 匹配,标记所有出现位置的起点
    for (int i = 1; i <= l; ++i) {
        while (j && q[j + 1] != s[i]) j = failArr[j];
        if (q[j + 1] == s[i]) ++j;
        if (j == m) {
            mark[i - m + 1] = true;
            j = failArr[j];
        }
    }
    // 构建 preArr:前缀最后一个出现位置
    preArr[0] = 0;
    for (int i = 1; i <= l; ++i) {
        if (mark[i]) preArr[i] = i;
        else preArr[i] = preArr[i - 1];
    }
    preArr[l + 1] = preArr[l];   // 扩展一位,方便查询时使用

    // 构建 nxtArr:后缀第一个出现位置
    nxtArr[l + 1] = 0;
    for (int i = l; i >= 1; --i) {
        if (mark[i]) nxtArr[i] = i;
        else nxtArr[i] = nxtArr[i + 1];
    }
    // 清空标记数组
    for (int i = 1; i <= l; ++i) mark[i] = false;
}

// ---------- DP 求最小值 ----------
int f[L][N];            // 记忆化数组:f[first][num]
bool vis[L][N];         // 访问标记
bool *st[L * N];        // 栈,用于记录哪些 vis 被访问过,便于清空
int top = 0;            // 栈顶指针

// dp(first, num):当前考虑从母串位置 first 开始,处理排列中第 num 到第 n 个子串,
// 返回该阶段能覆盖的最小字符数。
int dp(int first, int num) {
    if (num > n) return 0;                     // 所有子串已处理完
    first = nex[num][first];                   // 在当前块起点之后,子串 num 第一次出现的位置
    int &ans = f[first][num];
    if (vis[first][num]) return ans;           // 记忆化
    vis[first][num] = true;
    st[++top] = &vis[first][num];              // 记录,以便后续清空

    // 情况1:当前块的起点向右移动一位(相当于跳过一个位置不覆盖)
    ans = dp(first + 1, num);

    // 尝试将第 num 个子串作为当前块的第一个子串,并尽量与后续子串重叠(最小化长度)
    int last = first + curLen[num];            // 当前块的右边界(开区间)
    int x0 = first;                            // 记录当前块内上一个子串的起点,用于判断重叠
    while ((++num) <= n) {
        // 情况2:在第 num 个子串之前断开当前块,从 last 位置开始新块
        chmin(ans, dp(last, num) + last - first);

        // 尝试与当前块重叠:选择在 first 之后的最左出现位置(重叠最多)
        int x = nex[num][first];
        if (x < x0 || x >= last) return ans;   // 无法重叠,结束尝试
        x0 = x;
        chmax(last, x + curLen[num]);          // 更新当前块的右边界
    }
    // 所有剩余子串都合并到当前块,不额外断开
    chmin(ans, last - first);
    return ans;
}

// ---------- DP 求最大值 ----------
// get(first, num):与 dp 类似,但求最大值。
int get(int first, int num) {
    if (num > n) return 0;
    first = nex[num][first];
    if (!first) return -L;                     // 不存在该子串,返回极小值

    int last = first + curLen[num];
    int x0 = first;
    int ans = -L;

    while ((++num) <= n) {
        // 断开当前块,开启新块
        chmax(ans, get(last, num) + last - first);

        // 尽量与当前块重叠但重叠最少:选择在 last 之前的最右出现位置
        int x = pr[num][last];
        if (x < x0 || x >= last) return ans;   // 无法重叠
        x0 = x;
        chmax(last, x + curLen[num]);          // 更新右边界
    }
    chmax(ans, last - first);
    return ans;
}

// ---------- 主函数 ----------
int main() {

    // 初始化 f[0][i] 和 vis[0][i],处理 first=0 的情况(表示无法匹配)
    for (int i = 1; i <= 4; ++i) {
        vis[0][i] = true;
        f[0][i] = L;
    }

    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%s", s + 1);                    // 母串
        int l = strlen(s + 1);
        scanf("%d", &n);                       // 子串个数
        for (int i = 1; i <= n; ++i) {
            scanf("%s", q + 1);
            len[i] = strlen(q + 1);
            initFail();                        // 计算 KMP 失败函数
            ins(pre[i], nxtArr[i]);            // 填充该子串的 pre 和 nxtArr
        }

        int ansMin = L, ansMax = 0;
        // 初始化排列
        for (int i = 1; i <= n; ++i) id[i] = i;

        // 枚举所有放置顺序
        do {
            // 将当前顺序映射到指针和长度数组
            for (int i = 1; i <= n; ++i) {
                pr[i] = pre[id[i]];
                nex[i] = nxtArr[id[i]];
                curLen[i] = len[id[i]];
            }

            // 求该顺序下的最小值
            chmin(ansMin, dp(1, 1));
            // 清空记忆化标记
            while (top) {
                *st[top] = false;
                --top;
            }

            // 求该顺序下的最大值
            chmax(ansMax, get(1, 1));
            while (top) {
                *st[top] = false;
                --top;
            }

        } while (next_permutation(id + 1, id + n + 1));

        printf("%d %d\n", ansMin, ansMax);
    }
    return 0;
}
posted @ 2026-07-22 16:23  high_skyy  阅读(3)  评论(0)    收藏  举报
动态线条
动态线条end
浏览器标题切换
浏览器标题切换end
💬 加载中……