Day2 字符串哈希&KMP

字符串哈希 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} Pl1,其中 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=hi1×base+xi,其中 x i x_i xi 表示 S i S_i Si 的数字形式。则 f ( S l … r ) f(S_{l\dots r}) f(Slr) 即为 h r − h l − 1 × b a s e r − l + 1 h_r-h_{l-1}\times base^{r-l+1} hrhl1×baserl+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 1n,m106 0 ≤ k ≤ 5 0\le k\le 5 0k5

哈希+二分。先枚举 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 (nm+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} (1in)。如果要以 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 RiRi1+2。然后我们只需要暴力从 R i − 1 + 2 R_{i-1}+2 Ri1+2 开始递减,直到用哈希判出一个回文即可。

最长公共子串

给定 m m m总长不超过 n n n 的非空字符串,查找所有字符串的最长公共子串。其中 1 ≤ n , m ≤ 1 0 6 1\le n,m\le10^6 1n,m106

如果存在长度为 k k k 的最长公共子串,那么长度为 k − 1 k-1 k1 的子串也一定存在(挖掉一个字符即可)。所以可以二分这个子串的长度,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} T15 中的字符都匹配成功,但 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} nownxt5,表示匹配成功的字符个数从 5 5 5 变成 2 2 2
  • 继续匹配直到 T 1 … 2 T_{1\dots 2} T12 都匹配成功,但 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} nownxt2,表示匹配成功的字符个数从 2 2 2 变成 1 1 1
  • 继续匹配直到 T 1 … 6 T_{1\dots 6} T16 都匹配成功,但 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} nownxt6,表示匹配成功的字符个数从 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 nownow+1,ii+1;否则:

  • 如果 n o w ≠ 0 now\ne 0 now=0,则 n o w ← n x t n o w now\gets nxt_{now} nownxtnow i i i 不变;
  • 如果 n o w = 0 now=0 now=0,则 i ← i + 1 i\gets i+1 ii+1

重复匹配直到 n o w = ∣ T ∣ now=|T| now=T i ≥ ∣ S ∣ i\ge |S| iS。时间复杂度 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 nownow+1,ii+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} nownxtnow
  • 如果 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,ii+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} S1i S S S 中的出现次数。
  • 每个前缀 S 1 … i S_{1\dots i} S1i 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(inow,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(nownxtnow,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(inow,inow+nxtnow]=S(inxtnow,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(inxtnow,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 KiKnxti+1。对于求得最长且合法的 border,先和求 n x t nxt nxt 的过程一样求得最长的匹配成功的 n o w now now,然后再继续跳直到 2 × n o w ≤ i 2\times now\le i 2×nowi,此时 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}) fimin(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 inxtij(印章一下盖得完)。开一个桶记录一下即可,注意 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} ji+1nxtji+1 即为循环节(需要判断能否整除 j − i + 1 j-i+1 ji+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(i1,j)+1,f(i,j1)+1,f(i1,j1)+[Ai=Bj]}

  • 由于 f ( i , j ) ≥ ∣ i − j ∣ f(i,j)\ge |i-j| f(i,j)ij,考虑到操作次数不超过 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×logA)

这是老师上课讲的做法,相信各位都是一知半解反正我不大会就对了,所以这里讲一种相对暴力的做法。

首先 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} SjSj+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 的字典序大于栈顶,则说明删去栈顶后会得到一个一类字符串(字典序大于之前得到的一类字符串)。然后把栈顶部与栈顶字符相同的部分都弹出,反着输出。记得在栈中塞回一个分界符,区分二类字符串。

然后就考虑栈中剩下的二类字符串,处理方法与之前类似,从栈顶往下一路考虑即可。

相关推荐

  1. Day2 字符串&KMP

    2024-04-11 20:06:02       33 阅读
  2. C++ 字符串 || 字符串前缀

    2024-04-11 20:06:02       47 阅读
  3. 表(知识点+leetcode)以及字符串

    2024-04-11 20:06:02       24 阅读
  4. 字符串-最易懂的证明

    2024-04-11 20:06:02       32 阅读
  5. C++ 字符串(hush)讲解

    2024-04-11 20:06:02       23 阅读
  6. 「数据结构」2:实现

    2024-04-11 20:06:02       57 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-11 20:06:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-11 20:06:02       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-11 20:06:02       82 阅读
  4. Python语言-面向对象

    2024-04-11 20:06:02       91 阅读

热门阅读

  1. AI副业赚钱资讯合集

    2024-04-11 20:06:02       26 阅读
  2. Cloudflare是什么?有什么用途?怎么购买

    2024-04-11 20:06:02       32 阅读
  3. 构造函数不能作为虚函数

    2024-04-11 20:06:02       36 阅读
  4. CSS 1PX Border问题解决

    2024-04-11 20:06:02       35 阅读
  5. vue使用后端提供的接口

    2024-04-11 20:06:02       31 阅读
  6. 【笔记】EF文件中定义的SPN显示协议规则

    2024-04-11 20:06:02       30 阅读
  7. 5、ipex-llm(原bigdl-llm)英特尔GPU加速

    2024-04-11 20:06:02       37 阅读
  8. 嵌入式C语言(十三)

    2024-04-11 20:06:02       36 阅读
  9. 【数据结构与算法】力扣 349. 两个数组的交集

    2024-04-11 20:06:02       43 阅读
  10. [xboard]ok210-3 S5PV210光盘资料与功能测试

    2024-04-11 20:06:02       39 阅读
  11. Go-学会 map 的基本使用

    2024-04-11 20:06:02       42 阅读