Algorithm Review 1 字符串

字符串

最小表示法

  • s [ i … i + k − 1 ] = s [ j … j + k − 1 ] s[i\dots i+k-1] = s[j\dots j + k -1 ] s[ii+k1]=s[jj+k1] s [ i + k ] > s [ j + k ] s[i+k] > s[j+k] s[i+k]>s[j+k],则对于 0 ≤ p ≤ k 0\le p \le k 0pk,以第 i + p i + p i+p 个字符为首的表示都劣于以第 j + p j + p j+p 个字符为首的表示,可直接略去。
	int k = 0, i = 0, j = 1;
	while (k < n && i < n && j < n) 
	{
 		if (s[(i + k) % n] == s[(j + k) % n]) 
    		++k;
  		else 
		{
   			s[(i + k) % n] > s[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
    		if (i == j) i++;
  	 	 	k = 0;
  		}
	}
	i = Min(i, j);

KMP

  • 设主串为 A A A,模式串为 B B B
  • n e x t [ i ] = max ⁡ { j ∣ 0 ≤ j < i , B [ 1 … j ] = B [ i − j + 1 … i ] } next[i] = \max\{j|0\le j < i, B[1\dots j] =B[i - j + 1\dots i]\} next[i]=max{j∣0j<i,B[1j]=B[ij+1i]}
  • 连边 n e x t [ i ] → i next[i] \to i next[i]i,可构成有根树结构。
		for (int i = 2, j = 0; i <= m; ++i)
		{
			while (j > 0 && b[j + 1] != b[i])
				j = nxt[j];
			if (b[j + 1] == b[i])
				++j;
			nxt[i] = j;
		}
		for (int i = 1, j = 0; i <= n; ++i)
		{
			while (j > 0 && b[j + 1] != a[i])
				j = nxt[j];
			if (b[j + 1] == a[i])
				++j;
			if (j == m)
				++cnt, j = nxt[j];
		}
  • 结论1 长度为 n n n 的串最小循环节的长度为 n − n x t [ n ] n - nxt[n] nnxt[n],若 ( n − n x t [ n ] ) ∣ n (n - nxt[n]) | n (nnxt[n])n,则其为最小周期。
  • 结论2 若长度为 n n n 的串存在长度为 m m m n − m n - m nm 的循环节(即串存在长度为 m m m n − m n - m nm 的前缀与后缀相同),则该串存在 gcd ⁡ ( m , n − m ) \gcd(m, n - m) gcd(m,nm) 的周期。

证明 a = m , b = n − m a = m, b = n - m a=m,b=nm,不妨令 a ≥ b a \ge b ab,由作图易知,该串长度为 ( a m o d    b ) + b (a \mod b) + b (amodb)+b 的后缀存在长度为 a m o d    b a \mod b amodb b b b 的循环节,因此可不断递归下去,其过程与求解最大公约数的过程相同,故最终可得到该串存在 gcd ⁡ ( m , n − m ) \gcd(m, n - m) gcd(m,nm) 的周期。

  • 结论3 字符串循环节的长度一定为最小循环节长度的整数倍

证明 不妨设循环节长度不为其整数倍,由作图易知,最小循环节满足 结论2 的前提,因而可推出其包含更小的周期,与最小循环节的定义矛盾。

Trie

  • 0/1Trie \text{0/1Trie} 0/1Trie 内所有数全局 +1/-1。
    • 以 +1 为例,按低位至高位建树,交换左右子树,若存在进位则递归新的左子树。

AC自动机

  • f a i l [ x ] = y fail[x] = y fail[x]=y,满足 d e p t h [ y ] depth[y] depth[y] 最大且存在模式串 B i , B j B_i,B_j Bi,Bj B i [ 1 … d e p t h [ y ] ] = B j [ d e p t h [ x ] − d e p t h [ y ] + 1 … d e p t h [ x ] ] B_i[1\dots depth[y]] = B_j[depth[x] - depth[y] + 1 \dots depth[x]] Bi[1depth[y]]=Bj[depth[x]depth[y]+1depth[x]]
  • 连边 f a i l [ x ] → x fail[x] \to x fail[x]x,可构成有根树结构。
  • 设字符串总长为 ∣ S ∣ |S| S,构建 AC \text{AC} AC自动机 的时间复杂度为 O ( ∣ S ∣ ) \mathcal O(|S|) O(S),具体可见 AC自动机时空复杂度分析。
inline void buildFail()
{
	for (int i = 0; i < 26; ++i)
		g[0][i] = 1;
	que[qr = 1] = 1;
	for (int i = 1, x, y, v; i <= qr; ++i)
	{
		x = que[i];
		for (int j = 0; j < 26; ++j)
		{
			v = fail[x];
			while (!g[v][j]) v = fail[v];
			v = g[v][j];
			y = g[x][j];
			if (y) fail[y] = v, que[++qr] = y;
				else g[x][j] = v, fg[x][j] = true; 
				// fg 表示是否是原 Trie树 上的边
		}
	}
}

Manacher

  • 为了避免奇偶讨论问题,在字符串每一位两侧都添加同一个特殊字符,在字符串首位前和末位后添加两个不同的特殊字符。
		scanf("%s", t + 1);
		m = strlen(t + 1);
		s[n = 0] = '@';
		for (int i = 1; i <= m; ++i)
		{
			s[++n] = '#';
			s[++n] = t[i];
		}
		s[++n] = '#';
		s[n + 1] = '&';
  • 辅助变量 m x mx mx p p p,表示已有的回文半径覆盖到的最右边界和对应中心的位置。
  • max ⁡ 1 ≤ i ≤ n { r [ i ] − 1 } \max\limits_{1\le i \le n}\{r[i] -1\} 1inmax{r[i]1} 表示原串最长的回文串长度。
  • 注意数组大小需要开到原串的两倍
inline void Manacher(char *s)
{	
	int p = 0, mx = 0;
	for (int i = 1; i <= n; ++i)
	{
		r[i] = mx > i ? Min(r[(p << 1) - i], mx - i) : 1;
		while (s[i - r[i]] == s[i + r[i]])
			++r[i];
		if (i + r[i] > mx)
			mx = i + r[i], p = i;
	}
}

典例 CF17E

题目大意

  • 求长度为 n n n 的字符串相交的回文子串对数, n ≤ 2 × 1 0 6 n \le 2\times 10^6 n2×106

解法1

  • 先用 Manacher \text{Manacher} Manacher 求出每各个位置的回文半径。
  • 考虑枚举靠右的回文串中心,统计与之相交的回文串个数。
  • 对于从左到右的每一个回文中心 i i i,依次进行如下操作:
    1. 对原串中最长回文串左半部分对应的区间求和。
    2. 对原串中最长回文串右半部分对应的区间加上一个首项为 ⌊ r [ i ] 2 ⌋ \lfloor \frac{r[i]}{2} \rfloor 2r[i]、公差为 -1 的等差数列。
    3. 对原串中回文中心左侧的所有位置加上常数 ⌊ r [ i ] 2 ⌋ \lfloor \frac{r[i]}{2} \rfloor 2r[i]
  • 最后还要加上回文中心重合的情况,即 ∑ C ⌊ r [ i ] 2 ⌋ 2 \sum C_{\lfloor \frac{r[i]}{2} \rfloor}^2 C2r[i]2
  • 直接用线段树实现常数较大。
  • 操作 2 对操作 1 可以通过多阶差分 O ( n ) \mathcal O(n) O(n) 实现,操作 3 对操作 1 的影响可以通过常数较小的树状数组实现,总时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

解法2

  • 考虑用总对数减去不相交的对数。
  • c i c_i ci 为回文串最右侧位置为 i i i 的个数, d i d_i di 为回文串最左侧位置为 i i i 的个数。
  • 记串长为 m m m,则不相交的对数为 ∑ i = 1 m − 1 c i ∑ j = i + 1 m d j \sum \limits_{i = 1}^{m - 1}c_i\sum\limits_{j = i+1}^{m}d_j i=1m1cij=i+1mdj
  • c i , d i c_i, d_i ci,di 易由差分求得,时间复杂度 O ( n ) \mathcal O(n) O(n)

Z Algorithm

  • 对于长度为 n n n 的字符串 s s s,以 1 为起点,定义函数 z [ i ] z[i] z[i] 表示 s [ 1 , n ] s[1,n] s[1,n] s [ i , n ] s[i,n] s[i,n] 的最长公共前缀的长度。
  • 我们称 [ i , i + z [ i ] − 1 ] [i,i+z[i] - 1] [i,i+z[i]1] i i i 的匹配段,也叫 Z-box \text{Z-box} Z-box
  • 算法的过程中我们维护目前右端点最靠右的匹配段,记作 [ l , r ] [l,r] [l,r],初始时可令 l = r = 1 l = r = 1 l=r=1
  • 显然 z [ 1 ] = n z[1] = n z[1]=n,另外因其较为特殊,我们从 i = 2 i = 2 i=2 开始计算 z [ i ] z[i] z[i]
    • i ≤ r i \le r ir,有 s [ i , r ] = s [ i − l + 1 , r − l + 1 ] s[i,r] = s[i - l + 1, r - l + 1] s[i,r]=s[il+1,rl+1],因此 z [ i ] ≥ min ⁡ { z [ i − l + 1 ] , r − i + 1 } z[i] \ge \min\{z[i - l + 1],r - i + 1\} z[i]min{z[il+1],ri+1}
      • z [ i − l + 1 ] < r − i + 1 z[i - l + 1] < r - i + 1 z[il+1]<ri+1,则 z [ i ] = z [ i − l + 1 ] z[i] = z[i - l + 1] z[i]=z[il+1]
      • z [ i − l + 1 ] ≥ r − i + 1 z[i - l + 1] \ge r - i + 1 z[il+1]ri+1,令 z [ i ] = r − i + 1 z[i] = r - i + 1 z[i]=ri+1,之后暴力扩展即可。
    • i > r i > r i>r,同样暴力扩展即可。
    • 求出 z [ i ] z[i] z[i] 更新 l , r l, r l,r
  • 显然每次暴力扩展必定会使 r r r 右移,因此时间复杂度 O ( n ) \mathcal O(n) O(n)
	z[1] = m;
	for (int i = 2, l = 1, r = 1; i <= tm; ++i)	
		if (i <= r && z[i - l + 1] < r - i + 1)
			z[i] = z[i - l + 1];	
		else
		{
			z[i] = Max(0, r - i + 1);
			while (i + z[i] <= tm && t[i + z[i]] == t[z[i] + 1])
				++z[i];
			if (i + z[i] - 1 > r)
				l = i, r = i + z[i] - 1;
		}

字符串匹配

  • t t t 为文本串, p p p 为模式串,构造 s = p + ⋄ + t s = p + \diamond + t s=p++t,其中 ⋄ \diamond 为不在 p , t p,t p,t 中出现的分隔字符。
  • 容易求出 t t t 中与 p p p 匹配的每个子串所在位置。

本质不同子串数

  • 考虑计算在当前串 s s s 末尾添加一个字符 c c c 对本质不同子串数产生的增量。
  • 设串 t t t s + c s + c s+c 的反串,求其 Z \text{Z} Z 函数,易知 ∣ t ∣ − max ⁡ 1 ≤ i ≤ ∣ t ∣ { z [ i ] } |t| - \max\limits_{1\le i\le |t|}\{z[i]\} t1itmax{z[i]} 为增量。
  • 总时间复杂度 O ( n 2 ) \mathcal O(n^2) O(n2),同时可用于 O ( n ) \mathcal O(n) O(n) 计算增加/删除末尾或开头的一个字符后本质不同子串数的变化量。

后缀数组

  • s u f s ( i ) suf_s(i) sufs(i) 表示字符串 s s s 位置 i i i 开始的后缀,后缀数组 s a [ i ] sa[i] sa[i] 指的是将所有后缀按照字典序排序后得到的数组。
  • 常用的后缀数组的计算方法为倍增法,主要思路即已知从每个位置开始往后 2 k 2^k 2k 个字符的子串的排序结果,就能推出从每个位置开始往后 2 k + 1 2^{k + 1} 2k+1 个字符的子串的排序结果,时间复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn)
  • 定义 h e i g h t [ i ] height[i] height[i] s u f s ( s a [ i − 1 ] ) suf_s(sa[i - 1]) sufs(sa[i1]) s u f s ( s a [ i ] ) suf_s(sa[i]) sufs(sa[i]) 的最长公共前缀,则 s ( j ) s(j) s(j) s ( k ) s(k) s(k) 的最长公共前缀(设 r a n k [ j ] < r a n k [ k ] rank[j] < rank[k] rank[j]<rank[k])为 min ⁡ r a n k [ j ] < i ≤ r a n k [ k ] { h e i g h t [ i ] } \min\limits_{rank[j] < i \le rank[k]}\{height[i]\} rank[j]<irank[k]min{height[i]},常用 ST \text{ST} ST表 维护。
  • 易证 h e i g h t [ r a n k [ i ] ] ≥ h e i g h t [ r a n k [ i − 1 ] ] − 1 height[rank[i]] \ge height[rank[i - 1]] - 1 height[rank[i]]height[rank[i1]]1,可 O ( n ) \mathcal O(n) O(n) 预处理出 h e i g h t height height 数组。
