字符串题目合集

压缩

题意:一段由相同字符串重复而来的字符串可以写成次方的形式。给定 s,问 s 压缩之后最少还有多少个字符。

DP + KMP

dp[len][i] 表示长度 len 起点 i 的最少是多少。dp[len][i] 初值 len,也就是不做任何处理。

同时可以枚举断点。dp[len][i]dp[ki+1][i]+dp[len(ki+1)][k+1]

最后,可以让它自己压缩(只有在 s[ii+len1] 有循环节的时候才更新这一类)。dp[len][i]dp[cir][i]cir 表示最短循环节长度。

判断两个字符串 s1,s2 是否循环同构。

s=s1s1,判断 s 中是否有 s2 即可。跑一遍 KMP。

Periods of Words

题意:若 as 的真前缀,且 saa 的前缀,称 as 的周期。给定字符串 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[1i] 的后缀的最大前缀 rho[i]

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

s 每个前缀的出现次数。

SAM 直接冲

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

  2. Z 函数方法。先求出 Z 函数。对于前缀 s[1i],只要看 z[2n] 里有多少个 zi 即可。不要忘了加上自己开头出现了一次。

Extend to Palindrome

简化题意:求最长回文后缀。

t=rev(s)+#+s。对 tZ 函数,然后枚举每个后缀判断即可。

求本质不同子串数。

SAM 直接冲

考虑增量算法:已经处理了字符串 s,在前面加一个字符 c,考虑以 c 开头的子串们。利用 nxt[]/z[] 去重。

不过因为每新加入一个字符就要重新求一遍,复杂度 O(n2)

Pal-Palindrome

结论:两个回文串 a,babPalroot(a)=root(b)

那么弄一个类似桶的东西即可。

x-prime substrings

题意比较复杂,点进去看。

观察到 x20,而最坏情况下所有 x-prime 串的总长 5000。把所有 x-prime 串取出建立 AC 自动机,然后 DP。
dp[i][j] 表示前 i 个字符走到 AC 自动机的 j 号结点至少删几个。

Mysterious Code

题意:给定一个字符串 C 和两个字符串 s,tC 有一些位置可以填,一些位置固定。求填法使 Cs 出现次数 - t 出现次数最大。|C|1000,|s|,|t|50

s,t 分别建一个 KMP 自动机。dp[i][j][k] 表示 C 的前 i 个字符,在 aca[s] 位于 j,在 aca[t] 位于 k 的最大答案。

Bracket Substring

题意:求有多少个长为 2n 的合法括号序列,包含给定括号序列 s 作为子串。n100,|s|200

s 建立 KMP 自动机。
dp[i][j][k][0/1]:填 i 个字符,位于自动机上 j 号结点,左括号减去右括号等于 k,且尚未完成/已经完成了一次对 s 的完整匹配。

Resource Archiver

题意:给定 n 个资源串,m 个病毒串,都是 01 串。求一个串 s 包含所有资源串为子串,不包含任何病毒串为子串。n10,m1000,len1000
病毒串总长 50000

法一:把所有串丢进 AC 自动机里,给病毒串结点打病毒标记,给资源串结点标记编号。dp[S][i] 表示已经满足 S 内资源串的子串,当前位于自动机上 i 点。
但是空间会爆。

法二:同样把所有串丢进 AC 自动机里,同样打标记,dp[S][i] 表示已经满足 S 内资源串的子串,当前以 i 号资源串结尾。
对于自动机上一个点到所有点的最短路,可以用一次最短路解决;于是跑 n 次最短路,可以求得任意两个资源串结点的最短路。
转移就枚举下一个去哪个资源串,注意就算是去过的资源串也要考虑。

Median Replace 及其题解

CF1286E

题意:维护一个字符串 s,支持动态在末尾加入字符。初始字符串是空的,每个字符 ci 加入的时候都会给出对应的权值 wi。定义一个子串 s[lr] 的权值为 min(w[lr])
如果 s[lr]=s[1rl+1],就要统计它的权值;要求在每加入一个字符之后,都输出当前的权值之和。强制在线。

CF 3200 分

(为了契合这一题的题意,下面 border 的描述是包括了一整个串的)

考虑增量算法。这一次加入的是 (ci,wi)。记录 s[1i1] 的答案为 lstans,那么 s[1i] 的答案就是 lstans+ s[1i] 所有 border 的权值之和。

因为 s[1i] 的 border 可以看作 s[1i1] 的 border 加一个字符,考虑用一个数据结构维护当前所有 border 的集合。

但是维护 border 不好做,于是考虑用一个数据结构维护当前 border 的权值的集合。(这一步也不知道怎么想出来的)

记这个集合为 A。考虑新增 cis[1i1] 原本的 border 会怎么变化。

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

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

  3. 如果 s[1]=s[i],会新增一个 border 为 s[ii]

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

  1. 如何快速找到 s[k+1]s[i] 的 border?

    找 border,最先的想法就是在失配树(nxt 树)上找,然后在 map 里依次删除。但是如果 nxt[i1],nxt[nxt[i1]] 这样跳下去必定超时。

    考虑记录一个 dif 数组。dif[x] 表示在 nxt[x],nxt[nxt[x]], 中(失配树的祖先中),最近的与 x 期望的字符不同的是哪个。

    例如 s=ABAABA,既有 ABA 作为 border,又有 A 作为 border。但是 A 期望的字符是 BABA 期望的字符是 Adif[3] 就记录为 1。(期望的字符,就是在 s 后面添加哪个字符,还能保持是 border)

    如果有这个数组,可以这么做:令 x=i1,若 x 依然是 border,x=dif[x],这可以保证 x 会跳到一个不再是 border 的;否则 x=nxt[x]
    这样每跳两步就至少删除一个 border,因为 border 总数是 O(n) 的,所以跳的次数是均摊 O(1) 的。

  2. 权值 >wi 的全部改成 wi

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

  3. 新增一个长度为 1 的 border。这个很简单。

于是复杂度 O(nlogn)。在循环中其实要记录两个值:lstans,lstbor,表示上一次的答案和上一次的 border 权值和。
求一个 border 的权值就是简单的 RMQ 问题。最需要注意的是 map 中等于 0 的位置要 erase 掉。

Frequency of Strings

题意:
给你一个字符串 s(|S|105) ,有 n(n105) 个询问,第 i 个询问包含一个整数 ki 和一个字符串 mi(i|mi|105) 。要求找到一个字符串 t ,使得 ts 的子串并且 mi 至少在 t 中出现了 ki 次。你只需要求出 t 的最短长度。

保证 mi 互不相同。


记询问串长度和为 len,则询问串的长度种类数至多 O(len) 种。

因此所有出现位置 O(len|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,m100。初始库串长和 100

难点在于拼接的字符串长度会很大。考虑所有长度为 L 的 01 串,在初始库里,因为 |si|100,所以初始库里互不相同的 L 长度子串最多 100 个。

发现每次拼接产生新的子串,必然在于拼接处。一共拼接 100 次,每次从拼接处左右延伸,最多新增 L1 个不同的子串。所以一共最多有 100+100×(L1)=100L 个不同的子串。要是所有 L 长度子串都出现了,100L2L,有 L9

因此最大答案不会超过 9。这就意味着我们不需要在拼接时保留过多的字符,那我们需要保留多少呢?

因为一次拼接至多贡献出长度为 9 的 01 串,所以一共可能会在拼接时贡献字符串的字符一共 9×100 个,再加上初始可能有贡献的 100 个字符,所以每次拼接至多保留开头结尾各 1000 个字符。实际上这个界相当宽松,只保留 900 个也可以过。

论战捆竹竿题解

posted @   FLY_lai  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示