字符串指南 QwQ

KMP

KMP 是模式串匹配的算法,本来最坏时间复杂度可以达到 O ⁡ ( n × m ) \operatorname{O}(n\times m) O(n×m),但是 kmp 可以将复杂度优化到 O ⁡ ( n + m ) \operatorname{O}(n+m) O(n+m)

KMP 的思想是利用之前匹配过的信息进行下一轮的匹配。

首先,要利用匹配串 t t t 的信息来进行匹配。比如, t = "abcabc" t=\texttt{"abcabc"} t="abcabc" s = abcabdc s=\texttt{abcabdc} s=abcabdc,那么发现第一个字符 ’a’ \texttt{'a'} ’a’ 只能够在 1 1 1 4 4 4 匹配进去。发现这一个匹配模式跟本身的字符串有关,那么失配后只需要跳至下一个重复单元即可。

p r e ( n ) pre(n) pre(n) 表示 s s s 中的前缀 [ 1 , n ] [1,n] [1,n] s u f ( n ) suf(n) suf(n) 表示 s s s 中的后缀 [ ∣ s ∣ − n + 1 , ∣ s ∣ ] [|s|-n+1,|s|] [sn+1,s],那么 KMP 就是利用 n x t nxt nxt 维护这一个最大的 n n n s s s 的子串 [ 1 , m ] [1,m] [1,m] 下的答案。即 n x t m = max ⁡ n = 1 ⌊ ∣ s ∣ 2 ⌋ ( n [ p r e ( 1 , n ) = s u f ( n ) ] ) nxt_m=\max_{n=1}^{\lfloor\frac{|s|}{2}\rfloor}(n[pre(1,n)=suf(n)]) nxtm=maxn=12s(n[pre(1,n)=suf(n)])

发现如果这一个字符是匹配的,那么可以从上一个匹配的 n x t nxt nxt 继承过来,即 n x t i = n x t j + 1 nxt_i=nxt_j+1 nxti=nxtj+1,但是难点在于确定这一个 j j j

考虑失配的情况,这也是从原来推过来的!于是,我们可以不断地跳 n x t nxt nxt 来找到这个位置。于是, n x t nxt nxt 数组就得出来了。得出了 n x t nxt nxt,那么也可以进行 O ⁡ ( n + m ) \operatorname{O}(n+m) O(n+m) 的匹配了。

int len1=strlen(s1+1);
int len2=strlen(s2+1);
int pos=0;
for(int i=2;i<=len2;++i){
	while(pos&&s2[i]!=s2[pos+1]){
		pos=nxt[pos];//跳 nxt 
	}
	if(s2[i]==s2[pos+1]){
		++pos;//匹配成功 
	}
	nxt[i]=pos;//存入 nxt 
}
pos=0;
for(int i=1;i<=len1;++i){
	while(pos&&s1[i]!=s2[pos+1]){
		pos=nxt[pos];//跳 nxt 
	}
	if(s1[i]==s2[pos+1]){
		++pos;//匹配成功 
	}
	if(pos==len2){
		printf("%d %d\n",i,i+len2-1);//一个位置 
	}
}

例题

我们可以发现,在匹配成功的时候才需要删除,而且只能够从尾巴删除。于是考虑栈维护答案。如果没有匹配到,那么就把这个元素入栈。如果匹配到了,那么栈内后 m m m 个字符就组成了一个待删除的字符串,将栈内后 m m m 个字符出栈即可。

