字符串

Falling_Sakura

String

声明

1
2
3
4
5
6
string str1;   //默认构造函数
string str2("abc"); //传入string或者c_str
string str3(4, 'a'); //4个字符a构建成string
string str4(str2, 1); //从str2的位置1开始构造
string str5(str2.begin(), str2.end()); //从str2的迭代器开始构造
cout << str2 << endl << str3 << endl<< str4 << endl<<str5<<endl;
1
2
3
4
abc
aaaa
bc
abc

拼接

1
+=
1
2
3
4
5
6
7
8
string str1("str1");
str1.append("123"); //直接添加字符串至尾部
cout << str1 << endl;
str1.append("1230", 2); //从参数字符串起始位置开始添加2个字符到str1
cout << str1 << endl;
string str2("str2");
str1.append(str2, 0, 1); //把str2的区间[0,1)的字符添加至str1
cout << str2 << endl << str1 << endl;
1
2
3
4
str1123
str112312
str2
str112312s

Push_Back

1
a.puah_back('a');

删除

1
2
3
4
5
string str1("string1");
str1.erase(0, 1); //删除指定区间
cout << str1 << endl;
str1.erase(2); //指定删除的起始位置,直至末尾
cout << str1 << endl;
1
2
tring1
tr

弹出

1
str.pop_back();

清空

1
str.clear();

查询

1
str.find("tata");

切割

1
2
3
4
5
string str1("string1string1");
string temp = str1.substr(1, 2); //指定区间的字串
cout << temp << endl;
string temp2 = str1.substr(1); //从指定位置开始切割至尾部
cout << temp2 << endl;
1
2
tr
tring1string1

数字转换

1
2
3
4
5
6
7
string str1("124");
cout << stoi(str1) << endl; //转为整数
cout << stof(string("124.3")) << endl;//转为浮点型
string str = to_string(124); //数字转为字符串
cout << str << endl;
str = to_string(13.4);
cout << str << endl;

STL

strcmp

比较两个字符串。

1
2
char op[20];
strcmp(op, "osne");

相等,返回值为 0

前者大于后者,返回值为正数

前者小于后者,返回值为负数

KMP

推荐阅读:算法学习笔记(13): KMP算法

KMP 是一种字符串匹配算法,它可以在一个字符串(S)中找出所有另一个字符串(T)的出现。

如果是暴力匹配的话,遇到不匹配的一个字符,就得把T字符串相对S向右移动一位,同时将T指针归零,S指针与T对齐进行重新匹配,如下图:

image.png

但是这样极端情况下复杂度会达到 O(nm)O(nm),是无法接受的,于是 KMP 算法应运而生,它可以将已经匹配的部分进行重复利用,而不是像暴力那样傻傻的清零。

KMP 算法中定义了一个 pmtpmt 数组,pmtipmt_i 代表字符串的前 i+1i+1 位(其实就是 S0iS_{0\sim{i}},因为字符串下标是从 0 开始的)的 Border 长度

Border 为一个字符串前缀与后缀最长匹配的部分,例如 abacaba\text{abacaba},它的 Border 就是 aba\text{aba},长度为 3。

所以每次匹配失败之后就可以把T串已经匹配的部分变为它的 Border,假如T串当前匹配到下标为ii,那么就相当于把T串向右平移了 istrlen(Borderi)+1i-strlen(Border_i)+1 位,这样就可以利用之前已经匹配的部分继续匹配而无需归零。

注意 Border 的长度不超过自身字符串的长度。

代码实现大概是这样的:

1
2
3
4
5
6
7
for(int i=0,j=0;i<s.length();i++)
{
while (j&&s[i]!=t[j]) j=pmt[j-1];//不断向前跳,如果没有的话,最后会跳到0
if (s[i]==t[j]) j++;//当前位匹配
if (j==t.length())//已经完全匹配了一次,利用已匹配的部分去找下个完全匹配
j=pmt[j-1];
}

举个例子,S=ABCABACABCAB,T=ABCABS=\text{ABCABACABCAB},T=\text{ABCAB},先预处理出 TT 对应的 pmtpmt

pmtpmtBorderBorder
00
10
20
31
42

