字符串

字符串哈希

哈希是什么?

  • 把一个串或者字符映射成一串数字,再通过取模的方式来使其可以被存下

字符串哈希?

  • 把字符串用数字的方式写出

具体的,我们可以通过把字符串变成一个k进制数,最后通过取余实现

P3370 【模板】字符串哈希

#include<bits/stdc++.h>
#define int long long
#define mod 111111111
using namespace std;
const int base=131;
string s;
int n;
int p[1000010];
map<int,bool>haash;
int sum[100010];
signed main(){
	cin>>n;
	int ssum=0;
	for(int i=1;i<=n;i++){
		cin>>s;
		int len=s.size();
		int ans=0;
		for(int j=0;j<len;j++){
			ans=(ans*base+(int)s[j])%mod;
		}
		if(haash[ans])
			ssum++;	
		haash[ans]=1;
	}
	cout<<n-ssum;
	
	return 0; 
} 

前缀哈希:

  • 通过前缀和加哈希值的方式,再通过前缀和加减的方式求区间哈希值
  • 有进制限制怎么来求
  • 前面的乘上长度进制
for(int i=1;i<=n;i++){
	p[i]=p[i-1]*base;
	p[i]%=mod;
	pre[i]=(pre[i-1]*base+s[i])%mod;
}

int get_hash(int l,int r){
	return ((pre[r]-pre[l-1]*p[r-l+1]%mod)%mod+mod)%mod;
}

P3501 [POI2010] ANT-Antisymmetry

  • 看到这题要求子串相同,考虑用字符串哈希
  • 子串考虑前缀哈希,因为要取反,考虑用两个数组,一个是取反前的,一个是取反后的,然后发现长度又单调性,可以二分,只有当前满足,更小的一定有。
  • 所以加上长度,是满足要求的个数
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int base=131;
string s;
int len;
ull pre[500010],suf[500100],p[500010];
void init(){
	p[0]=1;
	for(int i=1;i<=len;i++){
		p[i]=p[i-1]*base;
		pre[i]=pre[i-1]*base+(s[i]-'0'+31);
	}
	for(int i=len;i>=1;i--){
		suf[i]=suf[i+1]*base+('1'-s[i]+31);
	}
}
ull get_hash(int l,int r){
	if(l==0)	return 0;
	return pre[r]-pre[l-1]*p[r-l+1];
}
ull get_suf(int l,int r){
	if(l==0)	return 0;
	return suf[l]-suf[r+1]*p[r-l+1];
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>len;
	cin>>s;s=" "+s;
	init();
	int	ret=0;
	for(int i=1;i<len;i++){
		int l=0,r=(i,len-i),ans=0;
		while(l<=r){
			int mid=l+r>>1;
			if(get_hash(i-mid,i)==get_suf(i+1,i+1+mid)){
				l=mid+1;ans=l; 
			}
			else	r=mid-1;
		}
		ret+=ans;
	}
	cout<<ret;
	return 0;
}

P3498 [POI2010] KOR-Beads

  • 读题发现这题有子串,还需要比较是否相同,发现可以用前缀后缀哈希
  • 发现可以枚举长度,然后再枚举每一个串,再做前缀和后缀和
  • 比较是否被标记
#include<bits/stdc++.h>
#define ll long long
#define ull long long
using namespace std;
ull base=11451411;
const int N=2e5+20;
int n,a[N];
ull p[N],h1[N],answer[N],h2[N];
char s[N];
ull hash1(int r,int l){
	return (h1[r]-h1[l-1]*p[r-l+1]);
}
ull hash2(int r,int l){
	return (h2[l]-h2[r+1]*p[r-l+1]);
}
map<ull,bool>q;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	p[0]=1;
	for(int i=1;i<=n;i++){
		p[i]=p[i-1]*base;
		h1[i]=(h1[i-1]*base+a[i]);
	}
	for(int i=n;i>=1;i--){
		h2[i]=h2[i+1]*base+a[i];
	}
	int ans=0,cnt=0;
	for(int k=1;n/k>=ans;k++){
		int sum=0;
		for(int i=k;i<=n;i+=k){
			int dw1=hash1(i,i-k+1);
			int dw2=hash2(i,i-k+1);
			if(q[dw1]==0&&q[dw2]==0){
				q[dw1]=1,q[dw2]=1;
				sum++;
			}
		}
		if(ans<sum){
			ans=sum;
			cnt=0;
			answer[++cnt]=k;
		}
		else if(ans==sum){
			answer[++cnt]=k;
		}
		q.clear();
	}
	cout<<ans<<" "<<cnt<<endl;
	for(int i=1;i<=cnt;i++){
		cout<<answer[i]<<" ";
	}
	return 0;
}

