字符串用法、算法总结

时隔多年,我又来写总结啦!!!

update: 2023.9.26

update: 2024.2.1

先总述一下
字符串(string)是一种常用的数据结构,存储方式类似于数组,c++里规定其存储的起始下标为0,通常用 l e n = s . s i z e ( ) len=s.size() len=s.size()来表示字符串长度。

另外,字符串也可以char数组的方式实现。此时需用len=strlen(s)求其长度

用法

c++中,string自带的用法有很多,此处仅介绍几个较为初级的、常用的用法。
(注:char数组没有以下用法)

插入

string支持最基础的+=插入。

string s="abcd";
char ch='e';
s+=ch;
//此时s="abcde"

下面列举 i n s e r t ( ) insert() insert()函数的具体用法

string s="abcd",ch="wxyz";
s.insert(2,ch);//在字符串s的位置2上插入字符串ch,此时s="abwxyzcd"
s.insert(2,ch,1,3);//在字符串s的2的位置上插入字符串ch中以位置1为开头,长度为3的字符串,此时s="abxyzcd"
char ch1='x';
s.insert(2,3,ch1);//在字符串s的位置2上插入3个字符ch1,此时s="abxxxcd"

感谢大佬纠错!!!@liang_2026

删除

string s="abcdefg";
s.erase(2,4);//删除字符串s的以位置2为开头、长度为4的子串,此时s="abg"

替换

string s="abcdefg",ch="xyz";
s.replace(2,4,ch);//将字符串s的以位置2为开头,长度为4的子串换成字符串ch,此时s="abxyzg"

用法到此结束。

算法

Hash

引入hash

hash表,又称散列表,一般由hash函数和链表结构共同实现。

hash算法的本质是建立映射关系,其他类似的算法还有离散化和 m a p (数据结构) map(数据结构) map(数据结构)。hash的思想是取一个 p p p值,将数字 x x x存在 h e a d head head数组中 x m o d    p x\mod p xmodp的格子里。但是这显然会有冲突(既必定存在多个 x x x,使得 x m o d    p x\mod p xmodp的值相等)。处理这种冲突的最常用方法为拉链法——开一个链表结构(类似于图论中的链式前向星)。具体写法见code:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int a[maxn],head[maxn]/*存储的格子*/;
int A/*数组长度*/,p/*为了达到效果,p要选大于A的质数*/,tot=0;
struct star {//链式前向星
	int nxt,val;
};
star h[maxn];
bool check(int x) {//找质数
	if(x==1) return 0;
	if(x==2) return 1;
	if(x%2==0) return 0;
	for(int j=3;j<=sqrt(x);j+=2)
		if(x%j==0) return 0;
	return 1;
}
void add(int x) {
	int k=(x%p+p)%p;//数字x的hash值,这么写是为了避免x<0的情况
	h[++tot].val=x;//数字原来的值
	h[tot].nxt=head[k];//数字在格子里的位置
	head[k]=tot;//这个格子最后一次存入数字是在第tot个前向星中
}
bool Find(int x) {//查找数字x是否出现过
	for(int j=head[x%p];j!=-1;j=h[j].nxt)
		if(h[j].val==x) return 1;//出现过
	return 0;
}
int main() {
	scanf("%d",&A);
	for(int i=1;i<=A;i++)
		scanf("%d",&a[i]);
	memset(head,-1,sizeof(head));
	for(p=A+1;;p++)
		if(check(p)) break;
	for(int i=1;i<=A;i++)
		add(a[i]);//插入
	return 0;
}

hash算法优于离散化和map的地方在于它的时间复杂度是严格O(n)的。在忽略常数的情况下

字符串hash

普通的hash算法针对的是数字,无法存储字符串。这个算法就是针对字符串而出现的。

对于一个字符串 s s s,它的hash值求法是:将 s s s中的每个字符都转换成一个数字,然后选取一个数 b a s e base base进制数,将 s s s转换位一个 b a s e base base进制数的数字。为了方便,我们一般将字符对应的ASCLL码作为其转换的数字,进制数一般取131(因为小写字母z的ASCLL码是所有大小写字母中最大的,为126)。因为这个数字过大,所以我们要进行取模运算。又因为取模运算常数很大,所以我们引入一个unsigned long long(无符号64位整型,范围0~264-1)。当数字大于 0 ∼ 0 \sim 0 264 − 1 -1 1时,它会自动对264取模,而且常数很小(可以想象成位运算)。