const int N = 1e6 + 5;
int rank[N], height[N], sa[N], w[N];
int n, r; char s[N];

inline bool Equal(int *x, int a, int b, int k)
{
	if (x[a] != x[b])
		return false;
	else
	{
		int p = a + k > n ? -1 : x[a + k],
			q = b + k > n ? -1 : x[b + k];
		return p == q; 
	}
}

inline void initSA()
{	
	int *x = rank, *y = height;
	r = 255;
	for (int i = 1; i <= n; ++i)
		++w[s[i]];
	for (int i = 2; i <= r; ++i)
		w[i] += w[i - 1];
	for (int i = n; i >= 1; --i)
		sa[w[s[i]]--] = i;
	x[sa[1]] = r = 1;
	for (int i = 2; i <= n; ++i)
		x[sa[i]] = s[sa[i - 1]] == s[sa[i]] ? r : ++r;
	for (int k = 1; r < n; k <<= 1)
	{
		int yn = 0;
		for (int i = n - k + 1; i <= n; ++i)
			y[++yn] = i;
		for (int i = 1; i <= n; ++i)
			if (sa[i] > k)
				y[++yn] = sa[i] - k;
		
		for (int i = 1; i <= r; ++i)
			w[i] = 0;
		for (int i = 1; i <= n; ++i)
			++w[x[y[i]]];
		for (int i = 2; i <= r; ++i)
			w[i] += w[i - 1];
		for (int i = n; i >= 1; --i)
			sa[w[x[y[i]]]--] = y[i];
		
		std::swap(x, y); 
		x[sa[1]] = r = 1;
		for (int i = 2; i <= n; ++i)
			x[sa[i]] = Equal(y, sa[i - 1], sa[i], k) ? r : ++r;
	} 
	for (int i = 1; i <= n; ++i)
		rank[i] = x[i];
		
	for (int i = 1, j, k = 0; i <= n; ++i)
	{
        if (rank[i] == 1)
            continue ;
		k ? --k : 0;
		j = sa[rank[i] - 1];
		while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) ++k;
		height[rank[i]] = k;
	}
    height[1] = 0;
}