然后进行匹配,设 iiSS 的指针,jjTT 的指针,那么当 i=j=4i=j=4 的之后第一次匹配完成,这时候 i=4,j=5,j=pmtj1=2i=4,j=5,j=pmt_{j-1}=2,然后 i=5i=5,就可以把 ABAB 这个前缀给利用起来了,注意 jj 始终是待匹配项,要比已经匹配的多 11,而当 j=lengthtj=length_t 的时候,说明前 jj 个都已经匹配完了,现在在看下一个字符,所以是要跳回到 pmtj1pmt_{j-1},其中因为字符串从 00 开始存在不少细节,请自行理解。

那么 pmtpmt 怎么求呢?

相当于拿自己和自己进行匹配,代码如下。

1
2
3
4
5
6
for(int i=1,j=0;i<t.length();i++)
{
while(j&&t[i]!=t[j]) j=pmt[j-1];
if (t[i]==t[j]) j++;
pmt[i]=j;
}

当然也有另一种写法,是存最长 Border 的右端点下标,记为 nxtinxt_i,与存长度类似,拿例题举例:

P3435

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<bits/stdc++.h>
using namespace std;
int n;
char a[1000005];
int nxt[1000005];
long long ans;
int main()
{
scanf("%d",&n);
scanf("%s",a);
memset(nxt,-1,sizeof(nxt));
for(int i=1,j=-1;i<n;i++)
{
while(j>-1&&a[i]!=a[j+1]) j=nxt[j];
if(a[i]==a[j+1]) j++;
nxt[i]=j;
}
for(int i=1,j=1;i<n;i++)
{
while(nxt[j]>-1) j=nxt[j];
if(nxt[i]>-1) nxt[i]=j;
ans+=i-j;
j=i+1;
}
printf("%lld",ans);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<bits/stdc++.h>
using namespace std;
int n;
char a[1000005];
int pmt[1000005];
long long ans;
int main()
{
scanf("%d",&n);
scanf("%s",a);
for(int i=1,j=0;i<n;i++)
{
while(j&&a[i]!=a[j]) j=pmt[j-1];
if(a[i]==a[j]) j++;
pmt[i]=j;
}
for(int i=1,j=2;i<n;i++)
{
while(pmt[j-1]) j=pmt[j-1];
if(pmt[i]) pmt[i]=j;
ans+=i-j+1;
j=i+2;
}
printf("%lld",ans);
return 0;
}

这道题解法就是对每个前缀找出它的最小 Border 的长度,然后拿当前前缀长度减去这个最小 Border 的长度,再把每个前缀的答案加起来就是最后的答案,细节见代码,有助于加深对 KMP 两种写法的理解。

P2375动物园

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int mod=1e9+7;
int n;
char a[N];
int nxt[N],num[N];
long long ans;
int main()
{
scanf("%d",&n);
while(n--)
{
scanf("%s",a+1);
ans=1;
num[1]=1;
int l=strlen(a+1);
for(int i=2,j=0;i<=l;i++)
{
while(j&&a[i]!=a[j+1]) j=nxt[j];
if(a[i]==a[j+1]) j++;
nxt[i]=j;
num[i]=num[j]+1;
}
for(int i=2,j=0;i<=l;i++)
{
while(j&&a[i]!=a[j+1]) j=nxt[j];
if(a[i]==a[j+1]) j++;
while((j<<1)>i) j=nxt[j];
ans=ans*(num[j]+1)%mod;
// printf("%d\n",num[j]);
}
printf("%lld\n",ans);
// for(int i=1;i<=l;i++) printf("%d\n",num[i]);
}
return 0;
}

先把所有的后缀数量和都求出来,这个可以通过递推关系求出,即sumi=sumnxti+1sum_i=sum_{nxt_i}+1

然后再匹配处理一次,匹配的过程中的时候对于每一个jj,一旦超过当前字符串长度的一半,就往前跳直到长度小于字符串的一半并累加答案。

EXKMP(Z)

Z 算法又被叫做扩展 KMP 算法,其核心是 Z 函数。
图片来源

ZiZ_i定义为字符串与ii位置到字符串末尾与整个字符串的最长公共前缀的长度,即处理每一个后缀与原字符串的 LCP(最长公共前缀)。

贴一张图:

image.png

特别地,令Z0=0Z_0=0

假设Zi0Z_i\ne0,定义区间[i,i+Zi1][i,i+Z_i-1]为一个Z-Box\text{Z-Box},那么ZBoxZ-Box一定是这个字符串的一个前缀。

我们从左向右枚举ii,并维护右端点最大的 ZBoxZ-Box 和它对应的左端点,这里保证lil\le{i}

那么就有以下三种情况:

  • i>ri>r,这说明我当前的ZBoxZ-Box左端点都比之前的右端点要大,所以没有什么可以利用的,就只能暴力往后枚举出当前的 ZBoxZ-Box 并维护信息。
  • iri\le{r},那么就可以对已经求出的 lcplcp 进行利用,见下图解释:

image.png

黑色为原序列,我现在要求每一个后缀与原序列的 lcp,即最大公共前缀。

比如我现在要求粉色点的 lcp,

但是发现这个点在当前最大lcp的右端点的范围内(红色线),即我们知道了灰色与黑色的 lcp,而灰色与蓝色的粉色部分又相同,于是蓝色和黑色的粉色部分也相同。

而对于粉色部分,我们已经知道了绿色与的 lcp,绿色<粉色,相同位置的绿色=粉色,于是绿色的 lcp 就可以被粉色点继承;假如绿色>粉色,但是超出部分在蓝色部分中不一定相同,因此最多能继承到粉色的边界处,因此绿色的 lcp的右端点下标要与粉色右端点下标取min\min后继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<bits/stdc++.h>
using namespace std;
char s[1000005];
int n;
int z[100005];
int main()
{
n=strlen(s);
z[0]=n;
for(int i=1,l=0,r=0;i<n;i++)
{
if(z[i-l]<r-i+1)//当前已经相同的部分比已知lcp要大
z[i]=z[i-l];
else
{
z[i]=max(r-i+1,0);
while(i+z[i]<n&&s[z[i]]==s[i+z[i]]) z[i]++;
l=i,r=i+z[i]-1;
}
}
return 0;
}

注意 l,rl,r 的定义,他们的初始值要比初始变量少 1。

这个过程中,rr 是单调不减的,每次内层循环都会使 rr 加一,所以时间复杂度为O(n)O(n)

Manacher

一种解决回文串问题的算法

回文串分为奇回文串和偶回文串,而偶回文串不存在固定的回文中心,因此就要进行一些处理,在字符间加上一些其他字符,比如#或者$

奇回文串abcba=$a$b$c$b$a$\text{abcba}=\text{\$a\$b\$c\$b\$a\$}

这样对于一个偶回文串abccba\text{abccba},就可以处理为$a$b$c$c$b$a$\text{\$a\$b\$c\$c\$b\$a\$},这样就可以用奇回文串的处理方法来处理偶回文串了。

设数组 did_i 为以 sis_i 为中心的奇回文串的数量,长度分别为 1,3,,2di11,3,\dots,2d_i-1,Manacher 就可以 O(n)O(n) 处理出这个数组。

而以 ii 为中心的最长回文串的长度就为 di1d_i-1

Manacher 的思想与 Z 算法很像,都是维护了一个最远的右端点,不过在 Manacher 里维护的是右端点最大的奇回文串,

当枚举到一个 ii 时,我们进行分类讨论:

  • i>ri>r,那就暴力计算did_i,并维护l,rl,r
  • iri\le{r},那么就可以找这个rr所在回文串的回文中心,这样就可以找到它的对称点,设这个对称点为 jj,如果以 jj 为回文中心的回文串左端点大于 ll,那么说明 djd_j 是被当前这个回文串完全包含的了,而对称位置对应相等,因此可以直接继承,di=djd_i=d_j
    • 如果以 jj 为回文中心的回文串左端点小于等于 ll,那么只能知道 ii 的回文串右端点至少为 jl+1j-l+1或者 ri+1r-i+1,剩余部分可以暴力解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;
int n;
int d[1000005];
char s[1000005];
int main()
{
for(int i=0,l=0,r=-1;i<n;i++)
{
int j=l+r-i;//对称点
int dj=j>=0?d[j]:0;//不能越界
d[i]=max(min(dj,r-i+1),0);
if(j-dj<l)//j-dj+1<=l
{
while(i-d[i]>=0&&i+d[i]<n&&s[i-d[i]]==s[i+d[i]]) d[i]++;
l=i-d[i]+1,r=i+d[i]-1;
}
}
return 0;
}

为了使自己本身也能被算上,所以初始 rr 为-1。

这个过程中,rr 是单调不减的,每次内层循环都会使 rr 加一,所以时间复杂度为O(n)O(n)

Trie树

字典树,可以理解为 nn 叉树,nn 取决于字符的种类。

模板:

P8306

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<bits/stdc++.h>
using namespace std;
const int N=3e6+10;
int t,cnt=1;
int nxt[N][80];
int val[N];
char a[N];
void init(int x)
{
// memset(nxt,0,sizeof(nxt));
// memset(val,0,sizeof(val));
for(int i=0;i<=x;i++)
{
val[i]=0;
for(int j=0;j<=77;j++)
nxt[i][j]=0;
}
cnt=1;
}
void insert(const string &s)
{
int cur=1;
for(auto &c:s)
{
if(!nxt[cur][c-'0']) nxt[cur][c-'0']=++cnt;
cur=nxt[cur][c-'0'];
val[cur]++;
}
}
int query(const string &s)
{
int cur=1;
for(auto &c:s)
{
if(!nxt[cur][c-'0']) return 0;
cur=nxt[cur][c-'0'];
}
return val[cur];
}
int main()
{
cin>>t;
while(t--)
{
int n,q;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
string s;
scanf("%s",a);
s=a;
insert(s);
}
for(int i=1;i<=q;i++)
{
string s;
scanf("%s",a);
s=a;
printf("%d\n",query(s));
}
init(cnt);
}
return 0;
}