所以,一般查找这个字符串有没有出现过时,仅靠字符串hash是不够的,需要搭配数字hash共同实现。code如下:

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int maxn=50050;
int mod=50021;
struct Hash {//数字hash(见上文)
	int nxt;
	ull val;
};
Hash edge[maxn];
int p=131,n;
int head[maxn],tot=0;
ull Get(string s) {//字符串hash值
	ull num=0;
	for(int j=0;j<s.size();j++)
		num=num*p+(ull)(s[j]-'a'+1);//转换为p进制数
	return num;
}
int H(ull x) {
	return (x%mod+mod)%mod;
}
bool add(ull x) {
	int k=H(x);
	for(int j=head[k];j!=-1;j=edge[j].nxt)
		if(edge[j].val==x) return 1;//出现过
	edge[++tot].val=x;//未出现,插入
	edge[tot].nxt=head[k];
	head[k]=tot;
	return 0;
}
int main() {
	scanf("%d",&n);
	memset(head,-1,sizeof(head));
	for(int i=1;i<=n;i++) {
		string x;
		cin>>x;
		ull res=Get(x);//得出字符串x的hash值
		if(add(res)/*查找res这个数字有没有出现过,如果没有就插入*/) 
			printf("%d\n",i);
	}
	return 0;
}

现在最基础的字符串hash我们已经了解。通过字符串hash我们还可完成更多操作。我们先引入几个公式:

H(S):字符串S的hash值
H(S+T)=H(S)*p^length(T)+H(T)
H(T)=H(S+T)-H(S)*p^length(T)

通过这三个公式,我们即可在O(n)的时间复杂度内求出字符串S的hash值,并在O(1)的时间复杂度内求出S的任意子串的hash值。例题:兔子与兔子
code:

#include<bits/stdc++.h>
using namespace std;
string s;
const int maxn=1e6+10;
typedef unsigned long long ull;
ull f[maxn],q[maxn];
int p=131;
void Prepare() {
	for(int i=0;i<s.size();i++) {
		if(i==0) f[i+1]=(ull)(s[i]-'a'+1);
		else f[i+1]=f[i]*p+(ull)(s[i]-'a'+1);
		if(i==0) q[i]=1;
		else q[i]=q[i-1]*p;
	}
}
int main() {
	cin>>s;
	Prepare();//O(n)求s各个位置的hash值
	int m;
	scanf("%d",&m);
	for(int i=1;i<=m;i++) {
		int l1,l2,r1,r2;
		scanf("%d%d%d%d",&l1,&l2,&r1,&r2);
		//判断区间[l1,l2]与[r1,r2]是否相等
		ull h1=f[l2]-f[l1-1]*q[l2-l1+1];
		ull h2=f[r2]-f[r1-1]*q[r2-r1+1];
		if(h1==h2) puts("Yes");
		else puts("No");
	}
	return 0;
}

KMP模式匹配

我们先来想一道题目:给定两个串A,B(保证 A . s i z e ( ) < = B . s i z e ( ) A.size()<=B.size() A.size()<=B.size()),求A串在B串中出现的次数以及每次出现的位置

通过我们之前学过的知识,我们便可以解决掉这道题——字符串Hash。部分code如下:

Prepare();//求A,B两个串每个位置的Hash值
int m=A.size(),ans=0;
for(int i=m;i<=B.size();i++)
	if((f_B[i]-f_B[i-m]*p[m])==f_A/*整个A串的Hash值*/) {
		ans++;
		printf("%d\n",i-m+1);//出现的位置
	}
printf("%d",ans);

它的时间复杂度是O(n+m)的。而我们接下来要讲的KMP,它的时间复杂度也是O(n+m)的。那它是不是没什么用呢??

KMP算法相比于Hash,最大的优点是它不光可以让我们求得A串在B串中出现的次数,还可以告诉我们一些其他的、有用的信息。 我们在后面会详细讲述。
下面开始正式讲述KMP模式匹配算法

