字符串进阶
一些不那么裸的字符串题,甚至出现了 parent 树优化建图这种离谱的东西。
前置知识:kmp,字符串哈希,AC 自动机,SA,SAM,Manacher
CF914F Substrings in a String
题意:给定字符串,要求支持单点修改,询问时给出字符串,求在 \([l,r]\) 中出现多少次。
思路:考虑一般的字符串维护方法都不支持修改,因此需要另辟蹊径。
考虑用 bitset 来维护字符串匹配。
其实很暴力,就是先求出每个字符的每个出现位置,然后按顺序扫描文本串,把新加的字符的 bitset 左移 \(i\) 位再与起来,最后得到的就是所有合法左端点。
复杂度是优秀的 \(O(\dfrac{n^2}{w})\)。
CF1207G Indie Album
题意:有\(n\)次操作,格式为\(1~c\)或\(2~j~c\),分别表示新建一个为\(c\)的字符串,和在第\(j\)次操作得到的串后接上\(c\)。
接着是\(m\)次询问,格式为\(i~t\),每次询问版本\(i\)的串中,\(t\)的出现次数。
思路:AC机模板题。
先离线询问,然后对 \(t_i\) 建 AC机,这样如果暴力求答案就是把所有 \(s_i\) 在 AC机上跑一遍,求有多少个跑到的节点在 fail 树上以 \(t_i\) 为终止节点的子树中。
虽然 \(s_i\) 的总长很大,但是串之间是有祖先后代关系的,那么就可以 dfs 了。
CF1483F Exam
题意:给定互不相同的字符串 \(s_1, s_2, \cdots, s_n\),求有多少对 \((i, j)\) 满足:
- \(i \neq j\)
- \(s_j\) 是 \(s_i\) 的子串。
- 不存在 \(k\) \((k \neq i, k \neq j)\) 满足 \(s_j\) 是 \(s_k\) 的子串且 \(s_k\) 是 \(s_i\) 的子串。
思路:AC 自动机进阶。
首先可以发现,答案的的上界是 \(\sum|s|\),因为对于 \(s_i\) 的一个前缀,一定只有一个后缀可以对答案做贡献。于是就可以考虑对于每个串 \(i\),求出有多少满足条件的 \(j\)。
我们先对于 \(s_i\) 的每个前缀,求出最长的能和后缀匹配的串 \(s_k\),然后去掉被覆盖的,剩下就是可能对答案做贡献的。关键在于怎么去掉被覆盖的。如果 \(s_j\) 作为某个前缀的最长匹配后缀出现过但是在某个位置被其他的串覆盖了,你们它作为最长匹配后缀出现的次数一定小于它在 \(s_i\) 中一共出现过的次数,因此条件就是这两个出现次数相等。
后者是 AC 机的简单应用,前者可以在求出每个前缀的答案后再从后往前扫一遍去掉被覆盖的来求出,这样就做完了。
P3735 [HAOI2017] 字符串
题意:给出一个字符串 $ s $ 和 $ n $ 个字符串 $ p_i $,求每个字符串 $ p_i $ 在 $ s $ 中出现的次数。注意这里两个字符串相等的定义稍作改变。
给定一个常数 $ k $,对于两个字符串 $ a, b $,如果 $ a = b $,那么满足:
一、$ |a| = |b| $
二、对于所有 $ a_i \neq b_i $ 以及 $ a_j \neq b_j $,满足 $ |i-j| < k $
如果 $ |a| = |b| \le k $,那么认为 $ a = b $。
思路:条件可以转成 \(lcp+lcs>=len-k\),然后考虑固定 \(lcp\) 算 \(lcs\) 的贡献。
对所有 \(p_i\) 的正串和反串一起建 AC 自动机,将询问挂在 \(p_i\) 的前缀对应节点上,然后对于 \([1,pos]\) 的前缀,把 \(pos+k+1\) 位置的后缀对应节点也挂上去,然后计算每个前缀的贡献。
对于 S,处理出每个前缀在 AC 机上匹配到 \(p\) 节点时,把后缀节点在 AC 机上匹配到 \(p+k+1\) 位置的节点挂在 \(p\) 节点上。
然后就是统计答案。我们遍历一遍 fail 树,到达 \(x\) 时将点 \(x\) 子树内的节点上挂着的文本串对应的后缀节点都 +1,询问时查询模式串对应的后缀节点的子树和即可。可以先求一遍答案,然后遍历一遍子树,加入贡献,再求一遍答案就是子树的贡献。
不过这样计算会算重复,减去每个节点上挂 \(p+k\) 的贡献即可。
P4022 [CTSC2012] 熟悉的文章
题意:给出字符串集合 \(T\),多次询问一个字符串 \(s\) ,求出最大的 \(l\) 满足将 \(s\) 分为若干子串,使得所有长度不小于 \(l\) 且在 \(T\) 中出现过的子串的长度之和不小于 \(0.9|s|\)。
思路:考场上想到了大概,就是没发现转移区间的左端点是递增的,因为每次匹配的长度不会超过前一次匹配的长度 +1,于是dp可以用单调队列优化到 \(O(n)\),加上二分是 \(O(n\log n)\),求最长的匹配长度可以直接用广义SAM。
P5319 [BJOI2019] 奥术神杖
思路:发现答案式子与指数有关,于是可以两边同时取 \(ln\),这样就变成了分数规划,可以直接在AC自动机上DP。
And Yet Another Bracket Sequence
题意:有一个括号序列(不保证合法),你能够进行以下两个操作:
-
在任意位置插入一个左括号或者右括号;
-
将序列最末的括号移到最前。
经过若干次操作后,得到的最短的合法括号序列是什么?若多解,输出字典序最小的。
思路:首先显然是贪心,显然是将一段后缀转到前缀后再在前面、后面加括号。加括号的数量是确定的,因此关键在于一开始转多长的后缀,这个可以根据合法括号串的性质来,直接判前缀最小值是否合法就行了,要求字典序最小可以用二分+哈希,也可以用SA。然后就做完了。
P5284 [十二省联考 2019] 字符串问题
题意:给定字符串s以及两类区间,再给定一些从第一类区间连向第二类区间的边,一个第二类区间能连向一个第一类区间当且仅当前者是后者的前缀。每个第一类区间的价值是区间长度,求这张图上的最长路(或判断无限长)。
思路:好题。
算是 parent 树优化建边的普及题。
发现答案就是类似求最长路的东西,考虑建图。我们设 \(A\) 类串的权值是其长度,\(B\) 类串的权值是 0,然后如果 \(B_j\) 是 \(A_i\) 的前缀,就从 \(B_j\) 向 \(A_i\) 连一条边,然后对于每一个支配关系,从 \(A_i\) 向 \(B_j\) 连边。
关键就在于怎么建边。对于和子串前缀有关的,我们考虑建反串的 parent 树,然后用倍增来定位子串,这样建边就是 \(O(n^2\log n)\) 的。
然后我们发现,对于定位到同一个节点的子串,我们按其长度排序,然后从小的往大的连,这样就是 \(O(m+n)\) 的边数,而后就可过了。
P5576 [CmdOI2019] 口头禅
题意:求 01 串集合的区间最长公共子串。
思路:妙妙题。
想到了扫描线的做法,但是因为不知道一些结论,因此不会。
有小清新的广义SAM+线段树做法。
首先,我们建出广义SAM,然后给每个字符串赋一种颜色,在加入SAM时对遍历到的每个节点加入这个颜色。这样,一个点实际的颜色集合就是自身和所有儿子的颜色集合的并。于是询问就变成了求包含了 \([l,r]\) 的颜色的节点的长度最大值。
于是就想到了线段树合并,但是这样不好处理询问,于是考虑用 set 维护每个节点的颜色集合的连续段,然后采用启发式合并,这样询问就是二维数点问题。不过这道题中可以更简单地处理。具体地,我们把每个询问挂在左端点上,按右端点从小到大排序,然后按长度从大到小访问 SAM 上每个节点,然后遍历区间的所有询问,如果右端点包含了询问的右端点就可以直接记录答案,否则就等着之后处理。为了保证复杂度,维护一个区间右端点的最小值,然后只有右端点包含了最小值才处理,这样就可以保证复杂度。
最终复杂度 \(O(|S|\log^2|S|+m\log n)\),瓶颈在于启发式合并。
Birthday
题意:给定 \(n\) 个仅包含 a,b
的字符串,保证它们两两不同。你需要去掉尽可能少的字符串,使得剩下的字符串中不存在某一个串是另一个串的子串。
思路:巨大混乱邪恶题。
先考虑怎么求出每一对串是否有包含关系。
我们把所有串的 AC自动机建出来,然后对于每个点,维护它在 fail 树上的最近的终止节点,然后对于每个终止节点,遍历它在 trie 树上的所有祖先,向这个祖先的最近的终止节点连边,然后用 floyd 传递闭包求出偏序集。
然后就是求最长反链,可以用二分图匹配解决。
A task for substrings
题意:统计满足 \(t[a,b]\) 在 \(s_1,s_2,\dots,s_n\) 中出现过且 \(l_i \leq a \leq b \leq r_i\) 的 \((a,b)\) 的个数。
思路:好题。
以为可以直接扫描线,然后才发现一每个右端点结尾的合法子串不是连续一段
先考虑如果询问的是前缀怎么做。我们预处理出 \(f_i\) 表示以 \(i\) 结尾的合法子串个数,记 \(pre_i\) 为 \(f_i\) 的前缀和,那么答案就是 \(pre_i\)。
如果不是前缀,可以直接用 \(pre_r-pre_{l-1}\) 吗?显然不行,这样只满足了右端点的限制,没有满足左端点的限制。
感觉这种解法和一类式子形如 \(f_r-f_p+(p-l)a_p\) 的式子很像,于是考虑求出一个位置 \(p\) 使得两部分贡献分别很好计算。在这道题中,容易想到就是第一个有合法子串左端点不超过 \(l\) 的右端点。形式化的,设 \(g_i\) 最长的是 \(T[1,i]\) 的后缀的 \(s\) 串的编号,那么我们要找的就是最大的 \(p\) 满足 \(p-|s_{g_p}|+1\le l\),此时 \(p+1\sim r\) 的答案就是 \(pre_r-pre_p\),\(l\sim p\) 的答案是 \(s_{g_p}\) 的长度是 \(p-l+1\) 的后缀有多少子串合法,可以预处理出 \(c_{i,j}\) 表示 \(s_i\) 的长度为 \(j\) 的后缀有多少子串出现在 \(s\) 中就可以了。
求 \(f,g\) 可以建原 \(s\) 串的 AC 自动机,求 \(c\) 可以建反串的 AC 自动机,求 \(p\) 可以用线段树二分,复杂度 \(O(|S|+|T|+m\log|T|)\)。
P2178 [NOI2015] 品酒大会
题意:对于每个长度,求有多少对后缀的 lcp 不少于这个长度和后缀权值乘积的最大值。
思路:从后缀的 lcp 入手不难想到后缀数组,而求出 \(height\) 数组后两个后缀的 lcp 就是 \(height\) 数组的区间最小值,那么我们要求的就是有多少个区间的最小值是某个数。
考虑按 \(height\) 从大到小加入点,那么每一次的贡献就是两边的连续段长度的乘积,同时维护最大最小的权值,合并可以类似并查集,这样复杂度就对了。
P1117 [NOI2016] 优秀的拆分
题意:如果一个字符串可以拆分成形如 AABB 的四个部分,那么这就是合法的拆分方式。求一个字符串的所有子串的所有合法拆分方式的个数。
思路:首先,可以想到维护每个点往前有多少 AA 串,往后有多少 BB 串,那么答案就是 \(\sum pre_isuf_{i+1}\)。直接哈希是 \(O(n^2)\) 的,可以获得 95 分的高分。
怎么快速计算 \(pre,suf\) 呢?我们考虑枚举 \(len\),然后判断每个点是否是长度为 \(2len\) 的 AA 串的端点。
我们考虑每隔 \(len\) 个位置放一个点,此时每个 AA 串都会至少经过两个相邻的点,再来求相邻两点的贡献。我们求出这两点的 LCP 和 LCS,分两种情况。
如果 \(LCP+LCS<len\),我们发现不存在满足条件的 AA 串,因为如果要满足的话 LCP 和 LCS 至少有一个要增加。
于是就剩下了 \(LCP+LCS\ge len\) 的情况。设这两个点是 \(a,b\),此时第一个满足条件的串就是 \([a-LCP+1,a-LCP+2len]\),最后一个满足条件的就是 \([b+LCS-2len,b+LCS-1]\),在这中间的所有串就都是合法的,于是就是一个区间加。
总的做法就是先进行后缀排序,然后枚举 \(len\),总的枚举量是调和级数的,每一次要进行的操作就是查 LCP,LCS,可以用 st 表做到 \(O(1)\),区间加操作可以差分下来,最后做一遍前缀和即可。
P2414 [NOI2011] 阿狸的打字机
题意:给一棵 Trie 树,多组询问一个点代表的字符串再另一个点代表的字符串中出现了多少次。
思路:考虑建出 AC 自动机,那么有 \(fail[x]\) 代表的字符串出现在了 \(x\) 代表的字符串中,那么我们要求的就是有多少个属于 \(y\) 的节点的 fail 指针直接或间接指向 \(x\)。如果建出 fail 树,就是以 \(x\) 为根的子树中有多少个点属于 \(y\)。
我们在 dfs 的过程中,只把当前子树里的点标成 1,然后把询问挂在 \(y\) 上,此时 \(x\) 的子树和就是答案,可以用树状数组维护。
P2444 [POI2000] 病毒
题意:给出若干个 01 串,求是否存在一个无限长的 01 串使得给出的串都不是这个串的子串。
思路:考虑建出 AC 自动机,那么我们把构造出来的串放进来匹配,就必须$$不经过所有终止节点,同时还要有环。
现在就是要找环。我们 dfs 一遍,记录一个点历史是否被访问过,同时记录一个点是否在栈中,如果访问到了已经入栈过的点就说明找到了答案。
P4696 [CEOI2011] Matching
题意:给你两个排列 P 、H, 求 H 中 任意一段 连续的 排列 与P相同的序列
思路:因为是匹配,考虑用 kmp。
我们考虑套用 kmp 的过程,那么当前的数 \(i\) 和 P 中的某一个数 \(j\) 相同等价于在当前的区间中 \(i\) 的排名和 \(P_j\) 的排名相同,那么用树状数组维护比一个数小的数,然后在跳 next 的过程中实时维护当前区间的左端点即可。
匹配的过程也类似,实时维护当前区间的左端点,移动左端点时去掉贡献即可。
复杂度 \(O(n+m\log n)\)。
P9523 [JOISC2022] 复制粘贴 3
题意:构造长度为 \(n\) 的目标串,初始 \(X\) 为空,支持三种操作:
- 输入 c,即 \(X\gets X+c\)。
- 剪切,将 \(X\) 剪切到剪切板 \(Y\),并令 \(X\) 为空。
- 粘贴,即 \(X\gets X+Y\)
分别有代价 A,B,C,求得到目标串的最小代价。
思路:设 \(f[l][r]\) 表示当前区间的答案,那么转移有两种,一种是 \(f[l][r]=A+f[l][r-1]\),一种是选择 \(k\),然后将 \(s_{[l,r]}\) 中删掉尽量多的 \(s_{[l,k]}\)。
直接转移复杂度很劣,考虑怎么优化。我们一加上 \(f[l][r]=A+f[l+1][r]\),这样我们进行剪切操作的转移就可以钦定在 \(s_{[l,p]}=s_{[q,r]}\) 时才进行。
预处理出 kmp 数组,然后每次跳 next 来转移,这样做是 \(O(n^2)\) 的。
现在还需快速求出 \([l,r]\) 中可以选出多少个 \([l,p]\)。可以预处理出倍增数组 \(g[l][r][k]\) 表示和 \(s_{[l,r]}\) 相同的不重复的后面 \(2^k\) 个子串的右端点,然后就可以快速求了。
复杂度 \(O(n^2\log n)\)。
P4384 [八省联考 2018] 制胡窜
题意:对于一个字符串 \(s\),我们定义 \(|s|\) 表示 \(s\) 的长度。
接着,我们定义 \(s_i\) 表示 \(s\) 中第 \(i\) 个字符,\(s_{l,r}\) 表示由 \(s\) 中从左往右数,第 \(l\) 个字符到第 \(r\) 个字符依次连接形成的字符串。特别的,如果 \(l \gt r\),或者 \(l \notin [1, |s|]\),或者 \(r \notin [1, |s|]\),我们可以认为 \(s_{l,r}\) 为空串。
给定一个长度为 \(n\) 的仅由数字构成的字符串 \(s\),现在有 \(q\) 次询问,第 \(k\) 次询问会给出 \(s\) 的一个子串 \(s_{l,r}\),请你求出有多少对 \((i, j)\),满足 \(1 \leq i \lt j \leq n\),\(i + 1 < j\),且 \(s_{l,r}\) 出现在 \(s_{1,i}\) 中或 \(s_{i+1,j-1}\)中或 \(s_{j,n}\) 中。
思路:不算太难,但是要分讨。
因为是子串相关,于是考虑用 SAM + 倍增来定位子串,难点在于计算贡献。
首先可以把转成在两个位置 \(i,j\) 切一刀,使得 \(s_{1,i}\),\(s_{i+1,j}\),\(s_{j+1,n}\),三个串中不包含 \(s_{l,r}\) 的方案数。
考虑暴力枚举第一刀切的位置,如果 \(s_{i+1,n}\) 中没有出现 \(s_{l,r}\),那么方案数为 \(n-i+1\);否则找出在 \(s_{i+1,n}\) 中第一次出现的位置 \(s_{p-len+1,p}\) 和最后一次出现的位置 \(s_{q-len+1,q}\),方案数就是 \(p-(q-len+1)\)。
考虑优化。
如果 \(s_{i+1,n}\) 中没有出现过 \(s_{l,r}\),那么求出交集 \(s_{L,R}\),贡献就是 \(\sum\limits_{i=L}^Rn-(i+1)\)。
如果出现过,那么每一次的贡献就是使得 \(s_{l,r}\) 第一次出现在 \(s_{i+1,n}\) 是 \(s_{p-len+1,len}\) 的方案数。切第二刀的方案数是 \(p-(q-len+1)\),切第一刀的方案数是 \(p-pre\),其中 \(pre\) 是最大的满足 \(pre<p,s_{pre-len+1,pre}=s_{l,r}\),那么此时 \(i\) 要满足 \(pre-len+1\le i\le p-len\),方案数就是 \(p-pre\)。
于是记 \(L=max(1,u-len+1),R=min(q-len,f-1)\),其中 \(u\) 是 \(s_{1,q-len}\) 中 \(s_{l,r}\) 最后一次出现的位置,\(f\) 是 \(s_{l,r}\) 第一次出现的位置,那么第一刀的范围就是 \([L,R]\)。
那么对于所有 \(s_{l,r}\) 出现的位置 \(p\),答案是 \(\sum|[pre-len+1,p-len]\cap[L,R]|(p-(q-len+1))\)。
首先要消除 \([L,R]\) 的限制。对于所有 \([pre-len+1,p-len]\in [L,R]\) 的 \((pre,p)\),相当于是 \([L+len-1,R+len]\) 中所有 \(s_{l,r}\) 出现的位置,记为 \(p_i\),那么贡献就是 \(\sum\limits_{i=2}^k(p_i-p_{i-1})(p_i-(q-len+1))+(p_1-(L+len-1))(p_1-(q-len+1))+((R+len)-p_k)(suf-(q-len+1))\)。
关键在于 \(\sum(p_i-p_{i-1})(p_i-(q-len+1))\),减号后面的贡献就是 \((p_k-p_1)(q-len+1)\),考虑前面的部分,相当于是每个位置的下边乘上和前驱的距离,可以用线段树维护。
那么答案化简后就是 \((\sum\limits_{i=1}^k(p_i-p_{i-1})p_i)+(r+len-p_k)suf-(R-L+1)(q-len+1)\)。
复杂度 \(O(n\log n)\)。
标签:子串,选做,进阶,后缀,len,字符串,节点,题意 From: https://www.cnblogs.com/Xttttr/p/18015641