常见应用

  • 字符串 t t t s s s 中至少出现两次,则称 t t t s s s 的重复子串。
  • 可重叠最长重复子串 h e i g h t height height 数组的最大值。
  • 不可重叠最长重复子串 二分答案 k k k,将排序后的后缀分成若干组,每组的后缀之间的 h e i g h t height height 值都不小于 k k k,则只需判断是否存在一组中 s a sa sa 值的极差不小于 k k k
  • 可重叠的 k k k 次最长重复子串 同样二分答案后分组,判断是否存在一组后缀个数不小于 k k k
  • 上述做法可以扩展到多个字符串的问题中,只需将多个字符串连接起来、不同的字符串用互不相同的分隔字符隔开求后缀数组。
  • 重复次数最多的连续重复子串
    • 先分别求出正串和反串的后缀数组。
    • 穷举长度 L L L,求长度为 L L L 的子串最多能连续重复几次。
    • 枚举 i ( 0 ≤ i ≤ ⌊ n L ⌋ ) i(0\le i \le \lfloor \frac{n}{L}\rfloor) i(0iLn⌋),求出 s [ L i + 1 ] s[Li+1] s[Li+1] s [ L ( i + 1 ) + 1 ] s[L(i + 1) + 1] s[L(i+1)+1] 向前和向后能匹配到多远,记这个总长度为 K K K,则这里连续重复了 ⌊ K L ⌋ + 1 \lfloor\frac{K}{L}\rfloor+1 LK+1 次,最后取所有情况的最大值。
    • 时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)
  • 本质不同的子串个数 易知为 ∑ i = 1 n ( n − s a [ i ] + 1 − h e i g h t [ i ] ) = n ( n + 1 ) 2 − ∑ i = 1 n h e i g h t [ i ] \sum \limits_{i = 1}^{n}(n-sa[i]+1 - height[i]) = \frac{n(n+1)}{2} - \sum \limits_{i = 1}^{n}height[i] i=1n(nsa[i]+1height[i])=2n(n+1)i=1nheight[i]
  • 最长公共子串 求串 a a a 和串 b b b 的最长公共子串,构造 s = a + ⋄ + b s = a + \diamond + b s=a++b,其中 ⋄ \diamond 为不在 a , b a,b a,b 中出现的分隔字符,求出 s s s h e i g h t height height 数组,则答案即对应的两个后缀不来自同一个串的 h e i g h t height height 的最大值。
  • 长度不小于 k k k 的公共子串个数
    • 求串 a a a 和串 b b b 的公共子串的个数,两个公共子串不同当且仅当它们在 a a a b b b 中的位置不同。
    • 与上一个问题做法相同,此时即求 a a a 的所有后缀与 b b b 的所有后缀之间的最长公共前缀之和。
    • 正反向各扫描一遍,每遇到一个 b b b 的后缀就统计与前面 a a a 的每一个后缀产生的贡献,显然可用单调栈维护。
  • 给定 n n n 个字符串,求每个字符串中是至少 k k k 个字符串的子串的子串个数
    • n n n 个字符串相连,不同的字符串用互不相同的分隔字符隔开,求出其 h e i g h t height height 数组。
    • 考虑计算每个后缀满足条件的最长前缀,对于任意包含恰好 k k k 个字符串的后缀的区间,对区间内每个后缀产生的贡献是区间内 h e i g h t height height 的最小值。
    • 枚举 l l l,尺取得到最小的 r r r 满足 [ l , r ] [l,r] [l,r] 内恰好包含 k k k 个字符串的后缀,用线段树做区间修改。
    • 问题是每次对 r r r 继续右移直至到达字符串尾的每个位置也有一定贡献,不难发现这一贡献实际上是区间修改的值与中间 h e i g h t height height min ⁡ \min min 的结果,实际上只要在最后正序枚举 i i i,用 i i i 的答案和 h e i g h t [ i + 1 ] height[i + 1] height[i+1] 的最小值更新 i + 1 i + 1 i+1 的答案即可。
    • 时间复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn)