KMP算法的核心在于两个数组: n e x t next next f f f n e x t [ i ] next[i] next[i]表示A串以i为结尾的非前缀子串与A串的前缀子串相等的最大长度。 如果难以理解,请见下方的例子:

A=abababac
next[1]=0;//没有以1为结尾的非前缀子串
next[2]=0;//A[2]!=A[1](以2为结尾的非前缀子串只有A[2])
next[3]=1;//A[3]:a
next[4]=2;//A[3~4]:ab
next[5]=3;//A[3~5]:aba
next[6]=4;//A[3~6]:abab
next[7]=5;//A[3~7]:ababa
next[8]=0;//无

f [ i ] f[i] f[i]表示B串以i为结尾的子串与A串的前缀子串相等的最大长度。 如果难以理解,请见下方的例子:

A=abac,B=abcabacbabcbab
f[1]=1;
f[2]=2;
f[3]=0;
f[4]=1;
f[5]=2;
f[6]=3;
f[7]=4;//A[1~4]=B[4~7]
......

通过这个例子我们可以发现,当 f [ i ] = = A . s i z e ( ) f[i]==A.size() f[i]==A.size()的时候,说明此时已经在B串中找到了一个子串,使他等于A串。所以我们最后在统计答案时,就是

if(f[i]==A.size()) {
	ans++;
	printf("%d\n",i-A.size()+1);
}

现在,我们了解了 n e x t next next, f f f数组,接下来我们就来探讨next、f数组的求法

n e x t next next数组的求法

在讲述之前,我们先来引进一个概念:候选项。什么是候选项呢?我们来看一个例子:

A=abababab
next[3]=1;//a
next[5]=3;//aba
next[7]=5;//ababa

通过 n e x t next next数组的定义,我们可知, n e x t [ i ] next[i] next[i]存储的是 A A A串以 i i i为结尾的非前缀子串与 A A A串的前缀子串相等的最大长度,但是,通过上面的例子可知,子串 a b a aba aba a a a也可以满足以上(除最大长度以外的)条件。我们把类似子串对应的next值(既上述例子中的next[5],next[3]) 称为 n e x t [ i ] next[i] next[i](既上述例子中的 n e x t [ 7 ] next[7] next[7])的候选项。

了解了候选项的定义,我们就来深入探讨一下候选项的作用。

现在,让我们再引入一个引理:对于next[i],如果存在j0是next[i]的一个候选项,那么小于j0的最大的next[i]的候选项是next[j0]。比如上述例子中,3(既 n e x t [ 5 ] next[5] next[5])是 n e x t [ 7 ] next[7] next[7]的候选项,那么,小于3的 n e x t [ 7 ] next[7] next[7]的最大候选项是 n e x t [ 3 ] next[3] next[3](既1)。在此不作证明因为怕读者不懂,请读者自行思考或查阅《算法进阶》等资料。

那么,有了这个引理,当我们求出了 n e x t [ i − 1 ] next[i-1] next[i1]后,我们便可得知 n e x t [ i − 1 ] next[i-1] next[i1]的所有候选项: n e x t [ i − 1 ] , n e x t [ n e x t [ i − 1 ] ] , n e x t [ n e x t [ n e x t [ i − 1 ] ] ] , . . . . . . next[i-1],next[next[i-1]],next[next[next[i-1]]],...... next[i1],next[next[i1]],next[next[next[i1]]],......所以,我们的 n e x t [ i ] next[i] next[i]的值就在 n e x t [ i − 1 ] + 1 , n e x t [ n e x t [ i − 1 ] ] + 1 , n e x t [ n e x t [ n e x t [ i − 1 ] ] ] + 1 , . . . . . . , 0 next[i-1]+1,next[next[i-1]]+1,next[next[next[i-1]]]+1,......,0 next[i1]+1,next[next[i1]]+1,next[next[next[i1]]]+1,......,0之间。具体见code:

nxt[1]=0;
int j=0/*对于next[i],j表示next[i-1]*/,m=s2.size();
for(int i=2;i<=m;i++) {
	while(j>0 && s2[i-1]!=s2[j]/*当前候选项不符合要求*/) j=nxt[j];//查找下一候选项
	if(s2[i-1]==s2[j]) j++;//此候选项符合标准
	nxt[i]=j;
}