相似的回忆

  • 小 D 和小 Y 是一起长大的好朋友。在他们的童年里,有许多相似的回忆。现在,小 D 和小 Y 已不再年轻,他们回想起过去的时光,希望你能帮他们找出这些相似的回忆。

我们把一个人的回忆,看做一个字符串 \(S[1…|S|]\) ,其中 \(|S|\) ,表示字符串的长度,\(S_i(1≤i≤|S|)\) 表示字符串第 \(i\) 位上的字符。

对于两段回忆 \(S[1…|S|]\) , \(T[1…|T|]\) ,我们认为它们是相似的,当且仅当满足如下两个条件:|S|=|T|。对于所有二元组 \((i,j)(1≤i≤j≤|S|)\)

  • \(S_i=S_j ,则 T_i=T_j\)
  • \(S_i≠S_j ,则 T_i≠T_j\)

    现在,小 D 把他的所有回忆 \(a[1…|a|]\) ,告诉了你,而小 Y 则给了你一个回忆片段 \(b[1…|b|]\) 。保证 \(|a|≥|b|\) 。请你求出,\(a\) 中有多少子串与 \(b\) 是相似的。

我们称一个串是另一个串的子串,当且仅当前者能通过从后者的开头、结尾各删除若干字符(可以为 \(0\) ,即不删)得到。

另外,由于小 D 和小 Y 的回忆非常丰富,不足以用有限的英文字母表示出来,所以我们用数字来表示这些串。每个数字代表串的一位。数字间用空格隔开。