后缀自动机

  • 参考 OI Wiki 后缀自动机 Meatherm的博客,这里只作简要论述。

基本概念及特点

  • 字符串 s s s SAM \text{SAM} SAM 是一个接受 s s s 所有后缀的最小 DFA \text{DFA} DFA(确定性有限状态自动机)。
  • 具体来说, SAM \text{SAM} SAM 具有以下特点:
    • 是一张边上标有字符的有向无环图。
    • 图中的结点被称为状态,边称为状态间的转移
    • 存在一个起始状态 s t st st 和若干个终止状态,每一个后缀均对应一条 s t st st 到某个终止状态的路径。总存在恰好一条 s t st st 到某个结点的路径,使得路径上所有转移连接起来的字符串对应某个子串。
    • 对于字符串的任意非空子串 t t t,记 t t t s s s 中所有结束位置构成的集合为 endpos ( t ) \text{endpos}(t) endpos(t),任意两个子串的 endpos \text{endpos} endpos 集合只可能存在包含或者不交的关系。
    • 每个状态 x x x 实际上对应着 endpos \text{endpos} endpos 集合相同的所有子串,这些子串彼此为后缀关系且长度连续,记 longest ( x ) \text{longest}(x) longest(x) 表示状态 x x x 对应的最长的字符串,其长度为 len ( x ) \text{len}(x) len(x)
    • 设后缀链接 link ( x ) \text{link}(x) link(x) 表示与 x x x 对应的 endpos \text{endpos} endpos 集合不同且 longest \text{longest} longest 小于 x x x 且最长的状态。则 link ( x ) \text{link}(x) link(x) 对应的所有子串均为 x x x 对应的所有子串的后缀, x x x 对应的 endpos \text{endpos} endpos 集合是 link ( x ) \text{link}(x) link(x) 对应的 endpos \text{endpos} endpos 集合的真子集, x x x 对应的所有子串的长度区间为 [ len(link ( x ) ) + 1 , len ( x ) ] [\text{len(link}(x))+1,\text{len}(x)] [len(link(x))+1,len(x)]
    • 对于除了初始状态 s t st st 以外的状态 x x x,将 link ( x ) \text{link}(x) link(x) x x x 连边可得到一棵有向树,树上非叶子结点的 endpos \text{endpos} endpos 集合即为其子结点的 endpos \text{endpos} endpos 集合的并,我们称其为 link \text{link} link 树。对于任意一个非空子串 t t t,找到其对应的状态 x x x,不断沿着 link ( x ) \text{link}(x) link(x) 往上跳,就能找到 t t t 的所有后缀。树上任意两点的 LCA \text{LCA} LCA len \text{len} len 值就是这两点对应的所有子串的最长公共后缀。

线性构造及证明

  • 具体构造过程见代码,这里主要分析其时间复杂度。
int V_sam, last;
int sze[N2];

struct sam
{
	int g[26];
	int len, link;
	
	inline void Clear()
	{
		memset(g, 0, sizeof(g));
		len = link = 0;
	}
}tr[N2];

inline void Init()
{
	tr[V_sam = last = 1].Clear();
}

inline void Extend(char ch)
{
	int cur = ++V_sam;
	tr[cur].Clear();
	ch -= 'a';
	tr[cur].len = tr[last].len + 1;
	sze[cur] = 1;
	int p, q, clone;
	for (p = last; p && !tr[p].g[ch]; p = tr[p].link)
		tr[p].g[ch] = cur;
	if (!p)
		tr[cur].link = 1;
	else
	{
		q = tr[p].g[ch];
		if (tr[q].len == tr[p].len + 1)
			tr[cur].link = q;
		else
		{
			tr[clone = ++V_sam] = tr[q];
			tr[cur].link = tr[q].link = clone;
			tr[clone].len = tr[p].len + 1;
			for (; p && tr[p].g[ch] == q; p = tr[p].link)
				tr[p].g[ch] = clone;
		}
	}
	last = cur;	 
}
  • 结论1 对于一个长度为 n n n 的字符串 s s s,其 SAM \text{SAM} SAM 的状态数不超过 2 n − 1 2n - 1 2n1

证明 SAM \text{SAM} SAM 的构造方法可知,初始时有一个状态,前两次每次一定只会增加一个状态,之后每次至多只会增加两个状态,总状态数不超过 2 n − 1 2n - 1 2n1

  • 结论2 对于一个长度为 n n n 的字符串 s s s,其 SAM \text{SAM} SAM 的转移数不超过 3 n − 4 3n - 4 3n4

证明 对于转移 ( p , q ) (p,q) (p,q),若 len ( p ) + 1 = len ( q ) \text{len}(p) + 1 =\text{len}(q) len(p)+1=len(q),则称该转移为连续的,否则为不连续的。考虑从 s t st st 开始的所有最长路径的生成树,生成树只包含连续的边,因此数量少于状态数,即连续的转移数不超过 2 n − 2 2n - 2 2n2

对于不连续的转移 ( p , q ) (p,q) (p,q),设其字符为 c c c,我们取它对应的字符串 s ′ = u + c + w s' = u + c + w s=u+c+w u u u 为初始状态到 p p p 的最长路径, w w w q q q 到终止状态的最长路径。显然每个转移对应的字符串 s ′ s' s 是不同的,且 s ′ s' s 是原串 s s s 的后缀,因为 s s s 只有 n n n 个非空后缀且 s ′ s' s 一定不会取到 s s s,所以不连续转移数不超过 n − 1 n - 1 n1

