【字符串基础】

字符串部分简介

字符串,就是由字符连接而成的序列。

常见的字符串问题包括字符串匹配问题、子串相关问题、前缀/后缀相关问题、回文串相关问题、子序列相关问题等。


字符串基础

定义

字符集

一个 字符集 Σ \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[i1]

子串

字符串 S S S子串 S [ i . . j ] , i ≤ j S[i..j],i≤j S[i..j]ij,表示 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| 1p1<p2<<pkS

后缀

后缀 是指从某个位置 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..∣S1]

真后缀 指除了 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] ∀1is,s[i]=s[s+1i] s s s

字符串的存储

  • 使用 char 数组存储,用空字符 \0 表示字符串的结尾(C 风格字符串)。
  • 使用 C++ 标准库提供的 string类。
  • 字符串常量可以用字符串字面量(用双引号括起来的字符串)表示。

标准库

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 str2str1 字典序小返回负值,两者一样返回 0str1 字典序更大则返回正值。请注意,不要简单的认为返回值只有 01-1 三种,在不同平台下的返回值都遵循正负,但并非都是 01-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):若 str2str1 的子串,则返回 str2str1 的首次出现的地址;如果 str2 不是 str1 的子串,则返回 NULL
  • strchr(const char *str, int c):找到在字符串 str 中第一次出现字符 c 的位置,并返回这个位置的地址。如果未找到该字符则返回 NULL
  • strrchr(const char *str, char c):找到在字符串 str 中最后一次出现字符 c 的位置,并返回这个位置的地址。如果未找到该字符则返回 NULL

C++ 标准库

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 mn

在最好情况下,BF 算法匹配成功时,时间复杂度为 O ( n ) O(n) O(n);匹配失败时,时间复杂度为 O ( m ) O(m) O(m)

在最坏情况下,每趟不成功的匹配都发生在模式串的最后一个字符,BF 算法要执行 m ( n − m + 1 ) m(n-m+1) m(nm+1) 次比较,时间复杂度为 O ( n m ) O(nm) O(nm)

如果模式串有至少两个不同的字符,则 BF 算法的平均时间复杂度为 O ( n ) O(n) O(n)。但是在 OI 题目中,给出的字符串一般都不是纯随机的。

Hash 的方法

参见:字符串哈希

KMP 算法

参见:前缀函数与 KMP 算法


字符串哈希

定义

我们定义一个把字符串映射到整数的函数 f f f,这个 f f f 称为是 Hash 函数。

我们希望这个函数 f f f 可以方便地帮我们判断两个字符串是否相等。

Hash 的思想

Hash 的核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。

这里的「值域较小」在不同情况下意义不同。
在 哈希表 中,值域需要小到能够接受线性的空间与时间复杂度。
在字符串哈希中,值域需要小到能够快速比较( 1 0 9 10^9 109 1 0 18 10^{18} 1018 都是可以快速比较的)。
同时,为了降低哈希冲突率,值域也不能太小。

性质

具体来说,哈希函数最重要的性质可以概括为下面两条:

  1. 在 Hash 函数值不一样的时候,两个字符串一定不一样;

  2. 在 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]×bli(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]×bi1(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]×bli(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])xli(modM),其中 l = max ⁡ ( ∣ s ∣ , ∣ t ∣ ) l=\max(|s|,|t|) l=max(s,t)。可以发现 h ( x ) h(x) h(x) 是一个 l − 1 l-1 l1 阶的非零多项式。

如果 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 l1 个根,如果我们保证 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} Ml1。简单验算一下,可以发现如果两个字符串长度都是 1 1 1 的时候,哈希碰撞的概率为 1 − 1 M = 0 \frac{1-1}{M}=0 M11=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)

Hash 的分析与改进

错误率

假定哈希函数将字符串随机地映射到大小为 M M M 的值域中,总共有 n n n 个不同的字符串,那么未出现碰撞的概率是 ∏ i = 0 n − 1 M − i M \prod_{i = 0}^{n-1} \frac{M-i}{M} i=0n1MMi(第 i i i 次进行哈希时,有 M − i M \frac{M-i}{M} MMi 的概率不会发生碰撞)。在随机数据下,若 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]bi1+s[2]bi2++s[i1]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]brl+s[l+1]brl1++s[r1]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)fl1(s)×brl+1 成立(可以手动代入验证一下),因此我们用这个式子就可以快速得到子串的哈希值。其中 b r − l + 1 b^{r-l+1} brl+1 可以 O ( n ) O(n) O(n) 的预处理出来然后 O ( 1 ) O(1) O(1) 的回答每次询问(当然也可以快速幂 O ( log ⁡ n ) O(\log n) O(logn) 的回答每次询问)。

Hash 的应用

字符串匹配

求出模式串的哈希值后,求出文本串每个长度为模式串长度的子串的哈希值,分别与模式串的哈希值比较即可。

允许 k k k 次失配的字符串匹配

问题:给定长为 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 1n,m106 0 ≤ k ≤ 5 0\leq k\leq 5 0k5

这道题无法使用 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 RiRi1+2,因此我们只需要暴力从 R i − 1 + 2 R_{i-1}+2 Ri1+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 1m,n106

很显然如果存在长度为 k k k 的最长公共子字符串,那么 k − 1 k-1 k1 的公共子字符串也必定存在。因此我们可以二分最长公共子字符串的长度。假设现在的长度为 k k kcheck(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。

你可能感兴趣的:(数据结构与算法,哈希算法,算法,数据结构)