字符串算法总结

前言

标题是骗你进来的,其实里面全是题目。
最近一直在搞字符串......
把一些有代表性或者有一定难度的题放在这里做一个总结。

[CF666E] Forensic

给你一个串\(S\)以及一个字符串数组\(T[1..m]\)\(q\)次询问,
每次问\(S\)的子串\(S[p_l..p_r]\)\(T[l..r]\)中的哪个串里的出现次数最多,并输出出现次数。
如有多解输出最靠前的那一个。数据范围:\(|S|,\sum |T|,q\leq 10^5\)

\(T[1...m]\)建立广义\(SAM\),用线段树合并维护出现次数。
每次查询先倍增到相应结点,然后直接线段树区间查询。

[NOI2011] 阿狸的打字机

给定一个打字机,有加字符、删字符、打印三种操作。
给定操作序列\(S\),然后有\(Q\)次询问,每次回答第\(x\)次打印的串在第\(y\)次打印的串中出现几次。
数据范围:\(|S|,Q\leq 10^5\)

用栈模拟建出所有打印串的\(Trie\)树,然后构建\(AC\)自动机与\(fail\)树。
每次询问相当于问\(x\)\(Trie\)的根这条路径上有多少个点能够跳\(fail\)跳到\(y\)
\(Trie\)进行\(dfs\),用树状数组维护\(fail\)树的子树和。

[BZOJ3670] 动物园

给定一个串\(S\),求每一个前缀的不相交\(border\)数。 数据范围:\(|S|\leq 10^7\)

解法一:建出\(fail\)树,然后\(dfs\)一遍\(fail\)树,用单调队列维护合法前缀大小。
解法二:先预处理出\(next\)数组,然后类似\(next\)数组的求法求答案,当不合法时跳\(next\)数组。

[SCOI2013] 密码

给定一个串的每个回文中心的扩展大小,构造满足条件的最小字典序串。数据范围:\(n\leq 10^5\)

对这个串跑一遍\(manacher\),直接模拟即可,用并查集维护两个位置的字符是否相同。
最后使用最小表示法求出答案串。

[NOI2015] 品酒大会

给定一个串\(S\),求\(\sum_{i} \sum_{i\neq j} lcp(suf_i,suf_j)\)。数据范围\(|S|\leq 10^5\)

使用后缀数组,按照\(Height\)排序后合并后缀,用带权并查集维护答案。
或者建出\(SAM\)\(fail\)树后,直接\(dfs\)一遍\(fail\)树,每次直接合并子树答案。

[POI2005] SZA-Template

求一个最短的模板串\(T\),使得用\(T\)对长度为\(n\)的串反复染色后可以得到\(S\)
注意:同一个位置可以多次染色,但只能染一种颜色。
数据范围:\(n\leq 5*10^5\)

显然\(T\)\(S\)的一个\(border\)
那么限制条件就是\(T\)在串\(S\)中的出现位置的最大间距不能超过\(|T|\),其中\(T\)为一个\(border\)
注意到\(border\)匹配的特殊性,当一个前缀\(pre\)\(border\)\(T\)时,\(T\)就在该点匹配上了一次。
所以跑\(kmp\)后建出\(fail\)树。
对于一个\(T\),它合法的充要条件为子树内的点之间的间距不超过\(|T|\),直接用\(set\)维护最大间距。

[BZOJ4310] 跳蚤

给一个串\(S\),把它划分为最多\(K\)段,
然后挑选出所有段中的最大字典序串,再把这些串取最大字典序串,称得到的串为\(T\)
最小化\(T\)的字典序并输出\(T\)。 数据范围:\(|S|\leq 3*10^5\)\(K\leq 20\)

最大最小化问题,首先二分\(T\)\(S\)的所有子串中的排名。
然后贪心验证。由于我们的后缀数组是以后缀排名的,所以从后往前扫。
若当前的后缀排名小于等于\(T\),则直接跳过。
否则需要考虑割一刀,先求\(lcp\),然后查看\(lcp\)长度范围内是否割了一刀,如果没有就割一刀。
最后比较割的次数与\(K\)的关系即可。
需要实现的功能有快速求\(lcp\),给串求排名,给排名求串,这些都可以用\(SA\)解决。

[HihoCoder1413] Rikka-with-String

给一个串\(S\),求把每一个位置替换为特殊字符#后本质不同的子串个数。
数据范围:\(|S|\leq 10^5\)

先构建\(SAM\),把原串本质不同的子串个数求出来,然后考虑变化量。
首先增加量很显然是\(i(n-i+1)\) ,考虑求减少量。
对于\(SAM\)上的每一个点,我们维护其最大\(endpos\)与最小\(endpos\)
然后作图可以发现,这个结点对两个区间的贡献分别为 等差数列 和 一个常数。
差分即可。

[BZOJ2384] Match

给定两个排列\(S,T\),要求\(T\)\(S\)中匹配了多少次。
这里的匹配定义为:相对大小相同即算匹配上。
数据范围:\(|S|,|T|\leq 10^6\) 。空间限制:\(64MB\)

魔改一下\(kmp\)算法。
对于\(T\),我们可以求出一个\(next\)数组,表示失配后到达位置,然后在\(S\)上类似的跑\(kmp\)即可。
现在的问题变为:求一个区间内大于某个数的个数,直接想法是主席树,但会\(MLE\)
深度发掘一下\(kmp\)的原理,发现它其实类似一个双指针。
所以使用树状数组,在跳\(next\)的时候暴力把删除元素在树状数组中删掉。

[CF932G] Palindrome

给定一个偶数长度的串\(S\),试着把它划分为偶数段。
设划分为了\(k\)段,那么需要满足\(s_i=s_{k-i+1}\),求方案数。数据范围:\(|S|\leq 10^6\)

首先构建串\(S'= s_1s_ns_2s_{n-1}...\),问题转化为把\(S'\)划分为若干偶数回文串的方案数。
对于一个回文串,
若它的若干回文后缀都不超过其长度的一半,则这些回文后缀一定构成等差数列。
这样的话,回文树上的某个结点的所有祖先最多只会构成\(log\)个等差数列。
我们考虑让同一等差数列中的点一起转移,维护\(up\)表示最浅的非等差位置。
对于同一等差数列中的点,由于其长度不超过原串长度的一半,
故我们对称后刚好只有\(up\)处的贡献没有加上,所以暴力跳等差数列的同时把该贡献加上即可。

[NOI2018] 你的名字

给定一个串\(S\),有\(Q\)次询问,每次给定一个串\(T\),求不在\(S[l,r]\)中出现的\(T\)的子串个数。
数据范围:\(|S|,Q,\sum |T| \leq 5 * 10^5\),其中\(l,r\)也是每次询问给定的变量。

\(S\)建立后缀自动机,用线段树合并得到其\(endpos\)集合。
对于每次询问,先对\(T\)建立后缀自动机,求的\(T\)的子串个数,然后再把在\(S\)中出现的子串减掉。
对于\(T\),我们求出其每个前缀的最长匹配后缀\(lim\)
那么对于\(T\)\(SAM\)上的每个点,利用\(lim\)把非法贡献减掉即可。
考虑求\(lim\),直接把\(T\)放在\(S\)\(SAM\)上做匹配,通过线段树查询\(endpos\)判断是否出现。
当失配的时候,不能直接跳\(fail\),而应该要使长度减一,然后再次检查。
可以发现这个过程中,\(T\)和匹配长度\(len\)的关系类似一个双指针,所以复杂度是没有问题的。

[NOI2016] 优秀的拆分

给定一个串\(S\),求所有子串划分为\(AABB\)形式的方案数之和。数据范围:\(|S|\leq 10^5\)

惊现\(NOI\)史上最良心出题人,白送\(95\)分简直搞笑。考虑最后\(5\)分应该怎么拿。
显然\(AABB\)是一个障眼法,我们其实只用求\(AA\)的划分方案就行了。
开始构造,枚举一下\(A\)的长度\(len\)
然后对于\(S\),每个长度\(len\)我们就设置一个顶标点\(p_i\)
那么对于任意\(|A|=len\)\(AA\)串,它一定恰好经过两个顶标点。
所以对于相邻两个顶标点,求一下它们的最长公共前、后缀,然后算一下贡献即可。

[CTSC2010] 珠宝商

给定一个串\(S\)和一棵\(n\)个结点树,其中树上的每一个结点有一个字符。
求树上每一条有序路径构成的字符串在\(S\)中的出现次数之和。数据范围:\(n,|S|\leq 50000\)

树上路径问题考虑点分治,建立点分树。由于\(n\)范围比较小,所以可以数据均摊分治。
对于点分子树大小\(\leq \sqrt{n}\)的点,我们直接暴力做,复杂度\(O(\sqrt{n}^3) = O(n\sqrt{n})\)
对于点分子树大小\(> \sqrt{n}\),这类点显然只有\(\sqrt{n}\)个,我们尝试用其它理论解决。
考虑得到了两条路径,然后把它们拼接在一起。
那么对应到字符串上,就是一个前缀和一个后缀进行拼接。
所以我们只需要知道每一个前缀的方案数和每一个后缀的方案数,然后再乘一下就行了。
我们以求前缀方案数为例,后缀方案数反过来做即可。
观察一下匹配过程,我们确定了一个\(endpos\)字符,然后需要向前做匹配。
这显然是\(SAM\)\(DAG\)\(fail\)树难以做到的。
所以我们需要将\(SAM\)生成的\(fail\)树进一步处理,得到其前缀树,然后匹配就是跳前缀树的子树。
最后的问题在于如何快速给匹配的点的所有\(endpos\)加上贡献。
回顾一下\(endpos\)集合的得到方法,不难发现只需要把这个过程给逆过来就行了。
我们先在当前点打上标记,全部匹配完后,顺着\(fail\)树把标记向下推送。
那么对于一个前缀,它在\(fail\)树上对应的结点一定是一个叶子结点,我们直接在该叶子查答案。