因此我们可以得到转移数的上界 3 n − 3 3n - 3 3n3,实际可以构造出的上界为 3 n − 4 3n - 4 3n4

  • 整个构造算法中,复杂度不确定的只有两次 for 语句和 c l o n e clone clone 复制转移。
    • 对于第一处 for 语句和 c l o n e clone clone 复制转移,操作次数显然不会超过总转移数,因而是线性的。
    • 对于第二处 for 语句,可以证明, c u r cur cur 的后缀链接链是 l a s t last last 的后缀链接链通过一条 c h ch ch 出边所到达的状态组成的子集。设 l i l_i li s [ 1 , i ] s[1,i] s[1,i] 对应状态的后缀链接链长, k i k_i ki 为添加第 i i i 个字符时的迭代次数,因此有 l i ≤ l i − 1 + 2 − k i l_i \le l_{i - 1}+2 - k_i lili1+2ki l 0 = 0 l_0 = 0 l0=0,因此 0 < ∑ i = 1 n ( 2 − k i ) ≤ n 0 < \sum \limits_{i = 1}^{n}(2 - k_i) \le n 0<i=1n(2ki)n,所以总迭代次数 ∑ i = 1 n k i \sum \limits_{i = 1}^{n}k_i i=1nki 是线性的。

常见应用

  • 最小表示法 构造 a = s + s a = s + s a=s+s 的后缀自动机,在后缀自动机贪心地走 n n n 步。
  • 字典序第 k k k 小子串 预处理出 f p = 1 + ∑ ( p , q ) f q f_p = 1 + \sum\limits_{(p,q)}f_q fp=1+(p,q)fq,表示从初始状态到达 p p p 再任意走能够得到的子串数,根据 f p f_p fp 贪心即可。
  • 最长公共子串 求串 a a a 和串 b b b 的最长公共子串,建出 a a a 的后缀自动机,不断沿着串 b b b 中每个字符对应的转移边走,若不存在对应的转移边则沿着后缀链接链往上跳,直到存在对应的转移边,每走一条转移边就更新一次答案,易知均摊复杂度为线性。
  • link \text{link} link 树上用线段树合并即可维护每个状态的 endpos \text{endpos} endpos 集合。
  • 强制在线,两种操作:在当前串后加上一个字符串,询问某串在原串中的出现次数。
    • Link Cut Tree \text{Link Cut Tree} Link Cut Tree 维护 link \text{link} link 树,并维护虚子树大小,注意 SAM \text{SAM} SAM 初始结点的 s z e sze sze 为 0/1,在平衡树上维护时应做区分,具体实现可直接记在 v s z e vsze vsze 数组中。

广义后缀自动机

  • 构建多串的后缀自动机,大致过程如下:
    • 将所有字符串插入到字典树中。
    • 按照 BFS \text{BFS} BFS 序,对每个结点在原字典树上进行构建。
    • 因为要考虑原字典树上的边,具体构建过程会略有区别。
  • 设字典树的总结点数为 n n n,构建的总复杂度为 O ( n ) \mathcal O(n) O(n)
inline void Extend(int last, char ch)
{
	cur = tr[last].g[ch];
	tr[cur].len = tr[last].len + 1;
	int p, q, clone;
	for (p = tr[last].link; p && !tr[p].g[ch]; p = tr[p].link)
		tr[p].g[ch] = cur; //p 应从 link(last) 开始循环
	if (!p)
		tr[cur].link = 1;
	else
	{
		q = tr[p].g[ch];
		if (tr[q].len == tr[p].len + 1)
			tr[cur].link = q;
		else
		{
			clone = ++V_sam; 
			tr[clone].link = tr[q].link;
			tr[clone].len = tr[p].len + 1;
			for (int j = 0; j < 26; ++j)
				if (tr[q].g[j] && tr[tr[q].g[j]].len)
					tr[clone].g[j] = tr[q].g[j];
            //注意复制结点的转移边时不能复制是原字典树上但不是当前后缀自动机上的转移边
			tr[cur].link = tr[q].link = clone;
			for (; p && tr[p].g[ch] == q; p = tr[p].link)
				tr[p].g[ch] = clone;
		}
	}
}

inline void buildGSAM()
{
	for (int j = 0; j < 26; ++j)
		if (tr[1].g[j])
			que[++qr] = std::make_pair(1, j);
	for (int i = 1, u, v, x; i <= qr; ++i)
	{
		u = que[i].first, v = que[i].second;
		Extend(u, v);
		x = tr[u].g[v];
		for (int j = 0; j < 26; ++j)
			if (tr[x].g[j])
				que[++qr] = std::make_pair(x, j);	
	}
}
  • 结论 设插入广义后缀自动机的字符串分别为 S 1 , S 2 , … , S n S_1,S_2,\dots,S_n S1,S2,,Sn,且 L = ∑ i = 1 n ∣ S i ∣ L = \sum \limits_{i = 1}^{n}|S_i| L=i=1nSi c n t x cnt_x cntx 定义为广义后缀自动机结点 x x x 所对应的所有字符串在 n n n 个字符串中的 c n t x cnt_x cntx 个出现过,则 O ( ∑ c n t x ) = O ( L L ) \mathcal O(\sum cnt_x) = \mathcal O(L\sqrt{L}) O(cntx)=O(LL )

证明 因为总结点数为 O ( L ) \mathcal O(L) O(L),对于任意一个串 S i S_i Si,它对答案的贡献为 O ( min ⁡ { ∣ S i ∣ 2 , L } ) \mathcal O(\min\{|S_i|^2,L\}) O(min{Si2,L})

  • ∣ S i ∣ > L |S_i| > \sqrt L Si>L ,这样的串的数量不会超过 O ( L ) \mathcal O(\sqrt L) O(L ) ,总复杂度 O ( L L ) \mathcal O(L\sqrt L) O(LL )
  • ∣ S i ∣ ≤ L |S_i| \le \sqrt L SiL ,总复杂度 O ( ∑ i = 1 n ∣ S i ∣ 2 ) = O ( L max ⁡ i = 1 n { ∣ S i ∣ } ) = O ( L L ) \mathcal O(\sum \limits_{i = 1}^{n}|S_i|^2)=\mathcal O(L\max\limits_{i=1}^{n}\{|S_i|\})=\mathcal O(L\sqrt L) O(i=1nSi2)=O(Li=1maxn{Si})=O(LL )

序列自动机

  • 字符串 s s s 的序列自动机为能够识别 s s s 所有子序列的 D F A DFA DFA
  • 设字符集为 C C C,字符串长度为 n n n,构建的时间复杂度为 O ( n ∣ C ∣ ) \mathcal O(n|C|) O(nC)
struct Seq_AM
{
	int lst[52], T;
	node tr[N];
	