f f f数组的求法

由于定义相似, f f f数组求法再次不多做赘述,具体见code:

int j=0,n=s1.size();
int ans=0;
for(int i=1;i<=n;i++) {
	while(j>0 && (j==m || s1[i-1]!=s2[j])/*这一候选项不符合要求*/) j=nxt[j];
	if(s1[i-1]==s2[j]) j++;
	f[i]=j;
	if(f[i]==m) ans++;//统计答案
}

数组的额外用法

对于 n e x t next next数组而言,他还有一个很好的用法:查找该字符串的最短循环元。详情请见题目:Period以及《算法进阶》上的相关内容。

//该code是针对上文例题的,对于其他另有要求的题目,可以不用特判i/(i-nxt[i])>1
for(int i=2;i<=n;i++) {//因为周期次数要大于1,所以必须从2开始
	if(i%(i-nxt[i])==0 && i/(i-nxt[i])>1)//mark
		printf("%d %d\n",i,i/(i-nxt[i]));
}
/*
mark处解释:
由定义可知,nxt[i]表示以i为结尾的非前缀子串与前缀子串相同的最大长度,
那么i-nxt[i]的值就一定是i的所有候选项中(i-nxt[nxt[i]],i-nxt[nxt[nxt[i]]],...)最小的,
现在可证,如果i-nxt[i]是i的因数(既i可以整除(i-nxt[i])),那么i-nxt[i]也一定是i的候选项,并且一定存在最小的,长度为nxt[i]的循环元
所以如果i%(i-nxt[i])==0,那么就说明已经找到了一个循环元,现在只需判断这个循环元循环的周期是不是1(既这个循环元是不是字符串本身)
*/

最小表示法

定义

我们先来看一组字符串:

S=cdba;
S1=acdb;
S2=bacd;
S3=dbac;
S4=cdba;

不难发现,我们每次将字符串 S S S最后一个字符移到开头,这样循环多次后,就可以得到上述字符串(既 S 1 S1 S1, S 2 S2 S2, S 3 S3 S3, S 4 S4 S4)。我们称这四个字符串是循环同构的。其中,字符串 S 1 S1 S1(既 a c d b acdb acdb)是这四个字符串中字典序最小的,我们就称字符串 S 1 S1 S1是字符串 S S S最小表示。为了方便,我们通常用 B [ i ] B[i] B[i]来表示以位置i为开头的循环同构串。已上述字符串为例,则: B [ 1 ] = S 4 B[1]=S4 B[1]=S4, B [ 2 ] = S 3 B[2]=S3 B[2]=S3, B [ 3 ] = S 2 B[3]=S2 B[3]=S2, B [ 4 ] = S 1 B[4]=S1 B[4]=S1

通俗来讲,最小表示法既为:将字符串 S S S循环同构,得到 S . s i z e ( ) S.size() S.size()个字符串,其中字典序最小的一个就是 S S S最小表示

求法

了解了什么是最小表示,接下来就该求字符串的最小表示了。

一般情况下,我们最先想到的方法就是暴力了。既:求出 S S S的所有循环同构串,然后 O ( n 2 ) O(n^2) O(n2)去比较。实际上,我们可以通过双指针的思路在线性的时间内求出最小表示。

首先,我们将 S S S串复制一份补在 S S S串后面,得到字符串 S S SS SS

S=acbd;
SS=acbdacbd;