[八省联考2018] 制胡窜

给定一个串\(S\)
\(Q\)次询问,每次求把串划分为非空三段,且三段中至少包含一个\(S[pl,pr]\)的方案数。
数据范围:\(|S|,|Q|\leq 100000\),其中\(pl,pr\)为每次给定的变量。

此题代码细节贼多,如果要写请务必做好代码调试至少一个晚上的准备。
正难则反,考虑求不包含\(S[pl,pr]\)的方案数。
为了书写方便我们令\(T = S[pl,pr]\),同时定义\(T\)\(S\)中的出现次数为\(n\)
定义\(T\)\(S\)中的出现串为\(p_1,p_2...p_n\),其中\(l_{p_i},r_{p_i}\)表示它们的左右端点。
\(mn = p_1,mx = p_n\),令\(len = pr-pl+1\)
我们现在的目标就是用两刀切掉\(S\)中出现的所有\(T\)
大力讨论:

( 1 ) 若存在三个不相交的\(T\),此时显然无解。
( 2 ) 否则,若\(r_{mn} > l_{mx}\),即存在一刀流切法。

  • 若第一刀不是一刀流,枚举第一刀切掉了哪些\(T\),则有:
    \(Ans = \sum_{i=1}^{n-1} (r_{p_{i+1}} - r_{p_{i}})(r_{mn} - l_{p_{i+1}})\)
    化简有\(Ans = (r_{mn}-len+1) \sum_{i=1}^{n-1} (r_{p_{i+1}} - r_{p_i}) - \sum_{i=1}^{n-1} r_{p_{i+1}} (r_{p_{i+1}} - r_{p_i})\)
  • 若第一刀是一刀流,那么第一刀的落刀范围为\([l_{mx},r_{mn}]\),考虑第二刀的位置。
    若第二刀也落在\([l_{mx},r_{mn}]\),这种情况的方案数显然为\(\binom{r_{mn} - l_{mx}}{2}\)
    否则可以发现第二刀落在\([l_{mn},l_{mx}-1)\)的方案已经算过了,
    故只用算落在其它位置的方案数,这个还是比较好算的。
    若落在右侧,则第一刀落在\([l_{mx},r_{mn})\),第二刀落在\([r_{mn} , n]\)
    若落在左侧,则第一刀落在\((l_mx,r_{mn}]\),第二刀落在\([1,l_{mn}]\)
    所以这种情况下的方案数为\(Ans = \binom{r_{mn} - l_{mx}}{2} + (r_{mn} - l_{mx}) (n-len)\)

( 3 ) 否则,即\(r_{mn} \leq l_{mx}\),即不存在一刀流切法。
顺着上一种思路,依旧考虑枚举左边那刀切掉了哪些\(T\)
那么有:\(Ans = \sum_{i} (r_{p_{i+1}} - r_{p_i}) (r_{mn} - l_{p_{i+1}})\)
但是由于不存在一刀流,所以一定会有一个\(i\)\(r_{mn}\)限制,导致切割范围不是完整区间。
所以我们把左端点最靠近\(r_{mn}\)的那个点先丢掉,设其为\(p_t\)
然后就可以得到一个关于\(i\)的限制条件:\(r_{i+1} > l_{mx}\)\(l_i < r_{mn}\)
这里需要注意一个天坑(你们就使劲感谢我吧):
我们找到的是最小的符合条件的\(r_{i+1}\),而计算区间端点应该是\(r_i\),所以这里需要找一次前趋。
然后我们再把之前丢掉的那个点\(p_{t}\)给捡回来,它的答案应该是\((r_{mn}-l_{p_t})(r_{p_{t+1}} - l_{mx})\)
QaQ
累死我了,要是上面哪里写错了麻烦各位吱一声。
到此为止我们已经讨论了所有情况。
唯一的问题在于如何实现,
可以注意到上面所有式子中与\(endpos\)有关的只有\(\sum r_i(r_{i+1}-r_i),\sum r_i\)
所以完全可以直接使用线段树维护区间最大、最小值,进而使得这两个信息可以合并。
同时我们发现,维护区间最大、最小值更是一并解决了找前趋、后继的问题。
所以我们对\(S\)\(SAM\),把询问离线,然后按拓扑序逆序进行线段树合并,作出相应回答即可。
至此问题终于解决。

posted @ 2018-12-24 15:43  GuessYCB  阅读(1022)  评论(0编辑  收藏  举报