	inline void Init()
	{
		T = 1;
		for (int i = 0; i < 52; ++i)
			lst[i] = 1; 
	}
	
	inline void Insert(char ch)
	{
		int c = islower(ch) ? ch - 'a' + 26 : ch - 'A', x;
		tr[x = ++T].par = lst[c];
		for (int i = 0; i < 52; ++i)
			for (int j = lst[i]; j && !tr[j].ch[c]; j = tr[j].par) 
				tr[j].ch[c] = x;
		lst[c] = x; 
	}
};

回文自动机

基本概念及构建

  • 严格来说,回文自动机并不满足 D F A DFA DFA 的定义,可将字符串 s s s 的回文自动机视为能识别 s s s 所有回文子串的中心及右半部分的有限状态自动机。
  • 为了处理奇偶回文串的情况,在回文自动机中分设两个初态,表示长度为 -1 和 0 的回文串,称其为奇根和偶根。
  • 类似后缀自动机,回文自动机通过增量法构造,设 fail ( x ) \text{fail}(x) fail(x) 表示结点 x x x 所代表回文串的最长回文后缀所对应的结点,则偶根的 fail \text{fail} fail 指针指向奇根,奇根总能适配。连边 x → fail ( x ) x \to \text{fail}(x) xfail(x),则所有结点形成一树形结构,称其为回文树。
  • 由于回文串的对称性,每次增加一个字符后至多产生一个新的本质不同的回文子串,因而回文自动机的结点数为 O ( ∣ s ∣ ) \mathcal O(|s|) O(s),每次创建新结点只会使回文树上的当前深度增加 1,因而求新结点 fail \text{fail} fail 指针的过程均摊 O ( 1 ) \mathcal O(1) O(1),即构建的总复杂度为 O ( ∣ s ∣ ) \mathcal O(|s|) O(s)
namespace pam
{
	int Vp, lenp, last;
	int cnt[N], dep[N]; 
    // cnt[x] 记录结点 x 代表回文子串的出现次数,deo[x] 表示结点 x 在回文树中的深度
	int ch[N][26], len[N], fail[N];
	char s[N];
	
	inline int newNode(int l)
	{
		++Vp;
		memset(ch[Vp], 0, sizeof(ch[Vp]));
		len[Vp] = l;
		fail[Vp] = cnt[Vp] = dep[Vp] = 0;
		return Vp;
	}
	
	inline void Clear()
	{
		Vp = -1;
		last = 0;
		s[lenp = 0] = '$'; // 避免访问越界
		newNode(0); 
		newNode(-1); 
		fail[0] = 1; // fail[偶根] -> 奇根
	}
	
	inline int getFail(int x)
	{
		while (s[lenp - len[x] - 1] != s[lenp])
			x = fail[x];
		return x;
	}
	
	inline void Extend(char c)
	{
		s[++lenp] = c;
		int now = getFail(last);
		if (!ch[now][c - 'a'])
		{
			int x = newNode(len[now] + 2);
			fail[x] = ch[getFail(fail[now])][c - 'a'];
			dep[x] = dep[fail[x]] + 1;
			ch[now][c - 'a'] = x;
		}
		last = ch[now][c - 'a'];
		++cnt[last];
	}
	
	inline void initTree()
	{
		for (int i = Vp; i >= 2; --i)
			cnt[fail[i]] += cnt[i];	
	}
}

常见应用

回文子串个数

  • 总回文子串的个数即所有结点在回文树上的深度之和(设奇根和偶根的深度均为 0)。

  • 本质不同的回文子串个数即回文自动机除去奇根、偶根的结点数。

双向增加字符

  • 由于回文串的对称性,双向增加字符时 fail \text{fail} fail 指针可以共用。
namespace pam
{
	int Vp, lp, rp, lastl, lastr; 
	ll tot;
	int cnt[N], dep[N];
	int ch[N][26], len[N], fail[N];
	char s[N];
	
	inline int newNode(int l)
	{
		++Vp;
		memset(ch[Vp], 0, sizeof(ch[Vp]));
		len[Vp] = l;
		fail[Vp] = cnt[Vp] = dep[Vp] = 0;
		return Vp;
	}
	
	inline void Clear()
	{
		Vp = -1;
		tot = 0;
		lastl = lastr = 0;
		for (int i = 0; i < N; ++i)
			s[i] = '$';
		lp = N + 1 >> 1;
		rp = lp - 1;		
		newNode(0); 
		newNode(-1); 
		fail[0] = 1; // fail[even root] -> odd root
	}
	
	inline int getFailL(int x)
	{
		while (s[lp + len[x] + 1] != s[lp])
			x = fail[x];
		return x;
	}
	
	inline int getFailR(int x)
	{
		while (s[rp - len[x] - 1] != s[rp])
			x = fail[x];
		return x;
	}
	
	inline void ExtendL(char c)
	{
		s[--lp] = c;
		int now = getFailL(lastl);
		if (!ch[now][c - 'a'])
		{
			int x = newNode(len[now] + 2);
			fail[x] = ch[getFailL(fail[now])][c - 'a'];
			dep[x] = dep[fail[x]] + 1;
			ch[now][c - 'a'] = x;
		}
		lastl = ch[now][c - 'a'];
		tot += dep[lastl];
		++cnt[lastl];
		if (len[lastl] == rp - lp + 1)
			lastr = lastl;
	}
	
	inline void ExtendR(char c)
	{
		s[++rp] = c;
		int now = getFailR(lastr);
		if (!ch[now][c - 'a'])
		{
			int x = newNode(len[now] + 2);
			fail[x] = ch[getFailR(fail[now])][c - 'a'];
			dep[x] = dep[fail[x]] + 1;
			ch[now][c - 'a'] = x;
		}
		lastr = ch[now][c - 'a'];
		tot += dep[lastr];
		++cnt[lastr];
		if (len[lastr] == rp - lp + 1)
			lastl = lastr;
	}
	
	inline void initTree()
	{
		for (int i = Vp; i >= 2; --i)
			cnt[fail[i]] += cnt[i];	
	}
}

