字符串专题-KMP+扩展KMP
KMP算法
例题1:E. Martian Strings【前缀函数的运用】
这一题笨笨地写了个SA+二分,慢死了(常数大)。虽然这一题是多串匹配,但是\(m=100\),S串长度为\(1e5\),所以是可以暴力check每一个pattern的。。但是因为这一题要把一个串分成两个不相交的区间,所以考虑顺序、逆序做一次KMP,特判长度为1的pat。复杂度\(O(m*n=1e7)\)
思路来自于:聚聚的blog LINK
查看代码
int n, q, k, nt[maxn], pv[maxn], sf[maxn];
char pat[1111], s[maxn];
inline void initNxt(char s[], int n) {
nt[1] = 0;
for (int i = 2, k = 0; i <= n; i++) {
while (k && s[i] != s[k + 1]) k = nt[k];
nt[i] = (s[i] == s[k + 1] ? ++k : 0);
}
}
inline void search(int* ar, int n, int m) {
for (int i = 1, k = 0; i <= n; i++) {
while (k && k < m && s[i] != pat[k + 1]) k = nt[k];
if (k < m && s[i] == pat[k + 1]) k++;
ar[i] = max(ar[i - 1], k); // 根据题目而取 max
}
}
inline bool chk() {
int m = strlen(pat + 1);
if (m > n || m == 1) return false;
initNxt(pat, m), search(pv, n, m);
reverse(pat + 1, pat + m + 1);
reverse(s + 1, s + 1 + n);
initNxt(pat, m), search(sf, n, m);
reverse(s + 1, s + 1 + n);
reverse(sf + 1, sf + 1 + n);
for (int i = 1; i < n; i++)
if (pv[i] + sf[i + 1] >= m) return true;
return false;
}
inline void solve() {
cin >> s + 1 >> q, n = strlen(s + 1);
int ans = 0;
for (int i = 1; i <= q; i++) cin >> pat + 1, ans += chk();
cout << ans << endl;
}
例题2:P2375 [NOI2014] 动物园【KMP + DP + 严格Border的数量】
定义严格border为str的border,且前缀和后缀不重叠。假设border长度为m,则\(str[1:m]==str[n-m+1:n]\),且\(n-m+1>m\)。求每个前缀有多少严格border。
我们求出nxt数组,发现nxt数组就是fail树上的父节点,从n开始倒着做滑动窗口/贪心/滑动指针即可(用儿子指针更新父节点指针)。就可以做到\(O(n)\)。当然也可以考虑树上倍增。
查看代码
const int maxn = 1e6 + 9, mod = 1e9 + 7;
char str[maxn];
int TEST, n, nxt[maxn], dep[maxn], ptr[maxn];
int main() {
scanf("%d", &TEST);
while (TEST--) {
scanf("%s", str + 1);
n = 1, dep[1] = nxt[1] = 0;
for (int i = 2, L = 0; str[i]; n++, i++) {
while (L && str[i] != str[L + 1]) L = nxt[L];
if (str[i] == str[L + 1]) L++;
ptr[i] = nxt[i] = L, dep[i] = dep[L] + (L > 0);
}
int ans = 1;
for (int i = n; i >= 1; i--) {
while (ptr[i] && ptr[i] * 2 > i) ptr[i] = nxt[ptr[i]];
ans = (1ll * ans * (dep[ptr[i]] + (ptr[i] > 0) + 1)) % mod;
if (dep[ptr[nxt[i]]] > dep[ptr[i]]) ptr[nxt[i]] = ptr[i];
}
printf("%d\n", ans);
}
}
例题3:D. Om Nom and Necklace【枚举 + 思维 + 循环节 + 贪心】
题意:给定长度为\(n\)的字符串str和一个常数\(k\)。对于每一个前缀str[1:i],问他能不能拆成ABABAB...A的形式,其中A出现k+1次,B出现k次,A和B可以为空。
对于一个前缀i,\(len=i-nxt[i]\)是最短的循环节,我们先贪心地把最短的循环节铺满,同时铺满的情况应该是k的倍数。假设AB的长度是L,那么有:\(L=len*v\),且\(v=\lfloor\cfrac{i}{k*(i-nxt[i])}\rfloor\)。
这个时候再检查一下是否有:\(v*len\ge i-v*len*k\)即可。注意特判\(k=1\)时,ans[1]='1'。
例题4:UVA11022 String Factoring【字符串压缩 + 区间DP】
题意:给你一个长度小于80的串,相邻的两个相邻的子串可以压缩,但是要用括号括住,之后和外部不能压缩了。求压缩后的最小长度。
如果一个串是有循环节组成的,那么它就是KMP直接压缩就行,压缩完之后还是要做区间DP的,因为还是有可能继续压缩。否则,我们考虑区间dp:\(f(str)=min(f(str[0,i])+f(str[i+1,n])) i是枚举的分割点,两边不会有压缩关系了\)。然后做一个记忆化搜索,虽然状态是字符串,但是还是很快的。
查看代码
string s;
map<string, int> ans;
int getNxt(const string& s) {
vector<int> nxt(s.size(), 0);
for (int i = 1, j = 0; i < s.size(); i++) {
while (j && s[i] != s[j]) j = nxt[j - 1];
if (s[i] == s[j]) j++;
nxt[i] = j;
}
return s.size() - nxt.back();
}
int dfs(string s) {
if (s.size() <= 1) return s.size();
int len = getNxt(s);
if (len < s.size() && s.size() % len == 0) s = s.substr(0, len);
if (ans.count(s)) return ans[s];
int ret = s.size();
for (int i = 1; i < s.size(); i++)
ret = min(ret, dfs(s.substr(0, i)) + dfs(s.substr(i, s.size() - i)));
return ans[s] = ret;
}
int main() {
ios_fast;
while (cin >> s && s != "*") cout << dfs(s) << '\n';
}
例题5:G. Anthem of Berland【暴力DP + KMP】
做法1:定义\(f[i][j]\)为S的前i位匹配了T的第j位的最大匹配次数,然后枚举下一个字母进行转移。为了更好实现,我们引入trans[i][c]数组表示字符串T已经被匹配了i位,下一位来了个c的时候,跳fail应该转移到第nj位,这样就能辅助转移(即dp[i][nj]=dp[i-1][j]+(nj==m))。为了更好对最后一位进行转移,我们在T后面添加一个字符'#',自然,匹配完第m位之后,m+1位就是'#'了,就要自动跳nxt进行转移了(注意,滚动数组注意清空,否则wa10)。
关于trans:trans数组的转移类似AC自动机的trie图一样,利用fail进行转移。
做法2:考虑i是匹配T的最后一位,(一开始先\(O(m)\)暴力for循环check一下,i能否作为最后一位)那么定义f[i]为S的第i位匹配了T的最后一位的最多匹配次数。然后转移就是枚举fail链(即枚举和之前重叠的部分而已)。这个做法剪掉了26的常数,还好出题人有点良心没有卡第一种做法。(f数组做一遍前缀最大值进行转移即可)
【同样用到trans数组的一个题目:KMP转移矩阵优化DP】:P3193 [HNOI2008]GT考试
例题6:P3435 [POI2006] OKR-Periods of Words【模板题 - 每个前缀的最长不严格周期】
我们知道最短不严格周期(就是长度不一定是n的约数)就是i-nxt[i],但是对于最长的不严格周期(是一个周期,但是不能是自己),我们只能暴力跳fail了。(注意,题目意思应该是如果没有的话,长度就是0)
可是暴力跳fail会被卡成\(O(n^2)\)复杂度,不行。但是我们发现,最大的i-nxt[i],其实就是取出fail树上i节点的最浅非0父亲而已。我们考虑一些单调性质。如果i节点已经知道答案,那么它的子树中的节点跳到i节点也会知道答案,最终每条边最多被跳一次,所以!记忆化之后是\(O(n)\)的。
查看代码
const int maxn = 1e6 + 7;
int n, nxt[maxn];
char str[maxn];
int main() {
ios_fast;
cin >> n >> str + 1;
for (int i = 2, j = 0; i <= n; i++) {
while (j && str[i] != str[j + 1]) j = nxt[j];
nxt[i] = (str[i] == str[j + 1] ? ++j : j);
}
long long ans = 0;
for (int i = 2; i <= n; i++) {
int j = i;
while (nxt[j]) j = nxt[j];
if (nxt[i]) ans += (i - j), nxt[i] = j;
// 只有nxt[i]!=0是 ,才有真正的最长非严格周期
}
cout << ans << endl;
}
扩展KMP算法 - Z函数
例题1: E. Text Editor【贪心 + Z函数预处理】
题意:给一个长度为m的T串,和一个长度为n>m的S串。现在让你删去S中的一些位置,使得S变成T。最开始光标在S串末尾,你只能操作1.backspace、2.left、3.right、4.home、5.end 这5个按键。求出最少按键次数。
思路:
枚举S中的一个分界点sp,再枚举T中的一个分界点tp。
① sp左侧尽可能往左边匹配,计算公式为:\(sp-tp+|LCS|\),其中LCS是S[1:sp]和T[1:tp]的最长公共后缀
② sp右侧尽可能往右匹配,计算公式为:\(|LCP|\),其中LCP是S[sp+1:n]和T[tp+1:m]的最长公共前缀。
考虑使用Z函数预处理优化掉一个n,同时使用贪心策略判断能不能表示,最后复杂度是:\(O(n^2+n*m)\)。妈的,因为脑子猪了,所以写了一年。
查看代码
int n, m, pre[maxn], suf[maxn];
string S, T, revT;
vector<int> getZ(const string& s) {
vector<int> z(s.size(), 0);
for (int i = 1, l = 0, r = 0; i < s.size(); ++i) {
if (i <= r && z[i - l] < r - i + 1) {
z[i] = z[i - l];
} else {
z[i] = max(0, r - i + 1);
while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]]) ++z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
return z;
}
void solve() {
cin >> n >> m >> S >> T;
revT = T, reverse(all(revT));
for (int i = 0, j = -1; i <= n; i++) {
if (i < n && j < m - 1 && S[i] == T[j + 1]) j++;
pre[i] = j;
}
suf[n] = m + 1; // 记得初始化
for (int i = n - 1, j = m; i >= 0; i--) {
if (j > 0 && S[i] == T[j - 1]) j--;
suf[i] = j;
}
int ans = inf_int;
string LS, RS = S;
for (int sp = 0; sp <= n; sp++) {
auto lz = getZ(LS + "#" + revT);
move(lz.begin() + LS.size() + 1, lz.end(), lz.begin());
auto rz = getZ(RS + "#" + T);
move(rz.begin() + RS.size() + 1, rz.end(), rz.begin());
for (int tp = 0; tp <= sp; tp++) {
if (tp - 1 > (sp ? pre[sp - 1] : -1)) break;
if (tp < suf[sp] || m - tp > n - sp) continue;
// 这里存在一个边界条件,当tp=0且sp=0时,说明不会按home键,反之默认按home键
int Lans = 2 * (int)LS.size() + (tp ? 1 - lz[m - tp] - tp : (sp != 0));
int Rans = (int)RS.size() - (tp < m ? rz[tp] : 0);
if (Lans < 0 || Rans < 0) continue;
ans = min(Lans + Rans, ans);
}
if (sp == n || RS.size() == 0) break;
LS.insert(0, 1, S[sp]);
RS = RS.substr(1, (int)RS.size() - 1); \\ string的erase有bug,会删除p之后所有字符
}
cout << (ans == inf_int ? -1 : ans) << '\n';
}

浙公网安备 33010602011771号