#include<bits/stdc++.h>
#define MAXN 1000001
using namespace std;
char s1[MAXN],s2[MAXN];
int nxt1[MAXN],nxt2[MAXN],stk[MAXN];
int main(){
	scanf("%s %s",s1+1,s2+1);
	int len1=strlen(s1+1);
	int len2=strlen(s2+1);
	int pos=0,top=0;
	for(int i=2;i<=len2;++i){
		while(pos&&s2[i]!=s2[pos+1]){
			pos=nxt1[pos];
		}
		if(s2[i]==s2[pos+1]){
			++pos;
		}
		nxt1[i]=pos;
	}
	pos=0;
	for(int i=1;i<=len1;++i){
		while(pos&&s1[i]!=s2[pos+1]){
			pos=nxt1[pos];
		}
		if(s1[i]==s2[pos+1]){
			++pos;
		}
		nxt2[i]=pos;
		stk[++top]=i;
		if(pos==len2){
			top-=len2;
			pos=nxt2[stk[top]];
		}
	}
	for(int i=1;i<=top;++i){
		putchar(s1[stk[i]]);
	}
	return 0;
}

字符串哈希

字符串哈希的思想是,通过把字符串看做 k k k 进制数来进行存储和比较。

  • 优点:相较于直接字符比较,更加迅速,而且能够 O ⁡ ( 1 ) \operatorname{O}(1) O(1) 查询某段子区间的哈希值。
  • 缺点:容易冲突。

为了应对冲突,我们需要对哈希进制做一些优化,模数也需要非常极品。下面,给出预处理模版代码。

typedef unsigned long long ull;
ull base[MAXN],pre[MAXN],suf[MAXN];//进制、前缀哈希、后缀哈希
int len;
char s[MAXN];
inline ull get_pre(int l,int r){//O(1) 查询子区间哈希值 
	return ((pre[r]-pre[l-1]*base[r-l+1])%MOD+MOD)%MOD;
} 
inline ull get_suf(int l,int r){//O(1) 查询子区间哈希值 
	return ((suf[l]-suf[r+1]*base[r-l+1])%MOD+MOD)%MOD;
}
inline void prework(){
	base[0]=1;
	for(int i=1;i<MAXN;++i){
		base[i]=(base[i-1]*HASH)%MOD;
	}
	for(int i=1;i<=len;++i){
		pre[i]=(pre[i-1]*HASH+s[i]+DIF)%MOD;//加上偏移量防卡 
	}
	for(int i=len;i>=1;--i){
		suf[i]=(suf[i+1]*HASH+s[i]+DIF)%MOD;//加上偏移量防卡 
	}
}

有时候,可以采取双模哈希来进行防卡,这样被卡的几率很小。