输出一行一个非负整数,表示 \(a\) 中有多少子串与 \(b\) 是相似的。

  • 看到这一题最暴力的做法就是把每一个的相同的上一个找出来,看与他的位置的距离是多远,最后比较一下
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[200010],b[200010],pre1[2000010],pre2[2000100];
map<int,int>mp;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(!mp[a[i]]){
			pre1[i]=0;
			mp[a[i]]=i;
		}
		else{
			pre1[i]=mp[a[i]];
			mp[a[i]]=i;
		}
	}
	mp.clear();
	for(int i=1;i<=m;i++){
		cin>>b[i];
		if(!mp[b[i]]){
			pre2[i]=0;
			mp[b[i]]=i;
		}
		else{
			pre2[i]=mp[b[i]];
			mp[b[i]]=i;
		}
	}
	int ans=0;
	for(int i=1;i<=n-m+1;i++){
		bool flag=0;
		for(int j=i;j<=m+i-1;j++){
			if((pre1[j]-i+1)*(pre1[j]>=i)!=pre2[j-i+1]){
				flag=1;
				break;
			}
		}
		if(!flag)	++ans;
	}
	cout<<ans;
	return 0;
}
  • 考虑优化一下,发现每过一个区间,至多有一个会发生变化.
  • 数字值域大,但多少较少,考虑先离散化
  • 考虑用字符串哈希处理与上一个的位置,但是每一个都会改变,怎么办呢,可以用线段树来维护哈希值,怎么维护,维护每个区间
  • 更新即为左区间乘以右区间的长度
  • 修改就是在与下一个相同的的值修改为 \(0\)
  • 怎么查询呢?只要如果全在查询区间中,那么就直接更新。否则如果答案在左区间,更新左区间,如果在右区间,更新右区间。否则就是当前区间中左区间乘上右区间区间长度,但如果超出范围,就取范围中的
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int base=13131;
const int N=4e5+10;
int n,m;
int a[N],b[N],c[N<<1],cnt;
int nxt[N],pre1[N],pre2[N];
map<int,int>mp;
int tree[N<<2],dw[N],len[N<<2];
ull hash_b;
int ls(int p){
	return p<<1;
}
int rs(int p){
	return p<<1|1;
}
void lsh(){
	sort(c+1,c+1+cnt);
	int tot=unique(c+1,c+1+cnt)-c-1;
	dw[0]=1;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(c+1,c+1+tot,a[i])-c;
		dw[i]=dw[i-1]*base;
	}
	for(int i=1;i<=m;i++){
		b[i]=lower_bound(c+1,c+1+tot,b[i])-c;
	}
}
void get(){
	for(int i=1;i<=n;i++){
		if(mp[a[i]]){
			pre1[i]=mp[a[i]];
			nxt[pre1[i]]=i;
			mp[a[i]]=i;
		}
		else{
			mp[a[i]]=i;
		}
		nxt[i]=INT_MAX;
	}
	mp.clear();
	for(int i=1;i<=m;i++){
		if(mp[b[i]]){
			pre2[i]=mp[b[i]];
			mp[b[i]]=i;
		}
		else{
			mp[b[i]]=i;
		}
	}
}
void get_hash_b(){
	for(int i=1;i<=m;i++){
		hash_b=hash_b*base+(i-pre2[i])*(pre2[i]!=0); 
	}
}
void push_up(int p){
	tree[p]=tree[ls(p)]*dw[len[rs(p)]]+tree[rs(p)];
}
void build(int p,int l,int r){
	len[p]=r-l+1;
	if(l==r){
		tree[p]=(l-pre1[l])*(pre1[l]!=0);
		return ;
	}
	int mid=l+r>>1;
	build(ls(p),l,mid);
	build(rs(p),mid+1,r);
	push_up(p);
}
void merge(int p,int l,int r,int x,int k){
	if(l==r){
		tree[p]=k;return ;
	}
	int mid=l+r>>1;
	if(mid<x){
		merge(rs(p),mid+1,r,x,k);
	}
	else	merge(ls(p),l,mid,x,k);
	push_up(p);
}
ull query(int p,int l,int r,int x,int y){
	if(l>=x&&y>=r){
		return tree[p];
	}
	int mid=l+r>>1;
	ull ans=0;
	if(mid>=y){
		ans=query(ls(p),l,mid,x,y);
	}
	else if(mid<x){
		ans=query(rs(p),mid+1,r,x,y);
	} 
	else {
		ans=query(ls(p),l,mid,x,y)*dw[min(r,y)-mid]+query(rs(p),mid+1,r,x,y);
	}
	push_up(p);
	return ans;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];c[++cnt]=a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>b[i];c[++cnt]=b[i];
	}
	lsh();
	get();
	get_hash_b();
	build(1,1,n);
	int num=0;
	for(int i=1;i<=n-m+1;i++){
		if(query(1,1,n,	i,i+m-1)==hash_b)	++num;
		if(nxt[i]==INT_MAX)	continue;	
		merge(1ll,1ll,n,nxt[i],0ll);
	
	}
	cout<<num;
	return 0;
} 

