字符串题目合集

压缩

题意:一段由相同字符串重复而来的字符串可以写成次方的形式。给定 \(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\)。这里又有两种方法。

  1. 失配树找最高级祖先。

  2. 类似并查集路径压缩。因为如果当前位置的 \(nxt\) 不是这个位置最小的 \(nxt\),那么任何时刻都不会用到这个位置的 \(nxt\)。于是每到一个新位置,直接暴力把当前位置的 \(nxt\) 一直递归到最小即可。

寻找 \(t\) 的最长前缀是 \(s\) 的子串。

SAM 直接冲

我们可以用 KMP 这个优雅的算法。令 \(t\) 作为模式串,\(s\) 作为母串。

观察得知,KMP 当匹配到母串的第 \(i\) 个位置时,实际上已经算出了 \(t\) 作为 \(s[1\sim i]\) 的后缀的最大前缀 \(rho[i]\)

答案就是 \(\max\{rho[i]\}\).

\(s\) 每个前缀的出现次数。

SAM 直接冲

  1. KMP 方法。先对 \(s\) 做一遍 KMP。对于前缀 \(s[1\sim i]\),只要看 \(nxt[i+1\sim n]\) 里有多少个 \(nxt=i\) 即可。不要忘了加上自己开头出现了一次。

  2. 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 会怎么变化。

  1. 一个 border 为 \(s[1\sim k]\),若 \(s[k+1]\neq s[i]\),应该删除这个 border;

  2. 在所有剩下的 border 里,所有权值 \(>w_i\) 的 border 都把权值改成 \(w_i\),因为是取 \(\min\)

  3. 如果 \(s[1]=s[i]\),会新增一个 border 为 \(s[i\sim i]\)

我们可以用一个 map 维护这些权值。\(mp[x]=y\) 表示当前权值为 \(x\) 的 border 有 \(y\) 个。

  1. 如何快速找到 \(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)\) 的。

  2. 权值 \(>w_i\) 的全部改成 \(w_i\)

    利用 map.end() 倒序遍历 map,直到所有 \(>w_i\) 的都改完。因为每进行一次修改,有至少一个 border 合并到权值为 \(w_i\) 的里面去了,而 border 总数 \(O(n)\) 的,所以至多修改 \(O(n)\) 次,总共复杂度 \(O(n\log n)\)

  3. 新增一个长度为 \(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;
posted @ 2024-08-05 10:27  FLY_lai  阅读(12)  评论(0编辑  收藏  举报