typedef unsigned long long ull;
ull base1[MAXN],pre1[MAXN],suf1[MAXN];
ull base2[MAXN],pre2[MAXN],suf2[MAXN];//进制、前缀哈希、后缀哈希
int len;
char s[MAXN];
inline ull get_pre1(int l,int r){
	return ((pre1[r]-pre1[l-1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
} 
inline ull get_suf1(int l,int r){
	return ((suf1[l]-suf1[r+1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_pre2(int l,int r){//O(1) 查询子区间哈希值 
	return ((pre2[r]-pre2[l-1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
} 
inline ull get_suf2(int l,int r){
	return ((suf2[l]-suf2[r+1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
inline void prework(){
	base1[0]=base2[0]=1;
	for(int i=1;i<MAXN;++i){
		base1[i]=(base1[i-1]*HASH1)%MOD1;
		base2[i]=(base2[i-1]*HASH2)%MOD2;
	}
	for(int i=1;i<=len;++i){
		pre1[i]=(pre1[i-1]*HASH1+s[i]+DIF1)%MOD1;
		pre2[i]=(pre2[i-1]*HASH1+s[i]+DIF2)%MOD2;//加上偏移量防卡 
	}
	for(int i=len;i>=1;--i){
		suf1[i]=(suf1[i+1]*HASH1+s[i]+DIF1)%MOD1;
		suf2[i]=(suf2[i+1]*HASH2+s[i]+DIF2)%MOD2;//加上偏移量防卡 
	}
}

例题

这一道题目可以先把哈希值处理出来,然后发现可以枚举中间点,然后向左右二分。由于向左延伸 n n n 格是回文串,那么 n − 1 n-1 n1 格肯定也是回文串。所以满足单调性可以二分。由于本题有 Hack 数据卡自然溢,所以要打双模。

#include<bits/stdc++.h>
#define MAXN 500005
#define HASH1 31
#define HASH2 29
#define MOD1 193910017
#define MOD2 1000000009
#define ADD 131 
using namespace std;
typedef long long ull; 
int len;
char s[MAXN];
ull base1[MAXN],pre1[MAXN],suf1[MAXN];
ull base2[MAXN],pre2[MAXN],suf2[MAXN];
inline ull get_pre1(int l,int r){
	if(!l){
		return 0;
	}
	return ((pre1[r]-pre1[l-1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_suf1(int l,int r){
	if(!l){
		return 0;
	}
	return ((suf1[l]-suf1[r+1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_pre2(int l,int r){
	if(!l){
		return 0;
	}
	return ((pre2[r]-pre2[l-1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
inline ull get_suf2(int l,int r){
	if(!l){
		return 0;
	}
	return ((suf2[l]-suf2[r+1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
int main(){
	scanf("%d %s",&len,s+1);
	base1[0]=base2[0]=1;
	for(int i=1;i<=len;++i){
		base1[i]=(base1[i-1]*HASH1)%MOD1;
		base2[i]=(base2[i-1]*HASH2)%MOD2;
		pre1[i]=(pre1[i-1]*HASH1+(s[i]-'0'+ADD))%MOD1;
		pre2[i]=(pre2[i-1]*HASH2+(s[i]-'0'+ADD))%MOD2;
	}
	for(int i=len;i>=1;--i){
		suf1[i]=(suf1[i+1]*HASH1+('1'-s[i]+ADD))%MOD1;
		suf2[i]=(suf2[i+1]*HASH2+('1'-s[i]+ADD))%MOD2;
	}
	ull ans=0;
	for(int i=1;i<len;++i){
		int l=0,r=min(i,len-i),res=0;
		if(s[i]==s[i+1]){
			continue;
		}
		while(l<=r){ 
			int mid=(l+r)>>1;
			if(get_pre1(i-mid,i)==get_suf1(i+1,i+1+mid)&&get_pre2(i-mid,i)==get_suf2(i+1,i+1+mid)){
				l=mid+1;
				res=l;
			}else{
				r=mid-1;
			}
		}
		ans+=res;
	}
	printf("%llu",ans);
	return 0;
}

字典树

字典树是一种 k k k 叉树结构。原理是每一个节点都有一个布尔值 f l a g flag flag,判断是否是一个字符串的结尾。每一条边都有一个字符,表示前面的字符拼接起来就是字符串。这种数据结构能够 O ⁡ ( n ) \operatorname{O}(n) O(n) 查找前缀。

比如,加入一个字符串到字典树里面,那就对这个字符串进行建边。枚举每一个字符作为边,比如 s = "abccc" s=\texttt{"abccc"} s="abccc",字典树里面已有 t 1 = "avcdv" , t 2 = "abcdh" t_1=\texttt{"avcdv"},t_2=\texttt{"abcdh"} t1="avcdv",t2="abcdh",那么第一个字符 ’a’ \texttt{'a'} ’a’ 已经有 t 1 , 1 = ’a’ t_{1,1}=\texttt{'a'} t1,1=’a’,那么直接跳。第二个字符有 t 2 , 2 = ’b’ t_{2,2}=\texttt{'b'} t2,2=’b’,第三个字符有 t 2 , 3 = ’c’ t_{2,3}=\texttt{'c'} t2,3=’c’,第四个字符没有,所以新建一条边,之后的边都需要重建。之后,在第五个字符后面的点标记一个 f l a g flag flag 表示这里有字符串作为结尾。

查找一个字符串也比较简单。比如当前的字典树里面有以下字符串:

  • t 1 = "abchd" t_1=\texttt{"abchd"} t1="abchd"
  • t 2 = "aabch" t_2=\texttt{"aabch"} t2="aabch"
  • t 3 = "bhcdx" t_3=\texttt{"bhcdx"} t3="bhcdx"
  • t 4 = "bhcdxe" t_4=\texttt{"bhcdxe"} t4="bhcdxe"

查找 s = "bhcd" s=\texttt{"bhcd"} s="bhcd",发现所有边都有,但是没有字符串标记,返回有字符串包含该前缀但是没有这个字符串。

查找 s = "abchd" s=\texttt{"abchd"} s="abchd",发现有 t 1 = s t_1=s t1=s,返回有这个字符串。

查找 s = aabcd s=\texttt{aabcd} s=aabcd,发现在 s 5 = ’d’ s_5=\texttt{'d'} s5=’d’ 没有边了,返回没有这个字符串。

int cnt,trie[MAXN][MAXT],flag[MAXN];
inline int turn(char c);//字符映射函数 
inline void insert(string s){
	int root=0;
	for(int i=0;i<s.size();++i){
		int p=turn(s[i]);
		if(trie[root][p]){//有没有节点创建过 
			root=trie[root][p];//有就跳 
		}else{
			root=trie[root][p]=++cnt;//没有就建边 
		}
	}
	flag[root]=true;//标记末尾 
}
inline bool find(string s){
	int root=0;
	for(int i=0;i<s.size();++i){
		int p=turn(s[i]);
		if(!trie[root][p]){//如果没有,直接返回 
			return false;
		} 
		root=trie[root][p];//跳 
	}
	return flag[root];//有没有这个末尾 
}

例题

这一道题目考虑贪心证明。如果在某一层, u u u 需要比 v v v 先,那么就建一条边。如果在下一层,出现了需要 v v v u u u 先的情况,那就冲突了,不可能是最优。也就是判环或者用种类并查集。用 Topu 或者 Tarjan 都可以判环。

#include<bits/stdc++.h>
#define MAXN 30003
#define MAXM 26
#define MAXK MAXN*10
using namespace std;
struct node{
	int nxt[MAXM];
	bool end;
}tree[MAXK];
vector<string> ans;
int n,cnt,indeg[MAXM];
bool vis[MAXM][MAXM];
string s[MAXN];
inline void insert(string str){
	int root=0;
	for(int i=0;i<str.size();++i){
		int dot=str[i]-'a';
		if(!tree[root].nxt[dot]){
			tree[root].nxt[dot]=++cnt;
		}
		root=tree[root].nxt[dot];
	}
	tree[root].end=true;
}
inline bool addedge(string str){
	int root=0;
	for(int i=0;i<str.size();++i){
		int dot=str[i]-'a';
		if(tree[root].end){
			return false;
		}
		for(int j=0;j<MAXM;++j){
			if(tree[root].nxt[j]&&dot!=j&&!vis[dot][j]){
				++indeg[j];
				vis[dot][j]=true;
			}
		}
		root=tree[root].nxt[dot];
	}
	return true;
}
inline bool topusort(){
	queue<int> q;
	for(int i=0;i<MAXM;++i){
		if(!indeg[i]){
			q.push(i);
		}
	}
	while(!q.empty()){
		int front=q.front();
		q.pop();
		for(int i=0;i<MAXM;++i){
			if(vis[front][i]){
				--indeg[i];
				if(!indeg[i]){
					q.push(i);
				}
			}
		}
	}
	for(int i=0;i<MAXM;++i){
		if(indeg[i]){
			return false;
		}
	}
	return true;
}
int main(){
	ios::sync_with_stdio(false);
	int n;
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>s[i];
		insert(s[i]);
	}
	for(int i=1;i<=n;++i){
		memset(indeg,0,sizeof(indeg));
		memset(vis,0,sizeof(vis));
		if(addedge(s[i])&&topusort()){
			ans.push_back(s[i]);
		}
	}
	cout<<ans.size();
	for(int i=0;i<ans.size();++i){
		cout<<endl<<ans[i];
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值