回文划分

  • 若题目要求(或可通过一定的转化)将原串划分若干个具有限制的回文串,且 DP 的转移形式大致形如
    f i = ( ∑ s [ j + 1 … i ] 是回文串 f j ) + C   或   f i = min ⁡ s [ j + 1 … i ] 是回文串 / max ⁡ s [ j + 1 … i ] 是回文串 { f j } + C f_i = \left(\sum\limits_{s[j+1\dots i]是回文串}f_j \right)+ C\ \ 或\ \ f_i = \min\limits_{s[j+1\dots i]是回文串} /\max\limits_{s[j+1\dots i]是回文串}\{f_j\} + C fi= s[j+1i]是回文串fj +C    fi=s[j+1i]是回文串min/s[j+1i]是回文串max{fj}+C
    则可通过回文树将每次转移的复杂度优化至 O ( log ⁡ ∣ s ∣ ) \mathcal O(\log |s|) O(logs)

  • 周期 0 < p ≤ ∣ s ∣ , ∀ 1 ≤ i ≤ ∣ s ∣ − p , s [ i ] = s [ i + p ] 0 < p \le |s|,\forall 1 \le i \le |s| - p, s[i] = s[i + p] 0<ps,∀1isp,s[i]=s[i+p],则 p p p s s s 的周期。

  • border 0 ≤ r < ∣ s ∣ 0\le r < |s| 0r<s s [ 1 … r ] = s [ ∣ s ∣ − r + 1 … ∣ s ∣ ] s[1\dots r] = s[|s| - r + 1\dots|s|] s[1r]=s[sr+1s],则 s [ 1 … r ] s[1\dots r] s[1r] s s s 的 border。

  • 由周期和 border 的定义不难证明, t t t s s s 的 border,当且仅当 ∣ s ∣ − ∣ t ∣ |s| - |t| st s s s 的周期。

  • 由该结论及回文串的对称性可顺序推导出下面四条引理:

    • 引理1 t t t 是回文串 s s s 的后缀, t t t s s s 的 border 当且仅当 t t t 是回文串。
    • 引理2 t t t s s s 的 border 且 ∣ s ∣ ≤ 2 ∣ t ∣ |s|\le 2|t| s2∣t s s s 是回文串当且仅当 t t t 是回文串。
    • 引理3 t t t 是回文串 s s s 的 border, ∣ s ∣ − ∣ t ∣ |s|-|t| st s s s 的最小周期当且仅当 t t t s s s 的最长真回文后缀。
    • 引理4 x x x 是一个回文串, y y y x x x 的最长真回文后缀, z z z y y y 的最长真回文后缀,若 x = u y , y = v z x = uy,y = vz x=uy,y=vz,则:
      1. ∣ u ∣ ≥ ∣ v ∣ |u| \ge |v| uv
      2. ∣ u ∣ > ∣ v ∣ |u| > |v| u>v,则 ∣ u ∣ > ∣ z ∣ |u| > |z| u>z
      3. ∣ u ∣ = ∣ v ∣ |u| = |v| u=v,则 u = v u = v u=v

    证明(前三条引理的证明较为容易,这里仅证明 引理4

    1. 引理3 ∣ u ∣ = ∣ x ∣ − ∣ y ∣ |u| = |x| - |y| u=xy x x x 的最小周期, ∣ v ∣ = ∣ y ∣ − ∣ z ∣ |v| = |y| - |z| v=yz y y y 的最小周期。假设 ∣ u ∣ < ∣ v ∣ |u|<|v| u<v,则因为 ∣ u ∣ |u| u 也是 y y y 的周期,与 ∣ v ∣ |v| v y y y 的最小周期矛盾。
    2. 如下图所示,因为 y y y x x x 的 border,所以 v v v x x x 的前缀,设字符串 x = v w x = vw x=vw,所以 z z z w w w 的 border。假设 ∣ u ∣ ≤ ∣ z ∣ |u| \le |z| uz,那么 ∣ z u ∣ ≤ 2 ∣ z ∣ |zu| \le 2|z| zu2∣z,由 引理2,因为 z z z 是回文串所以 w w w 是回文串,又因为 ∣ u ∣ > ∣ v ∣ |u|>|v| u>v,所以 ∣ w ∣ > ∣ y ∣ |w|>|y| w>y,与 y y y x x x 的最长真回文后缀矛盾。Algorithm Review 1 字符串_第1张图片
    3. u , v u,v u,v 都是 x x x 的前缀, ∣ u ∣ = ∣ v ∣ |u|=|v| u=v,所以 u = v u = v u=v
  • 结论1 s s s 的所有回文后缀按长度排序后,可划分成 O ( log ⁡ ∣ S ∣ ) \mathcal O(\log |S|) O(logS) 段等差数列。

证明 s s s 的所有回文后缀长度从小到大排序为 l 1 , l 2 , … , l k l_1,l_2,\dots,l_k l1,l2,,lk,对于相邻两段等差数列的分界处,有 l i − l i − 1 ≠ l i + 1 − l i l_i - l_{i - 1} \not = l_{i + 1} - l_i lili1=li+1li,由 引理4,必有 l i + 1 − l i > l i − l i − 1 l_{i+1} - l_i > l_i - l_{i - 1} li+1li>lili1 l i + 1 − l i > l i − 1 l_{i + 1} - l_{i} > l_{i - 1} li+1li>li1,即 l i + 1 > 2 l i − 1 l_{i + 1} > 2l_{i - 1} li+1>2li1,因而在相邻两段等差数列的分界处,回文后缀的长度至少翻一倍,显然至多只能翻倍 O ( log ⁡ ∣ s ∣ ) \mathcal O(\log |s|) O(logs) 次,原命题得证。

  • len ( x ) \text{len}(x) len(x) 表示结点 x x x 代表回文串的长度,设 dif ( x ) = len ( x ) − len ( fail ( x ) ) \text{dif}(x) = \text{len}(x) - \text{len}(\text{fail}(x)) dif(x)=len(x)len(fail(x)) slink ( x ) \text{slink}(x) slink(x) 表示从 x x x 沿 fail \text{fail} fail 指针往上跳找到的第一个节点 u u u,使得 dif ( x ) ≠ dif ( u ) \text{dif}(x) \not = \text{dif}(u) dif(x)=dif(u),由结论 1,由 slink \text{slink} slink 指针跳至树根只需跳 O ( log ⁡ ∣ s ∣ ) \mathcal O(\log |s|) O(logs) 次,以求回文划分数为例,记录 g x = ∑ dif ( u ) = dif ( x ) f i − len ( u ) g_x = \sum_{\text{dif}(u) = \text{dif}(x)}f_{i - \text{len}(u)} gx=dif(u)=dif(x)filen(u),跳 slink \text{slink} slink 指针的过程中将 g g g 相加即可实现转移,现在的问题时如何维护 g x g_x gx
  • 结论2 x x x fail ( x ) \text{fail}(x) fail(x) 属于同一个等差数列,那么 fail ( x ) \text{fail}(x) fail(x) 上一次出现的位置是 i − diff ( x ) i - \text{diff}(x) idiff(x)

证明引理1 fail ( x ) \text{fail}(x) fail(x) x x x 的 border,因而在 i − dif ( x ) i - \text{dif}(x) idif(x) 处出现。

由于 x x x fail ( x ) \text{fail}(x) fail(x) 属于同一等差数列,则 2 len ( fail ( x ) ) ≥ len(x) 2\text{len}(\text{fail}(x)) \ge \text{len(x)} 2len(fail(x))len(x),假设 fail ( x ) \text{fail}(x) fail(x) ( i − dif ( x ) , i ) (i - \text{dif}(x),i) (idif(x),i) 中的某位置出现,则该位置的 fail ( x ) \text{fail}(x) fail(x) i − dif ( x ) i - \text{dif}(x) idif(x) 处的 fail ( x ) \text{fail}(x) fail(x) 必然有交,设交集为 w w w,且 fail ( x ) = v w \text{fail}(x) = vw fail(x)=vw。由 引理1 w w w fail ( x ) \text{fail}(x) fail(x) 的 border,推出 w w w fail(x) \text{fail(x)} fail(x) 的回文串。则该位置的 fail ( x ) \text{fail}(x) fail(x) i − dif ( x ) i - \text{dif}(x) idif(x) 处的 fail ( x ) \text{fail}(x) fail(x) 的并集 v w v ′ ( v ′ 为 v 倒序得到的字符串 ) vwv'(v'为v倒序得到的字符串) vwv(vv倒序得到的字符串) 也为回文串且是 x x x 的前缀,显然有 ∣ v w v ′ ∣ > len ( fail ( x ) ) |vwv'| > \text{len}(\text{fail}(x)) vwv>len(fail(x)),与 fail ( x ) \text{fail}(x) fail(x) 是最长回文真前(后)缀矛盾。

  • 结论2 可知:

    • x x x fail ( x ) \text{fail}(x) fail(x) 不属于同一个等差数列, slink ( x ) = fail ( x ) \text{slink}(x) = \text{fail}(x) slink(x)=fail(x) g x = f i − len ( x ) = f i − len ( slink ( x ) ) − dif ( x ) g_x = f_{i - \text{len}(x)} = f_{i - \text{len}(\text{slink}(x)) - \text{dif}(x)} gx=filen(x)=filen(slink(x))dif(x)
    • x x x fail ( x ) \text{fail}(x) fail(x) 属于同一个等差数列, g x g_x gx 相比 g fail ( x ) g_{\text{fail}(x)} gfail(x) 只多了 f i − len ( slink ( x ) ) − dif ( x ) f_{i - \text{len}(\text{slink}(x)) - \text{dif}(x)} filen(slink(x))dif(x) 一项,且 g fail ( x ) g_{\text{fail}(x)} gfail(x) i − diff ( x ) i - \text{diff}(x) idiff(x) 处已被计算出,故 g x = g fail(x) + f i − len ( slink ( x ) ) − dif ( x ) g_x = g_{\text{fail(x)}} + f_{i - \text{len}(\text{slink}(x)) - \text{dif}(x)} gx=gfail(x)+filen(slink(x))dif(x)
  • 以求回文划分数为例,核心代码如下:

inline void Extend(char c)
{
    s[++lenp] = c;
    int now = getFail(last);
    if (!ch[now][c - 'a'])
    {
        int x = newNode(len[now] + 2);
        fail[x] = ch[getFail(fail[now])][c - 'a'];
        ch[now][c - 'a'] = x;
        dif[x] = len[x] - len[fail[x]];
        if (dif[x] == dif[fail[x]])
            slink[x] = slink[fail[x]];
        else 
            slink[x] = fail[x];
    }
    last = ch[now][c - 'a'];
}

inline void calc()
{	
	f[0] = 1;
	for (int i = 1; i <= m; ++i)
	{
		pam::Extend(t[i]);
		for (int x = last; x > 1; x = slink[x])
		{
			g[x] = f[i - len[slink[x]] - dif[x]];
			if (fail[x] != slink[x])
				add(g[x], g[fail[x]]);
			add(f[i], g[x]);
		}
	}
}

典例1 CF932G

题目大意

  • 求将字符串 s s s 划分成 k ( k 为偶数 ) k(k 为偶数) k(k为偶数) 个子串 p 1 , p 2 , … , p k p_1,p_2,\dots,p_k p1,p2,,pk 的方案数,使得 p i = p k − i + 1 p_i = p_{k - i + 1} pi=pki+1,答案对 1 0 9 + 7 10^9 + 7 109+7 取模, ∣ s ∣ ≤ 1 0 6 |s| \le 10^6 s106

解法

  • 显然 ∣ s ∣ |s| s 必须为偶数,设 ∣ s ∣ = 2 n |s| = 2n s=2n,做变换 t = s 1 s 2 n s 2 s 2 n − 1 … s n s n + 1 t = s_1s_{2n}s_2s_{2n - 1}\dots s_{n}s_{n + 1} t=s1s2ns2s2n1snsn+1,则原问题被转变为求 t t t 的偶回文划分方案数,通过上述回文树技巧优化转移即可。

典例2 CF906E

题目大意

  • 给定两个等长字符串 s s s t t t,每次可选择 t t t 的一个区间翻转,问最少操作几次能够使 s = t s = t s=t,要求输出方案, ∣ s ∣ , ∣ t ∣ ≤ 1 0 6 |s|,|t| \le 10^6 s,t106

解法

  • ∣ s ∣ = ∣ t ∣ = n |s| = |t| = n s=t=n,做变换 r = s 1 t 1 s 2 t 2 … s n t n r = s_1t_1s_2t_2\dots s_nt_n r=s1t1s2t2sntn,则原问题被转化为对 r r r 进行偶回文划分,长度等于 2 的代价为 0,大于 2 的代价为 1,可以将代价默认设为 1,代价为 0 的单独转移,其余同上。

你可能感兴趣的:(学习笔记,字符串)