此时我们来仔细研究下 B [ i ] B[i] B[i] B [ j ] B[j] B[j]两个字符串的比较过程。
比较过程
我们不难发现: B [ i ] B[i] B[i] B [ j ] B[j] B[j]前三位字符均相等,但在比较到第四位字符时, B [ i ] B[i] B[i]要大于 B [ j ] B[j] B[j],那么,不难得到结论: B [ i ] , B [ i + 1 ] , B [ i + 2 ] … … B [ i + k ] B[i],B[i+1],B[i+2]……B[i+k] B[i],B[i+1],B[i+2]……B[i+k]都是小于 B [ j ] , B [ j + 1 ] , B [ j + 2 ] … … B [ j + k ] B[j],B[j+1],B[j+2]……B[j+k] B[j],B[j+1],B[j+2]……B[j+k]的。那么, B [ i ] , B [ i + 1 ] , B [ i + 2 ] … … B [ i + k ] B[i],B[i+1],B[i+2]……B[i+k] B[i],B[i+1],B[i+2]……B[i+k]就不可能成为最终的答案了,我们可直接让 i = i + k + 1 i=i+k+1 i=i+k+1,跳过这一段。但因为 B [ j ] B[j] B[j]还有可能成为最终的答案,所以 j j j不做变动。当然, i i i j j j反过来也一样。但是,我们要注意一点: j = i + k + 1 j=i+k+1 j=i+k+1,我们需要 i + + i++ i++。因为如果当 i = = j i==j i==j时,继续比下去就没有双指针的作用了。

最后,当 i i i j j j已经延伸了 S . s i z e ( ) S.size() S.size()个位置,或者已经大于 S . s i z e ( ) S.size() S.size()时,说明最小表示就在 B [ m i n ( i , j ) ] B[min(i,j)] B[min(i,j)]中。不理解可以查阅《算法进阶》。我才不会说是因为我懒

int i=1,j=2;
while(i<=n && j<=n) {
	int k;
	for(k=0;k<n && s1[i+k]==s1[j+k];k++) ;//注意,此处为空语句
	if(k==n) break;
	if(s1[i+k]>s1[j+k]) {
		i=i+k+1;
		if(i==j) i++;
	}
	else {
		j=j+k+1;
		if(i==j) j++;
	}
}
i=min(i,j);
for(int k=1;k<=n;k++)
	mark1[k]=s1[i+k-1];//mark数组便为最小表示

trie树

定义

trie树是一种字典树,用于实现字符串快速检索的数据结构。trie树的总体结构见下图:
trie树
当我们在插入一个字符串到trie树里时,要让连接两个点的有向边来存储字符,在最后一个节点上做标记,表示这个字符串结束。

实现

通常,我们选择用一个二维数组来表示 t r i e trie trie树,用 t o t tot tot表示当前的总结点数。同时,要有一个 e n d end end数组来标记字符串是否结束。

初始化

因为 t r i e trie trie树本身就具有一个根节点,所以 t o t tot tot的初值为1, t r i e , e n d trie,end trie,end数组初始均为0。要特别注意的一点就是trie 数组以及end数组开的范围。

int trie[maxn][26], tot = 1;//在这里的26是默认只有大写字母或只有小写字母,要根据不同要求及时变化。
bool ed[maxn];//maxn的数值为总的字符数。例如,一道题中有n个字符串,每个字符串长度不超过m,那么maxn就应该开到n * m。

其中, t r i e [ i ] [ j ] = k trie[i][j]=k trie[i][j]=k表示第 i i i个节点通过边 j j j连接到的节点为 k k k

插入

对于插入这个操作,我们可以变相地理解一下:假设这棵树上有着所有字符所对应的边,但是都未被激活。把插入字符串理解为激活字符串。 将 t o t tot tot理解为已被激活的字符个数。这时,我们的插入操作(假设插入字符串 c a t cat cat)就分为以下几步:

  1. 定义一个节点 p p p,初始时 p p p为根。将 p p p通过字符边 c c c指向节点 q q q。若 q q q已被激活,令 p = q p = q p=q,否则将 q q q激活,令 p = q p = q p=q
  2. 重复上述步骤直至字符串结束,此时将节点 p p p所在的节点标记,表明到此有一条完整的字符串。详情见代码:
void add(string x) {
	int p = 1;//新建节点p
	for(int i = 0; i < x.size(); i++) {
		if(trie[p][x[i]-'A'] == 0) trie[p][x[i]-'A'] = ++tot;//节点q未被激活,将其激活
		p = trie[p][x[i]-'A'];//令p = q
	}
	ed[p] = 1;//将节点p标记
	return ;
}
查询

同插入,查询的步骤与插入几乎一致。

  1. 新建一个节点 p p p,初始时节点 p p p为根,将 p p p通过字符边 c c c指向节点 q q q。若 q q q未被激活,返回 f a l s e false false,否则令 p = q p = q p=q
  2. 重复上述步骤直至字符串查询结束,判断 e d [ p ] ed[p] ed[p]是否被标记。若未被标记,返回 f a l s e false false,否则返回 t r u e true true。详情见代码:
bool Search(string x) {
	int p = 1;
	for(int i = 0; i < x.size(); i++) {
		int ch = x[i] - 'a';
		int k = trie[p][ch];
		if(!k) return false;//判断q是否被激活
		p = k;
	}
	if(!ed[p]) return false;//判断是否是完整的字符串
	return true;
}

01trie

t r i e trie trie树有一个十分有用的算法:01trie,即 t r i e trie trie树的每一条边存储的是二进制中的 0 0 0 1 1 1。具体怎么用呢??我们来看一道例题。
例题:最长异或路径
在这之前,我们先来想一个问题:现在给出 n n n个数,让你从中选出两个,使其异或值最大。怎么做呢??

我们可以将这 n n n个数转换为n个长度为 32 32 32的二进制 01 01 01串,然后枚举每一个 01 01 01串。对于某个 01 01 01串的某一位,我们尽可能的去找与其相反的一位。即:如果这一位是 1 1 1,那我们就去找有没有与其相对应的 0 0 0,如果有,记录进答案;否则按照原来的 1 1 1继续找。反之亦然。详情见 c o d e code code

void add(int x) {//添加01串
	int p = 1;
	for(int i = 32; i >= 1; i--) {//转换成32位01串
		int ch = (x >> (i - 1)) & 1;//取出当前位
		if(ch == 1) x = x - (1 << (i - 1));//x随之改变(这一行可有可无)
		int &k = trie[p][ch];
		if(k == 0) k = ++tot;//正常插入操作
		p = k;
	}
	ed[p] = 1;
	return ;
}
int Search(int x) {
	int p = 1, cnt = 0;
	for(int i = 32; i >= 1; i--) {
		int ch = (x >> (i - 1)) & 1;
		if(ch == 1) x = x - (1 << (i - 1));//同插入,将x转为01串
		int k = trie[p][abs(ch-1)];//找与这一位相反的,因为想要让异或值更大,要尽可能让两个串不相等
		if(k) cnt += (1 << (i - 1)), p = k;//存在,存答案
		else p = trie[p][ch];//不存在,等于原来的
	}
	return cnt;//返回答案
}

有了这个思路,我们再来回顾此题。本题让我们找到任意两点之间的最长异或路径,那么我们可以先从根节点(假设为 1 1 1)遍历整个树,求出根节点到每个点的异或和,再在所有的点中任选两个,使其异或值最大。

为什么呢??请见下图:
1到2的道路
1 1 1 r o o t root root 的路径为 A A A,从 2 2 2 r o o t root root 的路径为 B B B,从 1 1 1 2 2 2 的路径为 C C C。那么显而易见, A ⊕ B = C A \oplus B = C AB=C。我们知道, x ⊕ x = 0 x \oplus x = 0 xx=0,所以 A A A B B B 相重合的部分就会被异或掉,留下的就只有 C C C。这道题就转化为了刚才的问题。详情见 C o d e Code Code

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
struct my_str {
	int End, Nxt, Value;
};
my_str edge[maxn*2];
int head[maxn];
int n, tot = 0;
int dp[maxn];
int trie[maxn*32][2];
void add(int u,int  v,int w) {
	edge[++ tot].Value = w;
	edge[tot].Nxt = head[u];
	edge[tot].End = v;
	head[u] = tot;
}
void dfs(int x, int fa) {
	for(int i = head[x]; i != -1; i = edge[i].Nxt) {
		int To = edge[i].End;
		if(To == fa) continue;
		dp[To] = dp[x] ^ edge[i].Value;
		dfs(To, x);
	}
	return ;
}
void Insert(int x) {
	int p = 1;
	for(int i = 31; i >= 0; i--) {
		int ch =((x >> i) & 1);
		int &k = trie[p][ch];
		if(k == 0) k = ++tot;
		p = k;
	}
	return ;
}
int Search(int x) {
	int p = 1;
	int cnt = 0;
	for(int i = 31; i >= 0; i--) {
		int ch = ((x >> i) & 1);
		int k = trie[p][ch ^ 1];
		if(k) cnt += (1 << i), p = k;
		else p = trie[p][ch];
	}
	return cnt;
}
int main() {
	scanf("%d", &n);
	memset(head, -1, sizeof(head));
	for(int i = 1; i < n; i++) {
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		add(u, v, w);
		add(v, u, w);//链式前向星建边
	}
	dp[1] = 0;
	dfs(1, 0);//求根到每一个点的异或值
	tot = 1;
	for(int i = 1; i <= n; i++) Insert(dp[i]);//同上
	int ans = -1;
	for(int i = 1; i <= n; i++) ans = max(ans, Search(dp[i]));
	printf("%d", ans);
	return 0;
}

