字符串相关

字符串基础

在这里插入图片描述

字符串的存储

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

标准库

C 标准库是在对字符数组进行操作:char[]/const char*

代码作用
strlen(const char *str)返回从 str[0] 开始直到 ‘\0’ 的字符数。注意,未开启 O2 优化时,该操作写在环条件中复杂度是 O ( n ) O(n) O(n)的。
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)。
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

代码作用
重载了赋值运算符 +当 + 两边是 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重载了比较逻辑运算符,复杂度是 O(n)的。

字符串匹配

单串匹配

一个模式串 (pattern),一个待匹配串,找出前者在后者中的所有出现位置
举例:Oulipo HDU - 1686(哈希或KMP)匹配字符串

多串匹配

多个模式串,一个待匹配串(多个待匹配串可以直接连起来)。
直接当做单串匹配肯定是可以的,但是效率不够高。
举例:Keywords Search HDU - 2222(AC自动机模板)

其他类型的字符串匹配问题

例如匹配一个串的任意后缀、匹配多个串的任意后缀等。

字符串哈希

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

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

我们定义一个把字符串映射到整数的函数 f f f,这个 f f f 称为是 Hash 函数。
我们希望这个函数 f f f 可以方便地帮我们判断两个字符串是否相等。
具体来说,哈希函数最重要的性质可以概括为下面两条:

  1. 在 Hash 函数值不一样的时候,两个字符串一定不一样;
  2. 在 Hash 函数值一样的时候,两个字符串不一定一样(但有大概率一样,且我们当然希望它们总是一样的)。

Hash 函数值一样时原字符串却不一样的现象我们成为哈希碰撞。

我们需要关注的是什么?
时间复杂度和 Hash 的准确率。

通常我们采用的是多项式 Hash 的方法,对于一个长度为 l l l 的字符串 s来说,我们可以这样定义多项式 Hash 函数: f ( s ) = ∑ i = 1 l s [ i ] × b l − i ( m o d f(s)=\sum^{l}_{i=1}s[i]\times b^{l-i}(mod f(s)=i=1ls[i]×bli(mod M ) M) M)。例如,对于字符串 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 f(s)=\sum^{l}_{i=1}s[i]\times b^{i-1}(mod f(s)=i=1ls[i]×bi1(mod M ) M) M) ,这种定义下,同样的字符串 x y z xyz xyz的哈希值就变为了 x + b y + z b 2 x+by+zb^2 x+by+zb2 了。显然,上面这两种哈希函数的定义函数都是可行的,但二者在之后会讲到的计算子串哈希值时所用的计算式是不同的,因此千万注意 不要弄混了这两种不同的 Hash 方式。由于前者的 Hash 定义计算更简便、使用人数更多、且可以类比为一个 b 进制数来帮助理解,所以本文下面所将要讨论的都是使用 f ( s ) = ∑ i = 1 l s [ i ] × b l − i ( m o d f(s)=\sum^{l}_{i=1}s[i]\times b^{l-i}(mod f(s)=i=1ls[i]×bli(mod M ) M) M) 来定义的 Hash 函数。

下面讲一下如何选择 M和计算哈希碰撞的概率。

这里 M 需要选择一个素数(至少要比最大的字符要大),b 可以任意选择。如果我们用未知数 x 替代b ,那么 f ( x ) f(x) f(x) 实际上是多项式环 Z M [ x ] \mathbb{Z}_{M}[x] ZM[x] 上的一个多项式。考虑两个不同的字符串 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 h(x)=f(s)-f(t)=\sum^{l}_{i=1}(s[i]-t[i])x^{l-i}(mod h(x)=f(s)f(t)=i=1l(s[i]t[i])xli(mod M ) M) M) ,其中 l = m a x ( ∣ s ∣ , ∣ t ∣ ) l=max(|s|,|t|) l=max(s,t) 。可以发现 h(x) 是一个 l − 1 l-1 l1 阶的非零多项式。如果 s与t 在x=b 的情况下哈希碰撞,则 b是h(x) 的一个根。由于 h(x) 在 Z M \mathbb{Z}_{M} ZM 是一个域(等价于 M 是一个素数,这也是为什么 M 要选择素数的原因)的时候,最多有 l − 1 l-1 l1 个根,如果我们保证 b 是从 [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 M \frac{1-1}{M} M11=0,此时不可能发生碰撞。

Hash 的实现

参考代码:(效率低下的版本,实际使用时一般不会这么写)

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);
}

Hash 的分析与改进

错误率

若进行 n次比较,每次错误率 1 M \frac{1}M{} M1,那么总错误率是 1 − ( 1 − 1 M ) n 1-(1-\frac{1}{M})^{n} 1(1M1)n。在随机数据下,若 M = 1 0 9 + 7 M=10^{9}+7 M=109+7 n = 1 0 6 n=10^6 n=106,错误率约为 1 1000 \frac{1}{1000} 10001,并不是能够完全忽略不计的。
所以,进行字符串哈希时,经常会对两个大质数分别取模,这样的话哈希函数的值域就能扩大到两者之积,错误率就非常小了。

多次询问子串哈希