H - String

  • 读题目发现子串匹配问题,又是套路哈希
  • 每次先枚举起点,每次都向后跳,先跳完规定的 \(M\) 个串,然后再一个一个向后跳,每一次给当前串新加的一段的哈希值加一,再给去除掉的最开头的减去一
  • 最后统计答案
  • 复杂度是外面枚举起点 \(\operatorname{O}(L)\),然后里面枚举每个串 \(\operatorname{O}(|S|-L)\),再加上 \(map\)\(\operatorname{O}(log2(l))\),所以是\(\operatorname{O}(L*|S|/L*log2(l))\)
    \(\operatorname{O}(S*log2(l))\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int base=131;
int l,m;
string s;
unsigned int Hash[200010];
unsigned int p[200010];
unsigned int get_hash(int l,int r){
	return Hash[r]-Hash[l-1]*p[r-l+1];
}
int c[200010];
signed main(){
	while(cin>>m>>l){
		cin>>s;
		s=" "+s;
		int len=s.size()-1;
		p[0]=1;
		int k=len/l;
		for(int i=1;i<=len;i++){
			p[i]=(p[i-1]*base);
			Hash[i]=(Hash[i-1]*base+(int)s[i]);
		}
		int cnt=0;
		int ans=0;
		for(int i=1;i<=len-l*m+1&&i<=l;i++){
			map<unsigned int,int>mp;
			for(int j=i;j<=i+l*m-1;j+=l){
				mp[get_hash(j,j+l-1)]++;
			}
			if(mp.size()==m)	ans++;
			for(int j=i+m*l;j+l-1<=len;j+=l){
				mp[get_hash(j,j+l-1)]++;
				unsigned int x=get_hash(j-l*m,j-l*m+l-1);
				mp[x]--;
				if(mp[x]==0)	mp.erase(x);
				if(mp.size()==m)	ans++;
			}
		}
		cout<<ans<<'\n';
	}

	return 0;
}

KMP

  • 是一种字符串匹配的算法,是一种比较难的字符串算法
  • 具体实现是通过记录一个字符串的最长公共前缀后缀来跳跃掉不必要的匹配,从而优化

怎么求最长前缀后缀( \(border\)

先举个栗子:\(acaacaca\) 观察一下这一个字符串的 \(boeder\) 是什么呢?

  • 没错,就是 \(aca\) 我们最朴素的方法可以把每一个前缀和每一个后缀进行比较
  • 可这样时间反而变高,还不如不处理,考虑优化一下,发现 \(border\) 是有单调性的,就比如 \(acaacaca\) 这个串中,\(aca\) 的 $border a $,也是 \(acaacaca\)\(border\) 由此可以发现当一个串不满足当前长度时,可以跳到他的前缀继续找。
  • 为什么可以这样跳呢?因为我们发现前面的都是匹配过的,而向前跳一次相当于更新一次,如果跳的不是 \(border\) 的话,不能保证它跳完之后它的匹配过的位置还是一样的
  • 所以要跳的它的 \(border\)

所以要通过跳它的 \(border\) 来加速匹配 ,可以通过当失配时,把匹配的长度变为 \(border\) 这个很好理解,就不多说了,

#include<bits/stdc++.h>
#define int long long
using namespace std;
string s1,s2;
int Next[1000100];
int len1,len2;
void get_nxt(){
	Next[1]=0;
	int k=0;
	for(int i=2;i<=len2;i++){
		while(k&&s2[k+1]!=s2[i])	k=Next[k];
		if(s2[k+1]==s2[i])	k++;
		Next[i]=k;
	}
}
void get_p(){
	int k=0;
	for(int i=1;i<=len1;i++){
		while(k&&s1[i]!=s2[k+1]){
			k=Next[k];
		}
		if(s2[k+1]==s1[i])	k++;
		if(k==len2){
			cout<<i-k+1<<'\n';
			k=Next[k];
		}
	}
}
signed main(){
	ios::sync_with_stdio(false); 
	cin>>s1>>s2;
	s1=" "+s1;
	s2=" "+s2;
	len1=s1.size()-1;
	len2=s2.size()-1;
	get_nxt();
	get_p();
	return 0;
} 

P4391 [BOI2009] Radio Transmission 无线传输

`

因为红色的和 \(1\) 区间相同,所以相同,\(1\)\(2\) 相同因为是 \(border\) 中,同等的,可以推出 答案应该是 红色的长度 \(n-next_n\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
string s1,s2;
int Next[1000100];
int len1,len2;
void get_nxt(){
	Next[1]=0;
	int k=0;
	for(int i=2;i<=len2;i++){
		while(k&&s2[k+1]!=s2[i])	k=Next[k];
		if(s2[k+1]==s2[i])	k++;
		Next[i]=k;
	}
}
signed main(){
	ios::sync_with_stdio(false); 
	int a;
	cin>>a;	cin>>s2;s2=" "+s2;
	len2=s2.size()-1;
	get_nxt();
	cout<<Next[len2]<<" ";
	cout<<len2-Next[len2];
	return 0;
} 

P3435 [POI2006] OKR-Periods of Words

  • 看题目可以转化一下,就是这个前缀的最小前缀,为什么可以这样呢?
  • 因为我们发现最小前缀后缀相同的话后面可以直接拼起来。
  • 所以这就变成KMP的模版
#include<bits/stdc++.h>
#define int long long
using namespace std;
string s1,s2;
int Next[1000100];
int len1,len2;
void get_nxt(){
	Next[1]=0;
	int k=0;
	for(int i=2;i<=len2;i++){
		while(k&&s2[k+1]!=s2[i])	k=Next[k];
		if(s2[k+1]==s2[i])	k++;
		Next[i]=k;
	}
}
signed main(){
	ios::sync_with_stdio(false); 
	int a;
	cin>>a;	cin>>s2;s2=" "+s2;
	len2=s2.size()-1;
	get_nxt();
	int ans=0;
	for(int i=2;i<=a;i++){
		int k=i;
		while(Next[k])	k=Next[k]; 
		if(k!=i) Next[i]=k;
		ans+=(i-k);
	}
	cout<<ans;
	return 0;
} 

再放一个代码,不能A,但可以参考一下这种思想

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
const int base=13131;
int len;
string s; 
ull p[2000010],hsh[2000010];
ull get_hash(int l,int r){
	return hsh[r]-hsh[l-1]*p[r-l+1];
}
signed main(){
	ios::sync_with_stdio(false); 
	cin>>len;
	cin>>s;s=" "+s;
	p[0]=1;
	for(int i=1;i<=len;i++)	p[i]=p[i-1]*base;
	for(int i=1;i<=len;i++){
		hsh[i]=hsh[i-1]*base+s[i];
	}
	int num=0;
	for(int i=1;i<=len;i++){
		int l=(i+1)/2+1,r=i;	
		int ans=0;
		for(int j=r;j>=l;j--){
			if(get_hash(1,r-j+1)==get_hash(j,r)){
				ans=j;break;
			}
		}
		if(!ans)	continue;
		num+=ans-1;
	}
	cout<<num;
	return 0;
} 

【模板】失配树

  • 看这个问题是求 两个字符串的最长公共 \(border\) ,根据 \(KMP\) ,我们发现可以求出每一个前缀的的最长 \(border\)
  • 公共让我们想到了什么?没错最近公共祖先
  • 为什么一定是呢?因为可以把每一个 \(kmp\) 数组记录一下,分别连向 \(border\) ,然后发现越往上越短,所以最长的一定是最近的
#include<bits/stdc++.h>
#define int long long
using namespace std;
int nxt[2000010],n;
string s;
int p,q;
int dp[1000010][30];
int dep[1000100];
int lg[1000010];
struct node{
	int to,nxt;
}a[2000010];
int head[10000100],tot;
void add(int x,int y){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	head[x]=tot;
}
void dfs(int now,int fa){
	dep[now]=dep[fa]+1;
	dp[now][0]=fa;
	for(int i=1;i<=lg[dep[now]-1];i++){
		dp[now][i]=dp[dp[now][i-1]][i-1];
	}
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		dfs(to,now);
	}
}
int lca(int x,int y){
	if(dep[x]<=dep[y]){
		swap(x,y);
	}
	while(dep[x]>dep[y]){
		x=dp[x][lg[dep[x]-dep[y]]];
	}
	if(x==y)	return dp[x][0];
	for(int i=lg[dep[x]-1];i>=0;i--){
		if(dp[x][i]!=dp[y][i]){
			x=dp[x][i],y=dp[y][i];
		}
	}
	return dp[x][0];
}
int len;
void get_nxt(){
	int k=0;
	add(0,1);
	for(int i=2;i<=len;i++){
		while(k&&s[i]!=s[k+1])	k=nxt[k];
		if(s[i]==s[k+1])	k++;
		nxt[i]=k;add(k,i);
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>s;
	cin>>n;
	s=" "+s;
	len=s.size()-1;
	get_nxt();
	lg[0]=-1;
	for(int i=1;i<=len;i++){
		lg[i]=lg[i>>1]+1;
	}
	lg[0]=0;
	dfs(0,-1);
	for(int i=1;i<=n;i++){
		cin>>p>>q;
		cout<<lca(p,q)<<'\n';
	}
	return 0;
}

剪花布条

  • 发现这就是裸的字符串匹配题,直接套个模板
  • 但注意这里还需要每次重新的一个开始匹配
#include<bits/stdc++.h>
#define int long long
using namespace std;
string s1,s2;
int Next[1000100];
int len1,len2;
void get_nxt(){
	Next[1]=0;
	int k=0;
	for(int i=2;i<=len2;i++){
		while(k&&s2[k+1]!=s2[i])	k=Next[k];
		if(s2[k+1]==s2[i])	k++;
		Next[i]=k;
	}
}
void get_p(){
	int k=0;
	int ans=0;
	bool flag=0;
	for(int i=1;i<=len1;i++){
		while(k&&s1[i]!=s2[k+1]){
			k=Next[k];
		}
		if(s2[k+1]==s1[i])	k++;
		if(k==len2){
			ans++;
			k=0;
		}
	}
	cout<<ans<<'\n';
}
int t;
signed main(){
	ios::sync_with_stdio(false); 
	
	while(1){
		memset(Next,0,sizeof(Next));
		cin>>s1;
		if(s1=="#")	break;
		cin>>s2;
		s1=" "+s1;
		s2=" "+s2;
		len1=s1.size()-1;
		len2=s2.size()-1;
		get_nxt();
		get_p();
	}

	return 0;
} 
  • 这题还可以用 \(hash\) 来做

Count the string

  • 发现一下,这个题目要求的就是每个前缀的 \(border\) 有多少个,为什么?
  • 因为这些是没有计算过的,因为是新的前缀,记住还要加上自己的1
#include<bits/stdc++.h>
using namespace std;
const int mod=1e4+7;
int T,n;
string s;
int nxt[2000010];
void get_nxt(){
	int k=0;
	nxt[1]=0;
	for(int i=2;i<=n;i++){
		while(s[i]!=s[k+1]&&k)	k=nxt[k];
		if(s[i]==s[k+1])	++k;
		nxt[i]=k;
	}
}
int ans=0;
void solve(){
	ans=n;	
	for(int i=2;i<=n;i++){
		int k=i;
		while(nxt[k]){
			++ans;k=nxt[k];
		}
		ans%=mod;
	}
	cout<<ans<<'\n';
}	
signed main(){
	cin>>T;
	while(T--){
		cin>>n;
		cin>>s;
		s=" "+s;
		get_nxt();
		solve();
	}
	return 0;
}
  • 这份代码可以A,但是有没有什么更好的办法更快呢,答案是存在的。
  • 发现每次每个前缀跳的多少都是固定的,所以递推来求
for(int i=1;i<=len;i++){
	dp[i]=dp[Next[i]]+1;
	ans=(ans+dp[i])%mod;
}

字典树

  • 字典树 (Trie),又称单词查找树、前缀树,是一种树形结构,是一种哈希树的变种。在统计、排序和保存大量的字符串(但不仅限于字符串)是具有更小的时间复杂度,因此可以应用于搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
  • 但缺点就是空间复杂度较高
接下来放一张生动的讲一下

  • 观察这张图片,我们发现它是把每一个字符串的每一个字母记录到一棵字典树上的每个结点,从而更加方便的查询前缀后缀、
字典树的性质:
  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
  3. 每个节点的所有子节点上的字符都不相同。
  • 一般会使用一个二维数组 \(tire[rt][to]\) 表示 \(rt\) 这个节点的 对to这个字符连边后的节点是什么
  • \(flag[rt]\) 表示是否是结尾,\(ans[rt]\) 表示是几个匹配串的前缀
  • 修改中可以通过从根节点开始向下跳,如果有的话就直接跳,否则新加入一个节点,再把这个前缀标记上
  • 查询时可以通过要使有就跳,否则就直接返回 \(0\) ,最后返回后缀数量
#include<bits/stdc++.h>
using namespace std;
int t,n,q;
string s;
int get_asc(char c){
	if('0'<=c&&c<='9')	return c-'0'+52;
	if('a'<=c&&c<='z')	return c-'a'+26;
	if('A'<=c&&c<='Z')	return c-'A'; 
}
int cnt=0;
int ans[3000100];
int tire[3000100][70];
void merge(string s){
	int len=s.size();
	int rt=0;
	for(int i=0;i<len;i++){
		int x=get_asc(s[i]);
		if(!tire[rt][x]) tire[rt][x]=++cnt;
		rt=tire[rt][x];
		ans[rt]++;
	}
}
int query(string s){
	int len=s.size();
	int rt=0;
	for(int i=0;i<len;i++){
		int x=get_asc(s[i]);
		if(!tire[rt][x]) return 0;
		rt=tire[rt][x];
	}
	return ans[rt];
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>t;
	while(t--){
		cnt=0;
		cin>>n>>q;
		for(int i=1;i<=n;i++){
			cin>>s;
			merge(s);
		}
		while(q--){
			cin>>s;
			cout<<query(s)<<'\n';
		}
		for(int i=0;i<=cnt;i++){
			for(int j=0;j<=70;j++){
				tire[i][j]=0;
			}
		}
		for(int i=0;i<=cnt;i++)	ans[i]=0;
	}
	return 0;
}

[USACO08DEC] Secret Message G

  • 板子题,记录一下每一个结尾,每一个前缀即可
#include<bits/stdc++.h>
#define int long long 
using namespace std;
int m,n;
int tire[2000100][2];
int flag[2000010];
int ans[2000010];
int siz;
int cnt=1;
void merge(string s){
	int rt=1;
	for(int i=0;i<s.size();i++){
		int to=s[i]-'0';
		if(!tire[rt][to])	tire[rt][to]=++cnt;
		rt=tire[rt][to];
		++ans[rt];
	}
	flag[rt]++;
	--ans[rt];
}
int query(string s){
	int rt=1,ret=0;
	for(int i=0;i<s.size();i++){
		int to=s[i]-'0';
		if(!tire[rt][to])	return ret;
		rt=tire[rt][to];
		ret+=flag[rt];
	} 
	return ans[rt]+ret;
}
signed main(){
	cin>>m>>n;
	for(int i=1;i<=m;i++){
		string s="";
		cin>>siz;
		for(int j=1;j<=siz;j++)	{
			char c;cin>>c;
			s+=c;
		}	
		merge(s);
	}
	for(int i=1;i<=n;i++){
		cin>>siz;
		string s="";
		while(siz--){
			char c;
			cin>>c;
			s+=c;
		}
		cout<<query(s)<<"\n";
	}
	return 0;
} 

[USACO12DEC] First! G

  • 发现如果要最大一定不能有字符串是他的前缀,也一定要在同等级不同字母的比较中最大,至于其他的比较是我们不关心的
  • 所以不能出现环,考虑拓扑排序找环
#include<bits/stdc++.h>
#define int long long
using namespace std;
char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
	return x*f;
}
inline void print(int x){
	if(x<0)	putchar('-'),x=-x;
	if(x>9)	print(x/10);
	putchar(x%10+'0');	
}
struct node{
	int to,nxt;
}a[1010];
int tot,head[40];
void add(int x,int y){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	head[x]=tot;
}
int n;
string s[30010];
int tire[300010][30],cnt;
bool End[300010],flag[31][31];
void merge(string s){
	int rt=0;
	for(int i=0;i<s.size();i++){
		int x=s[i]-'a'+1;
		if(!tire[rt][x]){
			tire[rt][x]=++cnt; 
		} 
		rt=tire[rt][x];
	}
	End[rt]=1;
}
int ind[40];
bool query(string s){
	int rt=0;
	for(int i=0;i<s.size();i++){
		int x=s[i]-'a'+1;
	//	cout<<rt<<' ';
		if(End[rt]){
			return false;
		}
		for(int k=1;k<=26;k++){
			if(flag[x][k]||(k==x)||(!tire[rt][k]))	continue;
			flag[x][k]=1;
			ind[k]++;
			add(x,k);
//			if(op==2)
//			cout<<rt<<" "<<x<<" "<<k<<'\n';
		}
		rt=tire[rt][x];
	}
//	cout<<'\n';
	return true;
}
bool f;
bool topo(){
	queue<int>q;
	for(int i=1;i<=26;i++){
		if(!ind[i])	q.push(i); 
	}
	while(q.size()){
		int fr=q.front();q.pop();
		for(int i=head[fr];i;i=a[i].nxt){
			int to=a[i].to;
			--ind[to];
			if(!ind[to])	q.push(to);
		}
	}
	for(int i=1;i<=26;i++){
		if(ind[i])	return false;
	}
	return true;
}
void init(){
	memset(head,0,sizeof(head));
	memset(ind,0,sizeof(ind));
	memset(flag,0,sizeof(flag));
	memset(a,0,sizeof(a));
	tot=0;
}
vector<string>st;
signed main(){
//	fropen(".in","r",stdin);
//	freopen(".out","w",stdout); 
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>s[i];
		merge(s[i]);
	}
	for(int i=1;i<=n;i++){
		init();	
		if(!query(s[i],i)){
		//	cout<<i<<' '<<s[i]<<'\n';
			continue;
		}	
		if(topo()){
			st.push_back(s[i]);
		}
	//	cout<<i<<' ';
		
	}
	cout<<st.size()<<'\n';
	for(int i=0;i<st.size();i++){
		cout<<st[i]<<'\n';
	}
	return 0;
} 

P10471 最大异或对 The XOR Largest Pair

  • 读题我们可以打出一个 \(\operatorname{O}(n^2)\)
  • 但是我们发现这是远远不够的,观察异或的性质,发现前面要是有不同的一定要尽量选上,因为前面的位数是最大的
  • 相同前缀,很容易想到字典树,可以给每个数的二进制放上树中,最后把每个串放进去匹配。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int a[3000010];
int tire[3000010][3],cnt;
void merge(int k){
	int rt=0;
	for(int i=31;i>=0;i--){
		int x=(k>>i)&1;
		if(!tire[rt][x]){
			tire[rt][x]=++cnt; 
		} 
		rt=tire[rt][x];
	}
}
int num;
int query(int x){
	int rt=0;
	int ans=0;
	for(int i=31;i>=0;i--){
		int k=(x>>i)&1;
		if(tire[rt][k^1])	{
			ans=ans*2+1;rt=tire[rt][k^1];
		}
		else{
			ans*=2;rt=tire[rt][k];
		}	
	}
	return ans;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		merge(a[i]);
	}
	for(int i=1;i<=n;i++){
		num=max(num,query(a[i]));
	}
	cout<<num;
	return 0;
} 

最长异或路径

  • 读题发现和上一题相似,而我们可以朴素的打出一个暴力,就是求出每个点到根的异或和,\(\operatorname{O}(n^2)\)
  • 发现可以字典树优化一下,一样可以放入字典树,最后向上一题一样与每一个比对一下
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int t[3000010];
int tire[3000010][3],cnt;
struct node{
	int to,nxt,w; 
}a[2000010];
void merge(int k){
	int rt=0; 
	for(int i=31;i>=0;i--){
		int x=(k>>i)&1;
		if(!tire[rt][x]){
			tire[rt][x]=++cnt; 
		} 
		rt=tire[rt][x];
	}
}
int num;
int query(int x){
	int rt=0;
	int ans=0;
	for(int i=31;i>=0;i--){
		int k=(x>>i)&1;
		if(tire[rt][k^1]){
			ans=ans*2+1;rt=tire[rt][k^1];
		}
		else{
			ans*=2;rt=tire[rt][k];
		}	
	}
	return ans;
}
int head[2000100],tot;
void add(int x,int y,int w){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	a[tot].w=w;
	head[x]=tot;
}
void dfs(int now,int fa){
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;
		t[to]=t[now]^a[i].w;
		dfs(to,now);
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v,c;
		cin>>u>>v>>c;
		add(u,v,c);
		add(v,u,c);
	}
	dfs(1,-1);
	for(int i=1;i<=n;i++){
		merge(t[i]);
	}
	for(int i=1;i<=n;i++){
		num=max(num,query(t[i]));
	}
	cout<<num;
	return 0;
} 
posted @ 2024-09-18 09:46  lmy333  阅读(7)  评论(0编辑  收藏  举报