字符串,就是由字符连接而成的序列。
常见的字符串问题包括字符串匹配问题、子串相关问题、前缀/后缀相关问题、回文串相关问题、子序列相关问题等。
一个 字符集 Σ \Sigma Σ 是一个建立了全序关系的集合,也就是说, Σ \Sigma Σ 中的任意两个不同的元素 α \alpha α 和 β \beta β 都可以比较大小,要么 α < β \alpha<\beta α<β,要么 β < α \beta<\alpha β<α。字符集 Σ \Sigma Σ 中的元素称为字符。
一个 字符串 S S S 是将 n n n 个字符顺次排列形成的序列, n n n 称为 S S S 的长度,表示为 ∣ S ∣ |S| ∣S∣。
如果字符串下标从 1 1 1 开始计算, S S S 的第 i i i 个字符表示为 S [ i ] S[i] S[i];
如果字符串下标从 0 0 0 开始计算, S S S 的第 i i i 个字符表示为 S [ i − 1 ] S[i-1] S[i−1]。
字符串 S S S 的 子串 S [ i . . j ] , i ≤ j S[i..j],i≤j S[i..j],i≤j,表示 S S S 串中从 i i i 到 j j j 这一段,也就是顺次排列 S [ i ] , S [ i + 1 ] , … , S [ j ] S[i],S[i+1],\ldots,S[j] S[i],S[i+1],…,S[j] 形成的字符串。
有时也会用 S [ i . . j ] S[i..j] S[i..j], i > j i>j i>j 来表示空串。
字符串 S S S 的 子序列 是从 S S S 中将若干元素提取出来并不改变相对位置形成的序列,即 S [ p 1 ] , S [ p 2 ] , … , S [ p k ] S[p_1],S[p_2],\ldots,S[p_k] S[p1],S[p2],…,S[pk], 1 ≤ p 1 < p 2 < ⋯ < p k ≤ ∣ S ∣ 1\le p_1< p_2<\cdots< p_k\le|S| 1≤p1<p2<⋯<pk≤∣S∣。
后缀 是指从某个位置 i i i 开始到整个串末尾结束的一个特殊子串。字符串 S S S 的从 i i i 开头的后缀表示为 Suffix(S,i) \textit{Suffix(S,i)} Suffix(S,i),也就是 Suffix(S,i) = S [ i . . ∣ S ∣ − 1 ] \textit{Suffix(S,i)}=S[i..|S|-1] Suffix(S,i)=S[i..∣S∣−1]。
真后缀 指除了 S S S 本身的 S S S 的后缀。
举例来说,字符串 abcabcd
的所有后缀为 {d, cd, bcd, abcd, cabcd, bcabcd, abcabcd}
,而它的真后缀为 {d, cd, bcd, abcd, cabcd, bcabcd}
。
前缀 是指从串首开始到某个位置 i i i 结束的一个特殊子串。字符串 S S S 的以 i i i 结尾的前缀表示为 Prefix(S,i) \textit{Prefix(S,i)} Prefix(S,i),也就是 Prefix(S,i) = S [ 0.. i ] \textit{Prefix(S,i)}=S[0..i] Prefix(S,i)=S[0..i]。
真前缀 指除了 S S S 本身的 S S S 的前缀。
举例来说,字符串 abcabcd
的所有前缀为 {a, ab, abc, abca, abcab, abcabc, abcabcd}
, 而它的真前缀为 {a, ab, abc, abca, abcab, abcabc}
。
以第 i i i 个字符作为第 i i i 关键字进行大小比较,空字符小于字符集内任何字符(即: a < a a a< aa a<aa)。
回文串 是正着写和倒着写相同的字符串,即满足 ∀ 1 ≤ i ≤ ∣ s ∣ , s [ i ] = s [ ∣ s ∣ + 1 − i ] \forall 1\le i\le|s|, s[i]=s[|s|+1-i] ∀1≤i≤∣s∣,s[i]=s[∣s∣+1−i] 的 s s s。
char
数组存储,用空字符 \0
表示字符串的结尾(C 风格字符串)。C 标准库操作字符数组。
char[]
/const char*
参见:fprintf、fscanf、空终止字节字符串
printf("%s", s)
:用 %s
来输出一个字符串(字符数组)。scanf("%s", &s)
:用 %s
来读入一个字符串(字符数组)。sscanf(const char *__source, const char *__format, ...)
:从字符串 __source
里读取变量,比如 sscanf(str,"%d",&a)
。sprintf(char *__stream, const char *__format, ...)
:将 __format
字符串里的内容输出到 __stream
中,比如 sprintf(str,"%d",i)
。strlen(const char *str)
:返回从 str[0]
开始直到 '\0'
的字符数。注意,未开启 O2 优化时,该操作写在循环条件中复杂度是 Θ ( N ) \Theta(N) Θ(N) 的。strcmp(const char *str1, const char *str2)
:按照字典序比较 str1 str2
若 str1
字典序小返回负值,两者一样返回 0
,str1
字典序更大则返回正值。请注意,不要简单的认为返回值只有 0
、1
、-1
三种,在不同平台下的返回值都遵循正负,但并非都是 0
、1
、-1
。strcpy(char *str, const char *src)
: 把 src
中的字符复制到 str
中,str
src
均为字符数组头指针,返回值为 str
包含空终止符号 '\0'
。strncpy(char *str, const char *src, int cnt)
:复制至多 cnt
个字符到 str
中,若 src
终止而数量未达 cnt
则写入空字符到 str
直至写入总共 cnt
个字符。strcat(char *str1, const char *str2)
: 将 str2
接到 str1
的结尾,用 *str2
替换 str1
末尾的 '\0'
返回 str1
。strstr(char *str1, const char *str2)
:若 str2
是 str1
的子串,则返回 str2
在 str1
的首次出现的地址;如果 str2
不是 str1
的子串,则返回 NULL
。strchr(const char *str, int c)
:找到在字符串 str
中第一次出现字符 c
的位置,并返回这个位置的地址。如果未找到该字符则返回 NULL
。strrchr(const char *str, char c)
:找到在字符串 str
中最后一次出现字符 c
的位置,并返回这个位置的地址。如果未找到该字符则返回 NULL
。C++ 标准库操作字符串对象,同时也提供对字符数组的兼容。
std::string
参见:std::basic_string
+
,当 +
两边是 string/char/char[]/const char*
类型时,可以将这两个变量连接,返回连接后的字符串(string
)。=
右侧可以是 const string/string/const char*/char*
。[cur]
返回 cur
位置的引用。data()/c_str()
返回一个 const char*
指针,内容与该 string
相同。size()
返回字符串字符个数。find(ch, start = 0)
查找并返回从 start
开始的字符 ch
的位置;rfind(ch)
从末尾开始,查找并返回第一个找到的字符 ch
的位置(皆从 0
开始)(如果查找不到,返回 -1
)。substr(start, len)
可以从字符串的 start
(从 0
开始)截取一个长度为 len
的字符串(缺省 len
时代码截取到字符串末尾)。append(s)
将 s
添加到字符串末尾。append(s, pos, n)
将字符串 s
中,从 pos
开始的 n
个字符连接到当前字符串结尾。replace(pos, n, s)
删除从 pos
开始的 n
个字符,然后在 pos
处插入串 s
。erase(pos, n)
删除从 pos
开始的 n
个字符。insert(pos, s)
在 pos
位置插入字符串 s
。std::string
重载了比较逻辑运算符,复杂度是 Θ ( N ) \Theta(N) Θ(N) 的。author: Frankaiyou, henrytbtrue, zymooll
本节将简述字符串匹配问题以及它的解法。
又称模式匹配(pattern matching)。该问题可以概括为「给定字符串 S S S 和 T T T,在主串 S S S 中寻找子串 T T T」。字符 T T T 称为模式串 (pattern)。
简称 BF (Brute Force) 算法。该算法的基本思想是从主串 S S S 的第一个字符开始和模式串 T T T 的第一个字符进行比较,若相等,则继续比较二者的后续字符;否则,模式串 T T T 回退到第一个字符,重新和主串 S S S 的第二个字符进行比较。如此往复,直到 S S S 或 T T T 中所有字符比较完毕。
=== “C++”
/*
* s:待匹配的主串
* t:模式串
* n:主串的长度
* m:模式串的长度
*/
std::vector<int> match(char *s, char *t, int n, int m) {
std::vector<int> ans;
int i, j;
for (i = 0; i < n - m + 1; i++) {
for (j = 0; j < m; j++) {
if (s[i + j] != t[j]) break;
}
if (j == m) ans.push_back(i);
}
return ans;
}
=== “Python”
def match(s, t, n, m):
if m < 1:
return []
ans = []
for i in range(0, n - m + 1):
for j in range(0, m):
if s[i + j] != t[j]:
break
else:
ans.append(i)
return ans
设 n n n 为主串的长度, m m m 为模式串的长度。默认 m ≪ n m\ll n m≪n。
在最好情况下,BF 算法匹配成功时,时间复杂度为 O ( n ) O(n) O(n);匹配失败时,时间复杂度为 O ( m ) O(m) O(m)。
在最坏情况下,每趟不成功的匹配都发生在模式串的最后一个字符,BF 算法要执行 m ( n − m + 1 ) m(n-m+1) m(n−m+1) 次比较,时间复杂度为 O ( n m ) O(nm) O(nm)。
如果模式串有至少两个不同的字符,则 BF 算法的平均时间复杂度为 O ( n ) O(n) O(n)。但是在 OI 题目中,给出的字符串一般都不是纯随机的。
参见:字符串哈希
参见:前缀函数与 KMP 算法
我们定义一个把字符串映射到整数的函数 f f f,这个 f f f 称为是 Hash 函数。
我们希望这个函数 f f f 可以方便地帮我们判断两个字符串是否相等。
Hash 的核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。
这里的「值域较小」在不同情况下意义不同。
在 哈希表 中,值域需要小到能够接受线性的空间与时间复杂度。
在字符串哈希中,值域需要小到能够快速比较( 1 0 9 10^9 109、 1 0 18 10^{18} 1018 都是可以快速比较的)。
同时,为了降低哈希冲突率,值域也不能太小。
具体来说,哈希函数最重要的性质可以概括为下面两条:
在 Hash 函数值不一样的时候,两个字符串一定不一样;
在 Hash 函数值一样的时候,两个字符串不一定一样(但有大概率一样,且我们当然希望它们总是一样的)。
我们将 Hash 函数值一样但原字符串不一样的现象称为哈希碰撞。
我们需要关注的是什么?
时间复杂度和 Hash 的准确率。
通常我们采用的是多项式 Hash 的方法,对于一个长度为 l l l 的字符串 s s s 来说,我们可以这样定义多项式 Hash 函数: f ( s ) = ∑ i = 1 l s [ i ] × b l − i ( m o d M ) f(s) = \sum_{i=1}^{l} s[i] \times b^{l-i} \pmod M f(s)=∑i=1ls[i]×bl−i(modM)。例如,对于字符串 x y z xyz xyz,其哈希函数值为 x b 2 + y b + z xb^2+yb+z xb2+yb+z。
特别要说明的是,也有很多人使用的是另一种 Hash 函数的定义,即 f ( s ) = ∑ i = 1 l s [ i ] × b i − 1 ( m o d M ) f(s) = \sum_{i=1}^{l} s[i] \times b^{i-1} \pmod M f(s)=∑i=1ls[i]×bi−1(modM),这种定义下,同样的字符串 x y z xyz xyz 的哈希值就变为了 x + y b + z b 2 x+yb+zb^2 x+yb+zb2 了。
显然,上面这两种哈希函数的定义函数都是可行的,但二者在之后会讲到的计算子串哈希值时所用的计算式是不同的,因此千万注意 不要弄混了这两种不同的 Hash 方式。
由于前者的 Hash 定义计算更简便、使用人数更多、且可以类比为一个 b b b 进制数来帮助理解,所以本文下面所将要讨论的都是使用 f ( s ) = ∑ i = 1 l s [ i ] × b l − i ( m o d M ) f(s) = \sum_{i=1}^{l} s[i] \times b^{l-i} \pmod M f(s)=∑i=1ls[i]×bl−i(modM) 来定义的 Hash 函数。
下面讲一下如何选择 M M M 和计算哈希碰撞的概率。
这里 M M M 需要选择一个素数(至少要比最大的字符要大), b b b 可以任意选择。
如果我们用未知数 x x x 替代 b b b,那么 f ( s ) f(s) f(s) 实际上是多项式环 Z M [ x ] \mathbb{Z}_M[x] ZM[x] 上的一个多项式。考虑两个不同的字符串 s , t s,t s,t,有 f ( s ) = f ( t ) f(s)=f(t) f(s)=f(t)。我们记 h ( x ) = f ( s ) − f ( t ) = ∑ i = 1 l ( s [ i ] − t [ i ] ) x l − i ( m o d M ) h(x)=f(s)-f(t)=\sum_{i=1}^l(s[i]-t[i])x^{l-i}\pmod M h(x)=f(s)−f(t)=∑i=1l(s[i]−t[i])xl−i(modM),其中 l = max ( ∣ s ∣ , ∣ t ∣ ) l=\max(|s|,|t|) l=max(∣s∣,∣t∣)。可以发现 h ( x ) h(x) h(x) 是一个 l − 1 l-1 l−1 阶的非零多项式。
如果 s s s 与 t t t 在 x = b x=b x=b 的情况下哈希碰撞,则 b b b 是 h ( x ) h(x) h(x) 的一个根。由于 h ( x ) h(x) h(x) 在 Z M \mathbb{Z}_M ZM 是一个域(等价于 M M M 是一个素数,这也是为什么 M M M 要选择素数的原因)的时候,最多有 l − 1 l-1 l−1 个根,如果我们保证 b b b 是从 [ 0 , M ) [0,M) [0,M) 之间均匀随机选取的,那么 f ( s ) f(s) f(s) 与 f ( t ) f(t) f(t) 碰撞的概率可以估计为 l − 1 M \frac{l-1}{M} Ml−1。简单验算一下,可以发现如果两个字符串长度都是 1 1 1 的时候,哈希碰撞的概率为 1 − 1 M = 0 \frac{1-1}{M}=0 M1−1=0,此时不可能发生碰撞。
参考代码:(效率低下的版本,实际使用时一般不会这么写)
=== “C++”
using std::string;
const int M = 1e9 + 7;
const int B = 233;
typedef long long ll;
int get_hash(const string& s) {
int res = 0;
for (int i = 0; i < s.size(); ++i) {
res = ((ll)res * B + s[i]) % M;
}
return res;
}
bool cmp(const string& s, const string& t) {
return get_hash(s) == get_hash(t);
}
=== “Python”
M = int(1e9 + 7)
B = 233
def get_hash(s):
res = 0
for char in s:
res = (res * B + ord(char)) % M
return res
def cmp(s, t):
return get_hash(s) == get_hash(t)
假定哈希函数将字符串随机地映射到大小为 M M M 的值域中,总共有 n n n 个不同的字符串,那么未出现碰撞的概率是 ∏ i = 0 n − 1 M − i M \prod_{i = 0}^{n-1} \frac{M-i}{M} ∏i=0n−1MM−i(第 i i i 次进行哈希时,有 M − i M \frac{M-i}{M} MM−i 的概率不会发生碰撞)。在随机数据下,若 M = 1 0 9 + 7 M=10^9 + 7 M=109+7, n = 1 0 6 n=10^6 n=106,未出现碰撞的概率是极低的。
所以,进行字符串哈希时,经常会对两个大质数分别取模,这样的话哈希函数的值域就能扩大到两者之积,错误率就非常小了。
单次计算一个字符串的哈希值复杂度是 O ( n ) O(n) O(n),其中 n n n 为串长,与暴力匹配没有区别,如果需要多次询问一个字符串的子串的哈希值,每次重新计算效率非常低下。
一般采取的方法是对整个字符串先预处理出每个前缀的哈希值,将哈希值看成一个 b b b 进制的数对 M M M 取模的结果,这样的话每次就能快速求出子串的哈希了:
令 f i ( s ) f_i(s) fi(s) 表示 f ( s [ 1.. i ] ) f(s[1..i]) f(s[1..i]),即原串长度为 i i i 的前缀的哈希值,那么按照定义有 f i ( s ) = s [ 1 ] ⋅ b i − 1 + s [ 2 ] ⋅ b i − 2 + ⋯ + s [ i − 1 ] ⋅ b + s [ i ] f_i(s)=s[1]\cdot b^{i-1}+s[2]\cdot b^{i-2}+\dots+s[i-1]\cdot b+s[i] fi(s)=s[1]⋅bi−1+s[2]⋅bi−2+⋯+s[i−1]⋅b+s[i]
现在,我们想要用类似前缀和的方式快速求出 f ( s [ l . . r ] ) f(s[l..r]) f(s[l..r]),按照定义有字符串 s [ l . . r ] s[l..r] s[l..r] 的哈希值为 f ( s [ l . . r ] ) = s [ l ] ⋅ b r − l + s [ l + 1 ] ⋅ b r − l − 1 + ⋯ + s [ r − 1 ] ⋅ b + s [ r ] f(s[l..r])=s[l]\cdot b^{r-l}+s[l+1]\cdot b^{r-l-1}+\dots+s[r-1]\cdot b+s[r] f(s[l..r])=s[l]⋅br−l+s[l+1]⋅br−l−1+⋯+s[r−1]⋅b+s[r]
对比观察上述两个式子,我们发现 f ( s [ l . . r ] ) = f r ( s ) − f l − 1 ( s ) × b r − l + 1 f(s[l..r])=f_r(s)-f_{l-1}(s) \times b^{r-l+1} f(s[l..r])=fr(s)−fl−1(s)×br−l+1 成立(可以手动代入验证一下),因此我们用这个式子就可以快速得到子串的哈希值。其中 b r − l + 1 b^{r-l+1} br−l+1 可以 O ( n ) O(n) O(n) 的预处理出来然后 O ( 1 ) O(1) O(1) 的回答每次询问(当然也可以快速幂 O ( log n ) O(\log n) O(logn) 的回答每次询问)。
求出模式串的哈希值后,求出文本串每个长度为模式串长度的子串的哈希值,分别与模式串的哈希值比较即可。
问题:给定长为 n n n 的源串 s s s,以及长度为 m m m 的模式串 p p p,要求查找源串中有多少子串与模式串匹配。 s ′ s' s′ 与 s s s 匹配,当且仅当 s ′ s' s′ 与 s s s 长度相同,且最多有 k k k 个位置字符不同。其中 1 ≤ n , m ≤ 1 0 6 1\leq n,m\leq 10^6 1≤n,m≤106, 0 ≤ k ≤ 5 0\leq k\leq 5 0≤k≤5。
这道题无法使用 KMP 解决,但是可以通过哈希 + 二分来解决。
枚举所有可能匹配的子串,假设现在枚举的子串为 s ′ s' s′,通过哈希 + 二分可以快速找到 s ′ s' s′ 与 p p p 第一个不同的位置。之后将 s ′ s' s′ 与 p p p 在这个失配位置及之前的部分删除掉,继续查找下一个失配位置。这样的过程最多发生 k k k 次。
总的时间复杂度为 O ( m + k n log 2 m ) O(m+kn\log_2m) O(m+knlog2m)。
二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
这个问题可以使用 manacher 算法 在 O ( n ) O(n) O(n) 的时间内解决。
通过哈希同样可以 O ( n ) O(n) O(n) 解决这个问题,具体方法就是记 R i R_i Ri 表示以 i i i 作为结尾的最长回文的长度,那么答案就是 max i = 1 n R i \max_{i=1}^nR_i maxi=1nRi。考虑到 R i ≤ R i − 1 + 2 R_i\leq R_{i-1}+2 Ri≤Ri−1+2,因此我们只需要暴力从 R i − 1 + 2 R_{i-1}+2 Ri−1+2 开始递减,直到找到第一个回文即可。记变量 z z z 表示当前枚举的 R i R_i Ri,初始时为 0 0 0,则 z z z 在每次 i i i 增大的时候都会增大 2 2 2,之后每次暴力循环都会减少 1 1 1,故暴力循环最多发生 2 n 2n 2n 次,总的时间复杂度为 O ( n ) O(n) O(n)。
问题:给定 m m m 个总长不超过 n n n 的非空字符串,查找所有字符串的最长公共子字符串,如果有多个,任意输出其中一个。其中 1 ≤ m , n ≤ 1 0 6 1\leq m, n\leq 10^6 1≤m,n≤106。
很显然如果存在长度为 k k k 的最长公共子字符串,那么 k − 1 k-1 k−1 的公共子字符串也必定存在。因此我们可以二分最长公共子字符串的长度。假设现在的长度为 k k k,check(k)
的逻辑为我们将所有所有字符串的长度为 k k k 的子串分别进行哈希,将哈希值放入 n n n 个哈希表中存储。之后求交集即可。
时间复杂度为 O ( m + n log n ) O(m+n\log n) O(m+nlogn)。
问题:给定长为 n n n 的字符串,仅由小写英文字母组成,查找该字符串中不同子串的数量。
为了解决这个问题,我们遍历了所有长度为 l = 1 , ⋯ , n l=1,\cdots ,n l=1,⋯,n 的子串。对于每个长度为 l l l,我们将其 Hash 值乘以相同的 b b b 的幂次方,并存入一个数组中。数组中不同元素的数量等于字符串中长度不同的子串的数量,并此数字将添加到最终答案中。
为了方便起见,我们将使用 h [ i ] h [i] h[i] 作为 Hash 的前缀字符,并定义 h [ 0 ] = 0 h[0]=0 h[0]=0。
“参考代码”
int count_unique_substrings(string const& s) {
int n = s.size();
const int b = 31;
const int m = 1e9 + 9;
vector<long long> b_pow(n);
b_pow[0] = 1;
for (int i = 1; i < n; i++) b_pow[i] = (b_pow[i - 1] * b) % m;
vector<long long> h(n + 1, 0);
for (int i = 0; i < n; i++)
h[i + 1] = (h[i] + (s[i] - 'a' + 1) * b_pow[i]) % m;
int cnt = 0;
for (int l = 1; l <= n; l++) {
set<long long> hs;
for (int i = 0; i <= n - l; i++) {
long long cur_h = (h[i + l] + m - h[i]) % m;
cur_h = (cur_h * b_pow[n - i - 1]) % m;
hs.insert(cur_h);
}
cnt += hs.size();
}
return cnt;
}
“CF1200E Compress Words”
给你若干个字符串,答案串初始为空。第 i i i 步将第 i i i 个字符串加到答案串的后面,但是尽量地去掉重复部分(即去掉一个最长的、是原答案串的后缀、也是第 i i i 个串的前缀的字符串),求最后得到的字符串。
字符串个数不超过 1 0 5 10^5 105,总长不超过 1 0 6 10^6 106。
note “题解”
每次需要求最长的、是原答案串的后缀、也是第 i i i 个串的前缀的字符串。枚举这个串的长度,哈希比较即可。
当然,这道题也可以使用 KMP 算法 解决。
“参考代码”
#include
using namespace std;
const int L = 1e6 + 5;
const int HASH_CNT = 2;
int hashBase[HASH_CNT] = {29, 31};
int hashMod[HASH_CNT] = {int(1e9 + 9), 998244353};
struct StringWithHash {
char s[L];
int ls;
int hsh[HASH_CNT][L];
int pwMod[HASH_CNT][L];
void init() { // 初始化
ls = 0;
for (int i = 0; i < HASH_CNT; ++i) {
hsh[i][0] = 0;
pwMod[i][0] = 1;
}
}
StringWithHash() { init(); }
void extend(char c) {
s[++ls] = c; // 记录字符数和每一个字符
for (int i = 0; i < HASH_CNT; ++i) { // 双哈希的预处理
pwMod[i][ls] =
1ll * pwMod[i][ls - 1] * hashBase[i] % hashMod[i]; // 得到b^ls
hsh[i][ls] = (1ll * hsh[i][ls - 1] * hashBase[i] + c) % hashMod[i];
}
}
vector<int> getHash(int l, int r) { // 得到哈希值
vector<int> res(HASH_CNT, 0);
for (int i = 0; i < HASH_CNT; ++i) {
int t =
(hsh[i][r] - 1ll * hsh[i][l - 1] * pwMod[i][r - l + 1]) % hashMod[i];
t = (t + hashMod[i]) % hashMod[i];
res[i] = t;
}
return res;
}
};
bool equal(const vector<int> &h1, const vector<int> &h2) {
assert(h1.size() == h2.size());
for (unsigned i = 0; i < h1.size(); i++)
if (h1[i] != h2[i]) return false;
return true;
}
int n;
StringWithHash s, t;
char str[L];
void work() {
int len = strlen(str); // 取字符串长度
t.init();
for (int j = 0; j < len; ++j) t.extend(str[j]);
int d = 0;
for (int j = min(len, s.ls); j >= 1; --j) {
if (equal(t.getHash(1, j), s.getHash(s.ls - j + 1, s.ls))) { // 比较哈希值
d = j;
break;
}
}
for (int j = d; j < len; ++j) s.extend(str[j]); // 更新答案数组
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str);
work();
}
printf("%s\n", s.s + 1);
return 0;
}
本页面部分内容译自博文 строковый хеш 与其英文翻译版 String Hashing。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。