字符串题目合集
压缩
题意:一段由相同字符串重复而来的字符串可以写成次方的形式。给定 \(s\),问 \(s\) 压缩之后最少还有多少个字符。
DP + KMP
\(dp[len][i]\) 表示长度 \(len\) 起点 \(i\) 的最少是多少。\(dp[len][i]\) 初值 \(len\),也就是不做任何处理。
同时可以枚举断点。\(dp[len][i]\leftarrow dp[k-i+1][i]+dp[len-(k-i+1)][k+1]\)。
最后,可以让它自己压缩(只有在 \(s[i\sim i+len-1]\) 有循环节的时候才更新这一类)。\(dp[len][i]\leftarrow dp[cir][i]\)。\(cir\) 表示最短循环节长度。
判断两个字符串 \(s1,s2\) 是否循环同构。
令 \(s=s1s1\),判断 \(s\) 中是否有 \(s2\) 即可。跑一遍 KMP。
Periods of Words
题意:若 \(a\) 是 \(s\) 的真前缀,且 \(s\) 是 \(aa\) 的前缀,称 \(a\) 是 \(s\) 的周期。给定字符串 \(w\),求 \(w\) 每个前缀的最长周期长度之和(如果不存在算 \(0\))。
容易发现,对于一个固定的 \(s\),它最大周期等于 \(|s|\) 减去 \(s\) 的最短真 border 的长度(如果不存在真 border 则不存在周期)。
因此跑一遍 KMP,可以求出每个前缀原始的 \(nxt\)。这里又有两种方法。
-
失配树找最高级祖先。
-
类似并查集路径压缩。因为如果当前位置的 \(nxt\) 不是这个位置最小的 \(nxt\),那么任何时刻都不会用到这个位置的 \(nxt\)。于是每到一个新位置,直接暴力把当前位置的 \(nxt\) 一直递归到最小即可。
寻找 \(t\) 的最长前缀是 \(s\) 的子串。
SAM 直接冲
我们可以用 KMP 这个优雅的算法。令 \(t\) 作为模式串,\(s\) 作为母串。
观察得知,KMP 当匹配到母串的第 \(i\) 个位置时,实际上已经算出了 \(t\) 作为 \(s[1\sim i]\) 的后缀的最大前缀 \(rho[i]\)。
答案就是 \(\max\{rho[i]\}\).
求 \(s\) 每个前缀的出现次数。
SAM 直接冲
-
KMP 方法。先对 \(s\) 做一遍 KMP。对于前缀 \(s[1\sim i]\),只要看 \(nxt[i+1\sim n]\) 里有多少个 \(nxt=i\) 即可。不要忘了加上自己开头出现了一次。
-
Z 函数方法。先求出 \(Z\) 函数。对于前缀 \(s[1\sim i]\),只要看 \(z[2\sim n]\) 里有多少个 \(z\ge i\) 即可。不要忘了加上自己开头出现了一次。
Extend to Palindrome
简化题意:求最长回文后缀。
令 \(t=rev(s)+\text{#}+s\)。对 \(t\) 求 \(Z\) 函数,然后枚举每个后缀判断即可。
求本质不同子串数。
SAM 直接冲
考虑增量算法:已经处理了字符串 \(s\),在前面加一个字符 \(c\),考虑以 \(c\) 开头的子串们。利用 \(nxt[]/z[]\) 去重。
不过因为每新加入一个字符就要重新求一遍,复杂度 \(O(n^2)\)。
Pal-Palindrome
结论:两个回文串 \(a,b\)。\(ab\in Pal\iff root(a)=root(b)\)。
那么弄一个类似桶的东西即可。
x-prime substrings
题意比较复杂,点进去看。
观察到 \(x\le 20\),而最坏情况下所有 x-prime 串的总长 \(\le 5000\)。把所有 x-prime 串取出建立 AC 自动机,然后 DP。
\(dp[i][j]\) 表示前 \(i\) 个字符走到 AC 自动机的 \(j\) 号结点至少删几个。
Mysterious Code
题意:给定一个字符串 \(C\) 和两个字符串 \(s,t\)。\(C\) 有一些位置可以填,一些位置固定。求填法使 \(C\) 中 \(s\) 出现次数 - \(t\) 出现次数最大。\(|C|\le 1000,|s|,|t|\le 50\)。
给 \(s,t\) 分别建一个 KMP 自动机。\(dp[i][j][k]\) 表示 \(C\) 的前 \(i\) 个字符,在 \(aca[s]\) 位于 \(j\),在 \(aca[t]\) 位于 \(k\) 的最大答案。
Bracket Substring
题意:求有多少个长为 \(2n\) 的合法括号序列,包含给定括号序列 \(s\) 作为子串。\(n\le 100, |s|\le 200\)。
给 \(s\) 建立 KMP 自动机。
\(dp[i][j][k][0/1]\):填 \(i\) 个字符,位于自动机上 \(j\) 号结点,左括号减去右括号等于 \(k\),且尚未完成/已经完成了一次对 \(s\) 的完整匹配。
Resource Archiver
题意:给定 \(n\) 个资源串,\(m\) 个病毒串,都是 01 串。求一个串 \(s\) 包含所有资源串为子串,不包含任何病毒串为子串。\(n\le 10,m\le 1000,len\le 1000\)。
病毒串总长 \(\le 50000\)。
法一:把所有串丢进 AC 自动机里,给病毒串结点打病毒标记,给资源串结点标记编号。\(dp[S][i]\) 表示已经满足 \(S\) 内资源串的子串,当前位于自动机上 \(i\) 点。
但是空间会爆。
法二:同样把所有串丢进 AC 自动机里,同样打标记,\(dp[S][i]\) 表示已经满足 \(S\) 内资源串的子串,当前以 \(i\) 号资源串结尾。
对于自动机上一个点到所有点的最短路,可以用一次最短路解决;于是跑 \(n\) 次最短路,可以求得任意两个资源串结点的最短路。
转移就枚举下一个去哪个资源串,注意就算是去过的资源串也要考虑。
Median Replace 及其题解
CF1286E
题意:维护一个字符串 \(s\),支持动态在末尾加入字符。初始字符串是空的,每个字符 \(c_i\) 加入的时候都会给出对应的权值 \(w_i\)。定义一个子串 \(s[l\sim r]\) 的权值为 \(\min(w[l\sim r])\)。
如果 \(s[l\sim r]=s[1\sim r-l+1]\),就要统计它的权值;要求在每加入一个字符之后,都输出当前的权值之和。强制在线。
CF 3200 分
(为了契合这一题的题意,下面 border 的描述是包括了一整个串的)
考虑增量算法。这一次加入的是 \((c_i,w_i)\)。记录 \(s[1\sim i-1]\) 的答案为 \(lstans\),那么 \(s[1\sim i]\) 的答案就是 \(lstans+\) \(s[1\sim i]\) 所有 border 的权值之和。
因为 \(s[1\sim i]\) 的 border 可以看作 \(s[1\sim i-1]\) 的 border 加一个字符,考虑用一个数据结构维护当前所有 border 的集合。
但是维护 border 不好做,于是考虑用一个数据结构维护当前 border 的权值的集合。(这一步也不知道怎么想出来的)
记这个集合为 \(A\)。考虑新增 \(c_i\) 后 \(s[1\sim i-1]\) 原本的 border 会怎么变化。
-
一个 border 为 \(s[1\sim k]\),若 \(s[k+1]\neq s[i]\),应该删除这个 border;
-
在所有剩下的 border 里,所有权值 \(>w_i\) 的 border 都把权值改成 \(w_i\),因为是取 \(\min\)。
-
如果 \(s[1]=s[i]\),会新增一个 border 为 \(s[i\sim i]\)。
我们可以用一个 map 维护这些权值。\(mp[x]=y\) 表示当前权值为 \(x\) 的 border 有 \(y\) 个。
-
如何快速找到 \(s[k+1]\neq s[i]\) 的 border?
找 border,最先的想法就是在失配树(\(nxt\) 树)上找,然后在 map 里依次删除。但是如果 \(nxt[i-1],nxt[nxt[i-1]]\) 这样跳下去必定超时。
考虑记录一个 \(dif\) 数组。\(dif[x]\) 表示在 \(nxt[x],nxt[nxt[x]],\dots\) 中(失配树的祖先中),最近的与 \(x\) 期望的字符不同的是哪个。
例如 \(s=ABAABA\),既有 \(ABA\) 作为 border,又有 \(A\) 作为 border。但是 \(A\) 期望的字符是 \(B\),\(ABA\) 期望的字符是 \(A\),\(dif[3]\) 就记录为 \(1\)。(期望的字符,就是在 \(s\) 后面添加哪个字符,还能保持是 border)
如果有这个数组,可以这么做:令 \(x=i-1\),若 \(x\) 依然是 border,\(x=dif[x]\),这可以保证 \(x\) 会跳到一个不再是 border 的;否则 \(x=nxt[x]\)。
这样每跳两步就至少删除一个 border,因为 border 总数是 \(O(n)\) 的,所以跳的次数是均摊 \(O(1)\) 的。 -
权值 \(>w_i\) 的全部改成 \(w_i\)。
利用 map.end() 倒序遍历 map,直到所有 \(>w_i\) 的都改完。因为每进行一次修改,有至少一个 border 合并到权值为 \(w_i\) 的里面去了,而 border 总数 \(O(n)\) 的,所以至多修改 \(O(n)\) 次,总共复杂度 \(O(n\log n)\)。
-
新增一个长度为 \(1\) 的 border。这个很简单。
于是复杂度 \(O(n\log n)\)。在循环中其实要记录两个值:\(lstans,lstbor\),表示上一次的答案和上一次的 border 权值和。
求一个 border 的权值就是简单的 RMQ 问题。最需要注意的是 map 中等于 \(0\) 的位置要 erase 掉。
Frequency of Strings
题意:
给你一个字符串 \(s \pod {|S| \le 10^5}\) ,有 \(n \pod {n \le 10^5}\) 个询问,第 \(i\) 个询问包含一个整数 \(k_i\) 和一个字符串 \(m_i \pod {\sum_i |m_i| \le 10^5}\) 。要求找到一个字符串 \(t\) ,使得 \(t\) 是 \(s\) 的子串并且 \(m_i\) 至少在 \(t\) 中出现了 \(k_i\) 次。你只需要求出 \(t\) 的最短长度。
保证 \(m_i\) 互不相同。
记询问串长度和为 \(len\),则询问串的长度种类数至多 \(O(\sqrt{len})\) 种。
因此所有出现位置 \(\le O(\sqrt{len}\cdot |S|)\) 个。
对所有询问串建立 AC 自动机,在 keyword 结束的结点维护一个 vector,用来保存结束位置。把 \(S\) 放到上面跑,每跑到一个位置,就把 fail 树上所有 keyword 祖先的 vector 增加对应位置。
void mdf(string &s) {
int x = 1;
for (int i = 0; i < s.size(); i++) {
x = abs(a[x].e[cd(s[i])]);
int p = x;
if (a[x].ed == 0)
p = a[p].nxtkey;
while (p != 1) {
a[p].v.push_back(i);
p = a[p].nxtkey;
}
}
}
回答询问的时候,找到对应结点,取出 vector 里的结束位置,枚举从哪个结束位置到哪个结束位置取个 \(\min\) 就行了。
int len = 0x3f3f3f3f;
for (int j = 0; j + k[i] - 1 < t.a[t.nd[i]].v.size(); j++)
len = min(len, (int)(t.a[t.nd[i]].v[j + k[i] - 1] - (t.a[t.nd[i]].v[j] - m[i].size() + 1) + 1));
cout << len << endl;
CF868D:Huge Strings
题意:给定初始 \(n\) 个 01 串入库,然后有 \(m\) 次操作,每次操作从库里选定两个 01 串拼接为 \(s\),然后 \(s\) 入库。对于每个 \(s\),找到最大的 \(k\) 使得 \(s\) 包含所有 \(k\) 长度 01 串为子串。\(n,m\le 100\)。初始库串长和 \(\le 100\)。
难点在于拼接的字符串长度会很大。考虑所有长度为 \(L\) 的 01 串,在初始库里,因为 \(\sum |s_i|\le 100\),所以初始库里互不相同的 \(L\) 长度子串最多 \(100\) 个。
发现每次拼接产生新的子串,必然在于拼接处。一共拼接 \(100\) 次,每次从拼接处左右延伸,最多新增 \(L-1\) 个不同的子串。所以一共最多有 \(100+100\times (L-1)=100L\) 个不同的子串。要是所有 \(L\) 长度子串都出现了,\(100L\ge 2^L\),有 \(L\le 9\)。
因此最大答案不会超过 \(9\)。这就意味着我们不需要在拼接时保留过多的字符,那我们需要保留多少呢?
因为一次拼接至多贡献出长度为 \(9\) 的 01 串,所以一共可能会在拼接时贡献字符串的字符一共 \(9\times 100\) 个,再加上初始可能有贡献的 \(100\) 个字符,所以每次拼接至多保留开头结尾各 \(1000\) 个字符。实际上这个界相当宽松,只保留 \(900\) 个也可以过。