字符串哈希 KMP
基本
字符串哈希
理论
将一个字符串转成一个数字,可以快速比较两个字符串是否相同等。要求为:相同字符串哈希值相同,不同字符串哈希值尽量不相同。
映射方法通常采用多项式哈希方法,很像进制转换。假设字符串为
S
S
S,其哈希值为
f
(
S
)
f(S)
f(S)。定义一个小的正整数
b
a
s
e
base
base(比如说
27
,
131
27,131
27,131),表示将
S
S
S 视为
b
a
s
e
base
base 进制下的数字。将字符串中涉及到的每个单独的字符
S
i
S_i
Si,转换为一个数字
x
i
x_i
xi(比如说
a
\texttt{a}
a 为
0
0
0,
c
\texttt{c}
c 为
2
2
2)后,用秦久韶算法把
S
S
S 转换后得到的数串
{
x
1
,
x
2
,
…
,
x
n
}
\{x_1,x_2,\dots,x_n\}
{x1,x2,…,xn} 变成
S
S
S 的哈希值:
f
(
S
)
=
(
…
(
(
(
(
x
1
×
b
a
s
e
)
+
x
2
)
×
b
a
s
e
)
+
x
3
)
×
b
a
s
e
)
+
…
)
×
b
a
s
e
)
+
x
n
f(S)=(\dots((((x_1\times base)+x_2)\times base)+x_3)\times base)+\dots)\times base)+x_n
f(S)=(…((((x1×base)+x2)×base)+x3)×base)+…)×base)+xn
举个例子,若
S
=
xyz
S=\texttt{xyz}
S=xyz,则
f
(
S
)
=
23
×
b
a
s
e
2
+
24
×
b
a
s
e
+
25
f(S)=23\times base^2+24\times base+25
f(S)=23×base2+24×base+25。由于
f
(
S
)
f(S)
f(S) 通常会很大,long long
会存不下,所以要么加上 unsigned
自然溢出,要么开一个大质数
P
P
P 把
f
(
S
)
f(S)
f(S) 对
P
P
P 取模。
如果有两个不同的字符串 S , T S,T S,T,在某种哈希方法中 f ( S ) = f ( T ) f(S)=f(T) f(S)=f(T) 则称为发生了哈希冲突。哈希函数的目的就是尽可能降低发生哈希冲突的概率。如果不采用任何优化手段,当 b a s e base base 在 [ 0 , P ) [0,P) [0,P) 中均匀随机选取,则发生哈希冲突的概率大约为 l − 1 P \cfrac{l-1}{P} Pl−1,其中 l = max ( ∣ S ∣ , ∣ T ∣ ) l=\max(|S|,|T|) l=max(∣S∣,∣T∣)。
如果字符串总数大于哈希值的范围( P P P),由鸽巢原理可知至少有两个字符串发生哈希冲突。所以最朴素的方法就是增加 P P P。但根据生日悖论, P P P 不够大时,只需要很少的字符串就很容易发生哈希冲突。以 P = 1 0 9 + 7 P=10^9+7 P=109+7 为例,当有 1 0 5 10^5 105 个不同的字符串时发生哈希冲突的概率高达 99.3263 % 99.3263\% 99.3263%。
解决哈希冲突的常见方法:
- 拉链法:用一条链把相同哈希值的元素挂在一起,查询时算出哈希值后顺着挂着的链遍历一遍查找该字符串。
- 开放地址法:如果先后遇到两个字符串 S , T S,T S,T, f ( S ) = f ( T ) f(S)=f(T) f(S)=f(T),则让 f ( S ) f(S) f(S) 保持不变, f ( T ) f(T) f(T) 去寻找新的位置填入(比如说 f ( T ) ← f ( T ) + 1 f(T)\gets f(T)+1 f(T)←f(T)+1 去探测)。
- 再哈希法:准备多个备用哈希函数,当发生冲突时,使用备用的哈希函数再次计算哈希值,直到不发生冲突后将字符串映射为该哈希值。
- 公共溢出法:准备一个额外的空间作为公共溢出区,当 S S S 发生哈希冲突时,就将 S S S 扔进公共溢出区的末尾。查询时扫描整个公共溢出区即可。
- 多模哈希:用多个不同的 ( b a s e , P ) (base,P) (base,P) 计算哈希值,当 f ( S ) ≠ f ( T ) f(S)\ne f(T) f(S)=f(T) 和 g ( S ) ≠ g ( T ) g(S)\ne g(T) g(S)=g(T) 同时成立时才判断 S ≠ T S\ne T S=T,其中 g g g 是另一个哈希函数。双哈希最为常用。
如何 O ( 1 ) O(1) O(1) 得到 S S S 的某个子串 [ l , r ] [l,r] [l,r] 的哈希值?我们在计算 f ( S ) f(S) f(S) 时多记录一下 S S S 的前缀哈希值 h h h: h i = h i − 1 × b a s e + x i h_i=h_{i-1}\times base+x_i hi=hi−1×base+xi,其中 x i x_i xi 表示 S i S_i Si 的数字形式。则 f ( S l … r ) f(S_{l\dots r}) f(Sl…r) 即为 h r − h l − 1 × b a s e r − l + 1 h_r-h_{l-1}\times base^{r-l+1} hr−hl−1×baser−l+1。可以提前预处理出 b a s e base base 的若干次幂。
在 OI 赛场上,建议使用不同寻常的质数作为 P P P,否则很有可能会被卡掉。给出一个双哈希模板:
// 假设字符串只有小写字母
#define ll long long
const int BASE1 = 27,P1 = 998244353;
const int BASE2 = 131,P2 = 1e9 + 7;
int F(char s[]) {
int n = strlen(s + 1); ll res = 0;
for (int i = 1;i <= n;i ++)
(res = (res * BASE1 % P1) + s[i] - 'a') %= P1;
return res;
}
int G(char s[]) {
int n = strlen(s + 1); ll res = 0;
for (int i = 1;i <= n;i ++)
(res = (res * BASE2 % P1) + s[i] - 'a') %= P2;
return res;
}
bool checkDifferent(char s[],char t[]) { return F(s) != F(t) || G(s) != G(t);
允许失配的匹配
给定长度为 n n n 的文本串 S S S 和长度为 m m m 的模式串 T T T。定义 S S S 的子串 S ′ S' S′ 与 T T T 匹配,当且仅当 ∣ S ′ ∣ = ∣ T ∣ |S'|=|T| ∣S′∣=∣T∣ 且最多有 k k k 个位置字符不同。求能够与 T T T 匹配的 S ′ S' S′ 的个数。
1 ≤ n , m ≤ 1 0 6 1\le n,m\le10^6 1≤n,m≤106, 0 ≤ k ≤ 5 0\le k\le 5 0≤k≤5。
哈希+二分。先枚举 S S S 中所有长度为 m m m 的子串 S ′ S' S′,尝试与 T T T 进行匹配。求出 S ′ S' S′ 和 T T T 的前缀哈希值 s u m S ′ , s u m T sumS',sumT sumS′,sumT( S ′ S' S′ 的前缀哈希用 S S S 的前缀哈希计算得到),然后二分出 S ′ , T S',T S′,T 第一个不同的位置(如果 s u m S m i d ′ = s u m T m i d sumS'_{mid}=sumT_{mid} sumSmid′=sumTmid 相同则往后找,否则往前找);最终将 S ′ S' S′ 和 T T T 中二分得到的失配位置及之前的部分全部删除,继续二分下一个不同的位置。如果二分到第 k + 1 k+1 k+1 轮发现仍有失配位置存在,则说明 S ′ S' S′ 无法匹配 T T T,否则答案加一。
二分至多进行 ( n − m + 1 ) × k (n-m+1)\times k (n−m+1)×k 轮,加上开始时计算的 s u m S , s u m T sumS,sumT sumS,sumT 的 O ( n + m ) O(n+m) O(n+m),总时间复杂度为 O ( m + k n log m ) O(m+kn\log m) O(m+knlogm)。
最长回文子串
给定长度为 n n n 的字符串 S S S,求其中的最长回文子串。
O
(
n
log
n
)
O(n\log n)
O(nlogn) 做法:预处理出
S
S
S 正着的前缀哈希,和倒着的前缀哈希。二分回文子串长度,check
的时候枚举每个回文中心,哈希判断两侧是否相等。
O ( n ) O(n) O(n) 做法:记 R i R_i Ri 表示以 i i i 结尾的最长回文子串的长度,最终答案即为 max { R i } ( 1 ≤ i ≤ n ) \max\{R_i\}\ (1\le i\le n) max{Ri} (1≤i≤n)。如果要以 i + 1 i+1 i+1 作为结尾,那么最优的 R i + 1 R_{i+1} Ri+1 即为:从以 i i i 结尾的最长回文子串的前一个字符与这段回文子串和 S i + 1 S_{i+1} Si+1 拼起来,组成一个长度为 R i + 2 R_i+2 Ri+2 的回文子串,前提是 S i + 1 S_{i+1} Si+1 与这个字符相同。所以有一个性质: R i ≤ R i − 1 + 2 R_{i}\le R_{i-1}+2 Ri≤Ri−1+2。然后我们只需要暴力从 R i − 1 + 2 R_{i-1}+2 Ri−1+2 开始递减,直到用哈希判出一个回文即可。
最长公共子串
给定 m m m 个总长不超过 n n n 的非空字符串,查找所有字符串的最长公共子串。其中 1 ≤ n , m ≤ 1 0 6 1\le n,m\le10^6 1≤n,m≤106。
如果存在长度为
k
k
k 的最长公共子串,那么长度为
k
−
1
k-1
k−1 的子串也一定存在(挖掉一个字符即可)。所以可以二分这个子串的长度,check
部分即为将所有字符串中长度为
k
k
k 的子串全部求一遍哈希值(前缀哈希),然后将求得的值分别放入
n
n
n 个表中,最终如果这些表存在交集则该答案合法。时间复杂度
O
(
n
log
n
m
)
O(n\log \cfrac{n}{m})
O(nlogmn)。
KMP
理论
用于计算给定一个文本串 S S S(长串)和一个模式串 T T T(短串),计算 T T T 在 S S S 中的出现次数/位置。适用于 T T T 不变的情况。令 n n n 为 ∣ S ∣ |S| ∣S∣, m m m 为 ∣ T ∣ |T| ∣T∣,暴力很显然是 O ( n m ) O(nm) O(nm)。
暴力中其实多了很多冗余的比较。KMP 中就对 T T T 多求了个 n x t nxt nxt 数组, n x t i nxt_i nxti 表示 T T T 中到 i i i 为止的子串的真前缀和真后缀最大相同的前缀位置(而字符串中真前后缀相同的部分称为原串的一个 border)。例如模式串 T = aabaabc T=\texttt{aabaabc} T=aabaabc,对其计算 n x t = { 0 , 1 , 0 , 1 , 2 , 3 , 0 } nxt=\{0,1,0,1,2,3,0\} nxt={0,1,0,1,2,3,0}。
假设 S = aabaaabaabaabca S=\texttt{aabaaabaabaabca} S=aabaaabaabaabca,则 KMP 的匹配过程即为:
- T 1 … 5 T_{1\dots5} T1…5 中的字符都匹配成功,但 T 6 T_6 T6 匹配失败( S 6 = a , T 6 = b , S 6 ≠ T 6 S_6=\texttt{a},T_6=\texttt{b},S_6\ne T_6 S6=a,T6=b,S6=T6),根据当前已经匹配成功的字符个数 n o w = 5 now=5 now=5,移动 T 1 T_1 T1 与 S 4 S_4 S4 对齐,即 n o w ← n x t 5 now\gets nxt_{5} now←nxt5,表示匹配成功的字符个数从 5 5 5 变成 2 2 2。
- 继续匹配直到 T 1 … 2 T_{1\dots 2} T1…2 都匹配成功,但 T 3 T_3 T3 失配( S 6 = a , T 3 = b S_6=\texttt{a},T_3=\texttt{b} S6=a,T3=b),根据当前已经匹配成功的字符个数 n o w = 2 now=2 now=2,移动 T 1 T_1 T1 与 S 5 S_5 S5 对齐,即 n o w ← n x t 2 now\gets nxt_{2} now←nxt2,表示匹配成功的字符个数从 2 2 2 变成 1 1 1。
- 继续匹配直到 T 1 … 6 T_{1\dots 6} T1…6 都匹配成功,但 T 7 T_7 T7 失配( S 1 1 = a , T 7 = c S_11=\texttt{a},T_7=\texttt{c} S11=a,T7=c),根据当前已经匹配成功的字符个数 n o w = 6 now=6 now=6,移动 T 1 T_1 T1 与 S 8 S_8 S8 对齐,即 n o w ← n x t 6 now\gets nxt_{6} now←nxt6,表示匹配成功的字符个数从 6 6 6 变成 3 3 3。
- 继续匹配就发现匹配成功了。
综上,匹配操作即为:令 n o w now now 表示已成功匹配的字符个数, i i i 表示 S S S 匹配到的位置,初始 n o w = 0 , i = 1 now=0,i=1 now=0,i=1;检查 S S S 中的当前字符和 T n o w + 1 T_{now+1} Tnow+1 是否相同,若相同,则 n o w ← n o w + 1 , i ← i + 1 now\gets now+1,i\gets i+1 now←now+1,i←i+1;否则:
- 如果 n o w ≠ 0 now\ne 0 now=0,则 n o w ← n x t n o w now\gets nxt_{now} now←nxtnow 且 i i i 不变;
- 如果 n o w = 0 now=0 now=0,则 i ← i + 1 i\gets i+1 i←i+1。
重复匹配直到 n o w = ∣ T ∣ now=|T| now=∣T∣ 或 i ≥ ∣ S ∣ i\ge |S| i≥∣S∣。时间复杂度 O ( n ) O(n) O(n),证明略。
怎么算 n x t nxt nxt 数组呢?这相当于 T T T 和 T T T 自己进行匹配。逐位向后扩展,当失配时利用已经得到的 n x t nxt nxt 跳。仍设两个变量 n o w , i now,i now,i,初始 n o w = 0 now=0 now=0,注意初始时 i = 2 i=2 i=2;显然 n x t 1 = 0 nxt_1=0 nxt1=0;当 T i = T n x t + 1 T_i=T_{nxt+1} Ti=Tnxt+1 时,计算 n x t i = n o w + 1 nxt_i=now+1 nxti=now+1 并且 n o w ← n o w + 1 , i ← i + 1 now\gets now+1,i\gets i+1 now←now+1,i←i+1;否则:
- 如果 n o w ≠ 0 now\ne 0 now=0,此时 n x t n o w nxt_{now} nxtnow 已经求出,所以 n o w ← n x t n o w now\gets nxt_{now} now←nxtnow。
- 如果 n o w = 0 now=0 now=0,则 n x t i = 0 , i ← i + 1 nxt_i=0,i\gets i+1 nxti=0,i←i+1。
时间复杂度 O ( m ) O(m) O(m)。给出一个模板:
// 求 nxt
nxt[1] = 0;
for (int i = 2,now = 0;i <= m;nxt[i] = now,i ++) {
while (now && T[i] != T[now + 1]) now = nxt[now];
if (T[i] == T[now + 1]) now ++;
}
// 匹配,这里仅输出第一个匹配成功的位置(开头)
for (int i = 1,now = 0;i <= n;i ++) {
while (now && S[i] != T[now + 1]) now = nxt[now];
if (S[i] == T[now + 1]) now ++;
if (now == m) {
printf("%d",i - m + 1);
break;
}
}
前缀出现次数
给定长度为 n n n 的字符串 S S S 和长度为 m m m 的字符串 T T T,统计:
- 每个前缀 S 1 … i S_{1\dots i} S1…i 在 S S S 中的出现次数。
- 每个前缀 S 1 … i S_{1\dots i} S1…i 在 T T T 中的出现次数。
在 oi-wiki 的这里,讲得非常细所以偷个懒。
题目
上午
因为老师没给我开所以写不了一点qwq
下午
优秀拆分 95pts
令 f i f_i fi 表示以 i i i 结尾的形如 AA \texttt{AA} AA 的子串个数, g i g_i gi 表示以 i i i 开头的形如 BB \texttt{BB} BB 的子串个数。计算方法即为:枚举每个长度为偶数的子串 [ L , R ] [L,R] [L,R],比较 [ L , m i d ] [L,mid] [L,mid] 和 [ m i d + 1 , R ] [mid+1,R] [mid+1,R] 的哈希值是否相同。然后枚举 AA,BB \texttt{AA,BB} AA,BB 之间的分界线 i i i,贡献即为 f i × g i + 1 f_i\times g_{i+1} fi×gi+1。复杂度 O ( n 2 ) O(n^2) O(n2),瓶颈在于求 f , g f,g f,g。
动物园
n
u
m
i
num_i
numi 其实就是求:从以
i
i
i 结尾长度不超过
⌊
i
2
⌋
\lfloor\frac{i}{2}\rfloor
⌊2i⌋ 的最长 border 开始,不断跳
n
x
t
nxt
nxt 直到
n
x
t
i
=
0
nxt_i=0
nxti=0 所用的次数。令
n
o
w
=
n
x
t
i
now=nxt_i
now=nxti(
n
o
w
now
now 合法),此时
S
[
1
,
n
o
w
]
=
S
(
i
−
n
o
w
,
i
]
(
1
)
S[1,now]=S(i-now,i]\ \ \ (1)
S[1,now]=S(i−now,i] (1)。如果
n
x
t
n
o
w
nxt_{now}
nxtnow 有值,那么说明
S
[
1
,
n
x
t
n
o
w
]
=
S
(
n
o
w
−
n
x
t
n
o
w
,
n
o
w
]
(
2
)
S[1,nxt_{now}]=S(now-nxt_{now},now]\ \ \ (2)
S[1,nxtnow]=S(now−nxtnow,now] (2)
根据
(
1
)
(1)
(1) 和
(
2
)
(2)
(2) 可知
S
(
i
−
n
o
w
,
i
−
n
o
w
+
n
x
t
n
o
w
]
=
S
(
i
−
n
x
t
n
o
w
,
i
]
(
3
)
S(i-now,i-now+nxt_{now}]=S(i-nxt_{now},i]\ \ \ (3)
S(i−now,i−now+nxtnow]=S(i−nxtnow,i] (3)
综合三者得
S
[
1
,
n
x
t
n
o
w
]
=
S
(
i
−
n
x
t
n
o
w
,
i
]
(
E
n
d
)
S[1,nxt_{now}]=S(i-nxt_{now},i]\ \ \ (End)
S[1,nxtnow]=S(i−nxtnow,i] (End)
即
n
x
t
n
o
w
nxt_{now}
nxtnow 也是
S
[
1
,
i
]
S[1,i]
S[1,i] 中的 border 之一。因为
n
x
t
nxt
nxt 存的总是当前最长的 border,所以
n
x
t
n
o
w
nxt_{now}
nxtnow 是
n
o
w
now
now 后紧接着的 border,不会漏数。所以我们在求
n
x
t
i
nxt_i
nxti 的时候顺便记录跳的次数
K
i
←
K
n
x
t
i
+
1
K_{i}\gets K_{nxt_i}+1
Ki←Knxti+1。对于求得最长且合法的 border,先和求
n
x
t
nxt
nxt 的过程一样求得最长的匹配成功的
n
o
w
now
now,然后再继续跳直到
2
×
n
o
w
≤
i
2\times now\le i
2×now≤i,此时
n
o
w
now
now 即为所求。
Censoring S
提前算出 T T T 的前缀哈希,开一个栈逐个往里扔 S i S_i Si 并计算栈中元素的前缀哈希。如果发现栈中元素有 ∣ T ∣ |T| ∣T∣ 个了就判断栈中栈顶 ∣ T ∣ |T| ∣T∣ 个元素的哈希值是否和 T T T 相同,如果相同则弹出这 ∣ T ∣ |T| ∣T∣ 个元素。最后从栈底输出到栈顶即可。
SZA-Template
显然合法的印章一定是字符串中的一个 border(至少前后缀相同,这个印章才可能覆盖整个字符串),考虑 dp。设 f i f_{i} fi 表示印 [ 1 , i ] [1,i] [1,i] 这一段所需最短的印章长度。显然 f 1 = 1 f_1=1 f1=1 即用自己作为整个印章。转移时从自己的 border 处转移,尝试使用 border 的印章盖出自己;但因为 KMP 中求出的 n x t nxt nxt 已经涵盖了较小的 border,所以直接从 n x t i nxt_i nxti 处转移即可。转移方程即为 f i ← min ( i , f n x t i ) f_i\gets \min(i,f_{nxt_i}) fi←min(i,fnxti)。
但不是随时都可以转移的。如果想从 n x t i nxt_i nxti 处转移,最多最多就是用 n x t i nxt_i nxti 作为印章,然后在之前的一段后面加上一段 n x t i nxt_i nxti 印出 i i i 来。即存在一个 j j j,使得两者使用的印章长度相同(有印章能用)且 i − n x t i ≤ j i-nxt_i\le j i−nxti≤j(印章一下盖得完)。开一个桶记录一下即可,注意 1 1 1 一开始就丢进桶里。
String Compression
考虑 dp。设 f i f_i fi 表示前 i i i 个字符进行压缩的最小长度,初始化 f i = i + 1 f_i=i+1 fi=i+1 即不进行任何内部的压缩。令 c n t i , j cnt_{i,j} cnti,j 表示完全压缩 [ i , j ] [i,j] [i,j] 这一段后的长度,显然这一段需要去找 [ i , j ] [i,j] [i,j] 的最短循环节。所以我们按照每个 i i i 为开头都跑一遍 KMP 求 n x t nxt nxt 的过程(此时令 i i i 为模式串的开头),此时求得的 j − i + 1 − n x t j − i + 1 j-i+1-nxt_{j-i+1} j−i+1−nxtj−i+1 即为循环节(需要判断能否整除 j − i + 1 j-i+1 j−i+1,不能则说明不存在循环节,只能不进行内部压缩)。转移方程即为 f i = min { f j + c n t j + 1 , i } ( j < i ) f_i=\min\{f_j+cnt_{j+1,i}\}\ (j < i) fi=min{fj+cntj+1,i} (j<i)。
// 核心代码,S 下标从 1 开始。
for (int i = 1;i <= n;i ++)
f[i] = i + 1;
for (int i = 0;i <= n;i ++) { // i = 0 时相当于尝试直接整段压缩。
KMP(S + i); // 跑出以 i + 1 为开头的 nxt 数组
for (int len = 1;i + len <= n;len ++) {
int j = i + len;
if (len % (len - nxt[len]) == 0)
f[j] = min(f[j],f[i] + count(len / (len - nxt[len])) + len - nxt[len]);
else f[j] = min(f[j],f[i] + len + 1);
// count 表示数出十进制位数。
}
}
说无可说
- 问题在于如何快速计算两个串的相似度。朴素算法有 dfs,动态规划等
- 用 f ( i , j ) f(i,j) f(i,j) 表示 A A A 的前 i i i 个字符与 B B B 的前 j j j 个字符需要多少次才能相同,则
f ( i , j ) = min { f ( i − 1 , j ) + 1 , f ( i , j − 1 ) + 1 , f ( i − 1 , j − 1 ) + [ A i ≠ B j ] } f(i,j)=\min\{f(i-1,j)+1,f(i,j-1)+1,f(i-1,j-1)+[A_i\ne B_j]\} f(i,j)=min{f(i−1,j)+1,f(i,j−1)+1,f(i−1,j−1)+[Ai=Bj]}
- 由于 f ( i , j ) ≥ ∣ i − j ∣ f(i,j)\ge |i-j| f(i,j)≥∣i−j∣,考虑到操作次数不超过 8 8 8,则第二维可以改成 Δ i \Delta i Δi,于是复杂度降为 O ( 16 × ∣ A ∣ ) O(16\times |A|) O(16×∣A∣)
- 用 h ( k , Δ i ) h(k,\Delta i) h(k,Δi) 表示使 f ( i , Δ i ) = k f(i,\Delta i)=k f(i,Δi)=k 的最大的 i i i。因为更大的 i i i 可以更早匹配完
- 中间用 lcp 来转移,二分 hash 也能计算 lcp,复杂度降为 O ( 64 × log ∣ A ∣ ) O(64\times \log |A|) O(64×log∣A∣)。
这是老师上课讲的做法,相信各位都是一知半解反正我不大会就对了,所以这里讲一种相对暴力的做法。
首先 O ( n 2 ) O(n^2) O(n2) 枚举两个 A , B A,B A,B,考虑暴搜算出 A , B A,B A,B 之间的相似度,因为操作次数不会超过 8 8 8 所以边搜边剪枝的复杂度不会太高。全局开一个 a n s ans ans 记录当前算到的最小操作次数,最后开一个桶记录对数即可。
课后
大段排骨
很像提高 1 的 第二饭堂
。从
S
S
S 的前后往中间扫,边扫边计算此时的前后缀哈希值,如果发现相等则说明在这之前的一段可以分出来。不断往中间靠拢即可。最后如果中间还剩下一段或
∣
S
∣
|S|
∣S∣ 为奇数则答案还要再加一。
倍增计算
首先破环成链。先用前缀和算环上任意一段中 A,B,C \texttt{A,B,C} A,B,C 字母的数量(在链上处理),然后就可以钦定环的起点按照 4 4 4 的幂次遍历整个环算答案了。
// 核心代码
// sum[i][0/1/2] 表示在链上 i 之前有几个 A/B/C。
for (int i = 1, ans = 0, pos = i;i <= m;i ++, ans = 0, pos = i) {
for (int j = n - 1;j >= 0;j --) {
for (int k = 0;k < 3;k ++)
ans += (1 << (j << 1)) - sum[pos + (1 << (j << 1)) * (k + 1)][k] + sum[pos + (1 << (j << 1)) * k][k];
// 修改次数为:环上所有字符 - 与目标字符一样的字符数。
pos += (1 << (j << 1)) * 3; // 往后处理
}
res = min(res,ans);
}
字符游戏
这回想明白辣!如果字符串字典序单调不减,那么显然删除的字符越靠后原串字典序越小(或相等);而如果原串有一个部分字典序减小了,假设 S i > S i + 1 S_i>S_{i+1} Si>Si+1,则 i i i 越靠前,删掉 S i S_i Si 所得的字典序越小,且一定不大于其他诸如 S j ≤ S j + 1 S_j\le S_{j+1} Sj≤Sj+1 时删掉 S j S_j Sj 后的字典序。
我们以
S
=
aabbaaaba
S=\texttt{aabbaaaba}
S=aabbaaaba 为例。一眼盯真可得删除
S
4
S_4
S4 时字典序最小,因为
S
4
>
S
5
S_4>S_5
S4>S5;进一步可以发现一段连续且相同的字符中删掉任意一个,所得字典序都是不变的,所以删除
S
3
S_3
S3 后字典序也是最小的。根据一开始的结论,删掉
S
8
S_8
S8 后所得字典序就是次小的。为了方便,我们将这么得到的字符串称为一类字符串。剩下的情况中,
S
[
1
,
2
]
,
S
[
5
,
8
]
S[1,2],S[5,8]
S[1,2],S[5,8] 中分别任意选择一个字符删去,分别得到的字典序不变。仍根据一开始的结论,得到的字典序从小至大为:删去
S
9
S_9
S9、删去
S
[
5
,
8
]
S[5,8]
S[5,8]、删去
S
[
1
,
2
]
S[1,2]
S[1,2]。我们将这么得到的字符串称为二类字符串。一类字符串的字典序总是小于二类字符串。
于是做法就出来了:开一个栈记录此时的字符,从前往后枚举 i i i,如果 S i S_i Si 的字典序大于栈顶,则说明删去栈顶后会得到一个一类字符串(字典序大于之前得到的一类字符串)。然后把栈顶部与栈顶字符相同的部分都弹出,反着输出。记得在栈中塞回一个分界符,区分二类字符串。
然后就考虑栈中剩下的二类字符串,处理方法与之前类似,从栈顶往下一路考虑即可。