例题:

P2580

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<bits/stdc++.h>
using namespace std;
int n,cnt,q;
char a[100];
int nxt[1000005][26];
int vis[1000005];
void init()
{
memset(nxt,0,sizeof(nxt));
cnt=1;
}
void insert(const string &s)
{
int cur=1;
for(auto &c:s)
{
if(!nxt[cur][c-'a']) nxt[cur][c-'a']=++cnt;
cur=nxt[cur][c-'a'];
}
}
int query(const string &s)
{
int cur=1;
for(auto &c:s)
{
if(!nxt[cur][c-'a']) return 0;
cur=nxt[cur][c-'a'];
vis[cur]++;
}
for(int i=0;i<26;i++)
if(vis[nxt[cur][i]]) return 0;
if(vis[cur]>1) return 1;
return 2;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",a);
string s=a;
insert(s);
}
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%s",a);
string s=a;
int op=query(s);
if(op==0) printf("WRONG\n");
else if(op==1) printf("REPEAT\n");
else if(op==2) printf("OK\n");
}
return 0;
}

可持久化 Trie 树

对于属于修改的但是已有节点,复制粘贴。

对于不属于修改但是原来有的节点,直接连接(相当于复制粘贴的时候直接把属性复制了过来)。

对于属于修改的新节点,新建一个节点。

都是对于上一个版本的操作。

image.png

每次先得到上一个版本,然后开一个新的节点,把根节点先克隆过来。

然后遍历每个节点,如果有就复制粘贴,没有就新建。

AcWing256

怎么判断左界?

记录一下每个节点的子树里的下标最大值即可。

怎么判断右界?看第 rr 个版本即可,可持久化。

前缀和处理:

ans=sNsp1xans=s_N\oplus{s_{p - 1}}\oplus x

于是我们在 lrl\sim r 中贪心地找到这样一个 p1p - 1 即可,也就是在 l1r1l-1\sim r-1 中找一个 pp,使得他们的异或值最大。

  • Title: 字符串
  • Author: Falling_Sakura
  • Created at : 2023-08-06 09:17:34
  • Updated at : 2025-09-24 10:28:17
  • Link: https://vercel.fallingsakura.top/3023808606.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments