P3426 [POI2005]SZA-Template - 传送门
题意
给你一字符串,需要你制作一个印章,能用它盖出该字符串(当然不能多盖)。
求最小印章长度。
盖印章:
-
同一位置,一样字符可以重复盖;
-
同一位置,不同字符不能重复盖。
例如:
字符串为 ababbababbabababbabababbababbaba
,印章为 ababbaba
。
盖法如下:
ababbababbabababbabababbababbaba
ababbaba
ababbaba
ababbaba
ababbaba
ababbaba
引理
我们用 f i f_i fi 表示能覆盖长度为 i 的前缀的最小印章长度。
用数组 nxt 表示 KMP 算法中的 kmp 数组。
具体思路、后续求法将在思路部分细说,此处先放引理及证明。
Part 1 - f i f_i fi 的取值情况
一、 f i = i f_i=i fi=i
这个不难理解,当 f i = i f_i=i fi=i 时当然覆盖完前缀 i 了。
二、 f i = f n x t i f_i=f_{nxt_i} fi=fnxti
1、不难得出 f i ≤ i f_i \le i fi≤i。
接下来我们只开始讨论 f i < i f_i<i fi<i 的情况。
( f i = i f_i=i fi=i 见引理一。)
2、易得 f i ≤ n x t i f_i \le nxt_i fi≤nxti。
证明如下:
若 f i > n x t i f_i > nxt_i fi>nxti,为满足题意,我们盖印章的时候必须在最前面和最后面分别盖一个。
例如:
ababbababbabababbabababbababbaba
ababbaba
ababbaba
此时, f i f_i fi 即为 i 的相同前后缀,且比 n x t i nxt_i nxti 还长(已假设 f i > n x t i f_i > nxt_i fi>nxti)。
这与 n x t i nxt_i nxti 为 i 最长相同前后缀的定义相矛盾,
故, f i f_i fi 只能小于等于 n x t i nxt_i nxti。
有了这个引理,我们就可以分成两种情况,分别见 3 和 4。
3、当 f i < n x t i f_i < nxt_i fi<nxti 时, f i = f n x t i f_i=f_{nxt_i} fi=fnxti。
-
若 f i < f n x t i f_i<f_{nxt_i} fi<fnxti,则无法用 f i f_i fi 填充前 n x t i nxt_i nxti 个字符,不然 f n x t i f_{nxt_i} fnxti 会更小。
所以, f i ≥ f n x t i f_i \ge f_{nxt_i} fi≥fnxti。
-
若 f i > f n x t i f_i>f_{nxt_i} fi>fnxti,则可以用 f n x t i f_{nxt_i} fnxti 去填充 f i f_i fi( f i < n x t i f_i<nxt_i fi<nxti),
与 f i f_i fi 为最短能填充 i 的定义相矛盾。
所以, f i ≤ f n x t i f_i \le f_{nxt_i} fi≤fnxti。
综上,得 f i = f n x t i f_i=f_{nxt_i} fi=fnxti。
4、当 f i = n x t i f_i=nxt_i fi=nxti 时, f i = f n x t i f_i=f_{nxt_i} fi=fnxti。
因为 f i f_i fi 的定义,即不可以再出现比 n x t i nxt_i nxti 还能短且能填充掉 n x t i nxt_i nxti 的串。
所以此时, f n x t i = n x t i = f i f_{nxt_i}=nxt_i=f_i fnxti=nxti=fi,
即 f i = f n x t i f_i=f_{nxt_i} fi=fnxti。
综上所述, f i f_i fi 的取值只可能是 i 或者 f n x t i f_{nxt_i} fnxti。
Part 2 - f i f_i fi 什么情况下可以取 f n x t i f_{nxt_i} fnxti
只要我们在 [ i − n x t i , i ] [i-nxt_i,i] [i−nxti,i] 中发现一个 j,使得 f j = f n x t i f_j=f_{nxt_i} fj=fnxti,
此时就可以让 f i ← f n x t i f_i \gets f_{nxt_i} fi←fnxti。
ababbaba bbabababbabababb ababbaba
ababbaba
ababbaba
例如当前字符串 i 的最长公共前后缀是 ababbaba
——也就是我空格出的部分。
ababbababbabababbabababb ababbaba
ababbaba
ababbaba
按照刚才所说,
我们需要在后面的 ababbaba
(最长公共前后缀中的后缀)中找到 j。
此时,
f n x t i f_{nxt_i} fnxti 可以覆盖 [ n x t i , i ] [nxt_i,i] [nxti,i], f j f_j fj 可以覆盖 [ 1 , j ] [1,j] [1,j];
又因为 j ≥ i − n x t i j \ge i-nxt_i j≥i−nxti;
还 f j = f n x t i f_j=f_{nxt_i} fj=fnxti;
所以,
我们就可以用 f n x t i f_{nxt_i} fnxti 覆盖掉整个字符串 i 了。
思路
我们用了 dp 的思路去求解了最小印章长度。
用 KMP 去找最长相同前后缀。
由引理,我们得知:
循环每次求给出字符串的长度为 i 的前缀的 f i f_i fi 值:
初始化 f i ← i f_i \gets i fi←i;
若出现了 j( f j = f n x t i f_j=f_{nxt_i} fj=fnxti, j ≥ i − n x t i j \ge i-nxt_i j≥i−nxti),那么 f i ← f n x t i f_i \gets f_{nxt_i} fi←fnxti。
怎么去找 j?
我们开个桶去记录,每次印章长度为 f i f_i fi 时最多能覆盖到的字符的下标即可。
具体见代码。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn = 500005;
int n;
char a[maxn];
int kmp[maxn], b[maxn];
int f[maxn];
int main ()
{
scanf ("%s", a + 1);
n = strlen (a + 1);
for (int i = 2, j = 0; i <= n; i++)
{
while (j and a[j + 1] != a[i]) j = kmp[j];
if (a[j + 1] == a[i]) j++;
kmp[i] = j;
}
int j = 0;
for (int i = 1; i <= n; i++)
{
f[i] = i;
if (b[f[kmp[i]]] >= (i - kmp[i])) f[i] = f[kmp[i]];
b[f[i]] = i;
}
printf ("%d\n", f[n]);
return 0;
}
算是 KMP 的 kmp 数组的应用,就是思维难度确实挺大。
kmp 数组理解深度 ++;
—— E n d End End——
By 531