单次计算一个字符串的哈希值复杂度是O(n) ,其中 n为串长,与暴力匹配没有区别,如果需要多次询问一个字符串的子串的哈希值,每次重新计算效率非常低下。
一般采取的方法是对整个字符串先预处理出每个前缀的哈希值,将哈希值看成一个b 进制的数对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]\times b^{i-1}+s[2]\times b^{i-2}+...+s[i-1]\times 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 − l ] × b + s [ r ] f(s[l...r])=s[l]\times b^{r-l}+s[l+1]\times b^{r-l-1}+...+s[r-l]\times b+s[r] f(s[l...r])=s[l]×brl+s[l+1]×brl1+...+s[rl]×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(1) 的回答每次询问(当然也可以快速幂 O(log n)的回答每次询问)。

Hash 的应用

字符串匹配

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

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

问题:给定长为 n 的源串 ,以及长度为m 的模式串 ,要求查找源串中有多少子串与模式串匹配。 s ′ s' s 与s 匹配,当且仅当 s ′ s' s与s 长度相同,且最多有 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 第一个不同的位置。之后将 s ′ s' s 与 p 在这个失配位置及之前的部分删除掉,继续查找下一个失配位置。这样的过程最多发生 k 次。总的时间复杂度为 O ( m + k n O(m+kn O(m+kn l o g 2 log_2 log2 m ) m) m)

最长回文子串

二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。时间复杂度 O ( n O(n O(n l o g log log n ) n) n)
这个问题可以使用 manacher 算法 在 O ( n ) O(n) O(n) 的时间内解决。

通过哈希同样可以 O ( n ) O(n) O(n) 解决这个问题,具体方法就是记 R i R_{i} Ri 表示以 i i i 作为结尾的最长回文的长度,那么答案就是 m a x i = 1 n R i max^{n}_{i=1}R_{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 ,则 z z z 在每次 i i i 增大的时候都会增大 2,之后每次暴力循环都会减少1 ,故暴力循环最多发生2n 次,总的时间复杂度为 O(n)。

最长公共子字符串

问题:给定 m 个总长不超过n 的非空字符串,查找所有字符串的最长公共子字符串,如果有多个,任意输出其中一个。其中 1 ≤ m , n ≤ 1 0 6 1\leq m,n\leq10^6 1m,n106

很显然如果存在长度为k 的最长公共子字符串,那么 k-1 的公共子字符串也必定存在。因此我们可以二分最长公共子字符串的长度。假设现在的长度为k ,check(k) 的逻辑为我们将所有所有字符串的长度为k 的子串分别进行哈希,将哈希值放入 n 个哈希表中存储。之后求交集即可。
时间复杂度为 O ( n O(n O(n l o g 2 log_2 log2 n m ) \frac{n}{m}) mn)

确定字符串中不同子字符串的数量

问题:给定长为n 的字符串,仅由小写英文字母组成,查找该字符串中不同子串的数量。

为了解决这个问题,我们遍历了所有长度为 l = 1 , . . . , n l=1,...,n l=1,...,n 的子串。对于每个长度为 l l l,我们将其 Hash 值乘以相同的 b 的幂次方,并存入一个数组中。数组中不同元素的数量等于字符串中长度不同的子串的数量,并此数字将添加到最终答案中。
为了方便起见,我们将使用 h [ i ] h[i] h[i] 作为 Hash 的前缀字符,并定义 h [ 0 ] = 0 h[0]=0 h[0]=0

字典树 (Trie)

字典树,英文名 trie。顾名思义,就是一个像字典一样的树。
先放一张图:

可以发现,这棵字典树用边来代表字母,而从根结点到树上某一结点的路径就代表了一个字符串。举个例子, 1 → 4 → 8 → 12 1\rightarrow4\rightarrow8\rightarrow12 14812 表示的就是字符串 caa。

trie 的结构非常好懂,我们用 δ ( u , c ) \delta(u,c) δ(u,c) 表示结点 u u u c c c 字符指向的下一个结点,或着说是结点 u u u 代表的字符串后面添加一个字符 c c c 形成的字符串的结点。( c c c 的取值范围和字符集大小有关,不一定是 0 0 0~26。)

有时需要标记插入进 trie 的是哪些字符串,每次插入完成时在这个字符串所代表的节点处打上标记即可。
Phone List POJ - 3630(字典树模板题)

应用

检索字符串

字典树最基础的应用——查找一个字符串是否在“字典”中出现过。
例题:字典树模板+洛谷P2580 于是他错误的点名开始了

AC 自动机

trie 是 AC 自动机 的一部分。

维护异或极值

将数的二进制表示看做一个字符串,就可以建出字符集为{0,1} 的 trie 树。

前缀函数与 KMP 算法
Boyer-Moore算法
Z 函数(扩展 KMP)
自动机

AC 自动机

Keywords Search HDU - 2222(AC自动机模板)

后缀数组 (SA)
后缀自动机 (SAM)
后缀平衡树
广义后缀自动机
后缀树

Manacher

最长回文 HDU - 3068(求最长回文串的长度【马拉车算法Manacher】)

回文树
序列自动机
最小表示法
Lyndon 分解

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值