AC自动机

\qquad AC自动机,一个让人看了十分兴奋的名字(雾)。它可以看做在Trie树上做KMP,结合了Trie树的结构和KMP的思想。主要解决多模式串匹配的问题。我们通过一个实例来深入理解AC自动机原理。

\qquad 假设现在有 3 3 3 个模式串: S 1 = A B C S_1=ABC S1=ABC S 2 = B C D F S_2=BCDF S2=BCDF S 3 = C D E S_3=CDE S3=CDE,和一个文本串: T = A B C D E T=ABCDE T=ABCDE。我们首先要建造出这三个模式串的Trie树。
Trie

\qquad 然后我们在Trie树上找,首先找到了 3 3 3 号节点,匹配到了 A B C ABC ABC 这一模式串。接着呢?重回到根开始查找吗?那样效率显然太慢了,跳到 5 5 5 号节点显然可以继续向下查找。可是让我们到了 6 6 6 号节点,发现下面的字符匹配不上了。这时不得不回到根了吧?不!我们仍可以跳到 9 9 9 号节点继续向下找到 C D E CDE CDE 这一模式串。本次查找过程中,两次跳跃使得我们的查找效率大幅提升。

\qquad 我们若将这两次跳跃画成图上的指针,AC自动机就是所有跳跃指针全部画出后的状态。
AC自动机

\qquad 上图中,我们只画出了最有用的两条指针。最终的AC自动机是比这个要复杂的。我们把AC自动机上的跳跃指针称为fail指针,又叫做失配指针。顾名思义,失配指针就是我们在Trie树上查找,失配之后跳跃的指针。AC自动机最重要的步骤就是构建失配指针。对于一个节点 p p p,我们要枚举一遍字符集,看是否存在 t r i e p , c trie_{p},_{c} triep,c 这一节点。若存在,对应上图 2 2 2 号节点找到 3 3 3 号节点的情况,我们便直接将 f a i l p fail_p failp 的信息赋给 t r i e p , c trie_p,_c triep,c f a i l t r i e p , c = t r i e f a i l p , c fail_{trie_{p},_c}=trie_{fail_p},_c failtriep,c=triefailp,c。若不存在,对应上图中 6 6 6 号节点找到 9 9 9 号节点的情况,因为 t r i e p , c trie_p,_c triep,c 本身不存在,便不需要提前跳走,只需将 f a i l p fail_p failp 的信息赋给 t r i e p , c trie_p,_c triep,c,相当于一个路径压缩的过程。

\qquad 整个过程可以用 b f s bfs bfs 实现。

C o d e : \qquad Code: Code:

void Insert() {//插入模式串
	int p = 0;
	for(int i = 1; i <= sze; i ++) {
		int v = (int)(s[i] - 'a' + 1);
		if(!tr[p][v]) tr[p][v] = ++ tot;
		p = tr[p][v];
	}
	flag[p] ++;
}

void get_fail() {
	for(int i = 1; i <= 26; i ++)//把与根相连的节点存入队列中
		if(tr[0][i]) q.push(tr[0][i]), fail[tr[0][i]] = 0;
	fail[0] = 0;
	while(!q.empty()) {
		int p = q.front(); q.pop();
		for(int i = 1; i <= 26; i ++) {
			if(!tr[p][i]) tr[p][i] = tr[fail[p]][i];
			else fail[tr[p][i]] = tr[fail[p]][i], q.push(tr[p][i]);
		}
	}
}

后缀数组,后缀自动机,广义后缀自动机

\qquad here

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值