pb_ds
在OI比赛(特别是FJOI)中,常常会出现一类和子序列有关的计数、最优化问题。在以前,这类问题并没有一些比较通用的思考方法,导致了较大的思维复杂度,也使得状态数优化较为困难。
而一类子串问题中,我们引入和后缀自动机(SAM)这一有限状态自动机。在这类问题中,我们左有后缀自动机的简单构造,右有自动机和后缀树的优美性质,SAM常常能所向披靡,轻而易举的解决各类和子串有关的问题。
本文提出了next数组这一能识别一个串所有子序列的“序列自动机”,描述了它的
设next[i][w]表示在原串
显然,最简单的构造方法是
def build(s, next):
n = |s|
next[n][*] = -1
for i = n -> 1:
for j in A:
next[i - 1][j] = next[i][j]
next[i - 1][s[i]] = i
构造完毕后,我们就可以开始使用了。
这里也明显的突出了next数组的缺点:不适合
这里将介绍一些经典的单串问题(不一定是DP)。
给一个串,求一个字典序最小的子序列。显然这是一个非常简单的问题,但我们考虑用next数组来做。
next数组可以看作是一棵缩成DAG的trie树(其实就是trie图),因此我们按照最小字典序在next数组上走就行了。
时间复杂度:
给一个串,求它不同子序列计数。其实就是求next数组扩展成trie树的节点数。
暴力统计显然不行,比如一个1~n的排列,任何子序列均不同,因此答案达到
考虑另一种思路,next数组可以看成DAG,任何一条从0到任意节点的路径都是一个不同的子序列,因此我们统计的就是从0开始的路径条数,DAG上DP即可。并且由于点的编号就是拓扑序,实现起来十分方便,可以不用显式的建出数组。
时间复杂度:
注意到这里的路径和子序列是一一对应的,而DAG上的DP又是我们熟悉的,因此我们能这样不重不漏的统计子序列,这给了我们一个不错的计数方式。
给定一个串,求一个最长的上升子序列。
由于要求上升,则
对于
殊途同归。
时间复杂度:
这个做法可以轻松推广到计数问题上。
一个串是两个串的公共子序列,当且仅当它在两个序列自动机中都有对应的节点。
我们用一个
那么其实我们在做的事就是求一个自动机的并了。可以看作是一个DAG的并,一条边存在当且仅当这条边在两个DAG中都存在。这个新的自动机中,起始状态为
给定两个串,求它们的字典序最小的公共子序列。
在合并了的自动机中,每次按照最小字典序走。
时间复杂度:
给定两个串,求它们的所有公共子序列个数。
我们就在这个新的DAG中做路径条数的DP即可。
时间复杂度:
”可是这个复杂度过不了啊?“
注意到一个状态
并且若
唯一能卡到接近上界的是一种随机的01串,不过这样相等对数也大概为
为了能只取有效状态,避免扩展无效状态,大力资磁用记忆化搜索实现DP。
给你两个串,求它们的最长公共子序列。
转化为DAG之后,我们要求的就是DAG最长路。仍然是熟知的DAG上DP。
时间复杂度:
这个时间复杂度是劣于经典做法的
给三个串
这时,如果只记录二元组
这个自动机的构造也相当显然。设
然后,我们利用三元组
时间复杂度:
同样存在严格
给定
先考虑第4个子问题。如果仍用二元组记录,那么这样的接受状态就是任意
考虑其他子问题。能接受所有子串的自动机就是后缀自动机(并把所有节点标记为接受),那么我们就可以推广到一个一般的问题:给出两个自动机,求出被第一个自动机接受而不被第二个自动机接受的最短串。显然上面那个方法可以推广,因此这个问题就这样被解决了。
时间复杂度:
这个算法同样可以被推广到这一类计数问题上
给出
既然我们能做2个串,我们就也能做k个串:把k个自动机合并起来就OK了。但复杂度不是很优美,也没有什么好的替代方法。
注意到多个串会导致有效状态数大大减小,因此可以用记忆化搜索和哈希表来保存状态。
时间复杂度:
如果加入“至少是
给出两个串
解决子串问题需要用到AC自动机,由于这里需要是所有串的子串,我们还得记录一个
这题的状态相当多,因此我们仍然需要记忆化+哈希存状态。除此之外,我们还可以加一些剪枝,比如如果当前长度就算后面忽略了限制都无法超过答案,就停止扩展此状态。另一个剪枝就是预处理
时间复杂度:
如果转为计数问题,就不能加最优性剪枝。
给出
同样可以使用上升序列自动机的组合进行DP。如果字符集太大,需要离散化字符集。
我们知道对于
时间复杂度:
给出一个串,求其不同回文子序列个数。
考虑从左右两端构造回文串。构造原串及反串的序列自动机,DP。
对于状态
时间复杂度:
作为自动机,next数组解决字典序问题是浑然天成的。同样,增加一维状态表示长度也可以用来做和长度有关的题目。
给出一个串,求其所有不同的子序列长度之和。
一个比较naive的做法,就是增加长度这一维,把状态记为
时间复杂度:
考虑转化为DAG模型,则答案就是每条边走的次数之和乘上边长(这里都是1)。假设当前在节点i,且以next[i][w]开头的串的个数为
时间复杂度:
给出一个串,求其长度在
这下就必须记录长度作为一维状态了。
时间复杂度:
给出一个串,求其长度在
考虑一子序列,它的任意前缀的字典序均比它小,因此
考虑当前在节点i,已有长度为
时间复杂度:
给出两个串,求一个长度在
同样,只需考虑限制
设
时间复杂度:
给出一个串,求其第k小字典序子序列。
相同的子序列算一个,则就相当于在next数组上走,每次考虑走字母w能获得的子序列个数是否不超过k,如果不超过,则这一位为w;否则,令k减去该个数,并考虑下一个字母。如果k为0,则停止并输出答案。
时间复杂度:
给出一个串,求其长度在
考虑修改问题15的统计个数的方式。其实我们只需要用问题12的计数方法即可解决此问题。
注意当
时间复杂度:
序列自动机给了我们一个强有力的理论支撑,让我们可以更加系统的处理一类关于子串的最优化问题,也提供了唯一可以用于子串计数的工具,甚至送给了我们一个绝妙的附属品:关于字典序的处理。