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;
}

浙公网安备 33010602011771号