前言
标题是骗你进来的,其实里面全是题目。
最近一直在搞字符串......
把一些有代表性或者有一定难度的题放在这里做一个总结。
[CF666E] Forensic
给你一个串(S)以及一个字符串数组(T[1..m]),(q)次询问,
每次问(S)的子串(S[p_l..p_r])在(T[l..r])中的哪个串里的出现次数最多,并输出出现次数。
如有多解输出最靠前的那一个。数据范围:(|S|,sum |T|,qleq 10^5)。
对(T[1...m])建立广义(SAM),用线段树合并维护出现次数。
每次查询先倍增到相应结点,然后直接线段树区间查询。
[NOI2011] 阿狸的打字机
给定一个打字机,有加字符、删字符、打印三种操作。
给定操作序列(S),然后有(Q)次询问,每次回答第(x)次打印的串在第(y)次打印的串中出现几次。
数据范围:(|S|,Qleq 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] 密码
给定一个串的每个回文中心的扩展大小,构造满足条件的最小字典序串。数据范围:(nleq 10^5)
对这个串跑一遍(manacher),直接模拟即可,用并查集维护两个位置的字符是否相同。
最后使用最小表示法求出答案串。
[NOI2015] 品酒大会
给定一个串(S),求(sum_{i} sum_{i eq j} lcp(suf_i,suf_j))。数据范围(|S|leq 10^5)。
使用后缀数组,按照(Height)排序后合并后缀,用带权并查集维护答案。
或者建出(SAM)的(fail)树后,直接(dfs)一遍(fail)树,每次直接合并子树答案。
[POI2005] SZA-Template
求一个最短的模板串(T),使得用(T)对长度为(n)的串反复染色后可以得到(S)。
注意:同一个位置可以多次染色,但只能染一种颜色。
数据范围:(nleq 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)、(Kleq 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(nsqrt{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}]),这种情况的方案数显然为(inom{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 = inom{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),把询问离线,然后按拓扑序逆序进行线段树合并,作出相应回答即可。
至此问题终于解决。