字符串做题记录

CF25E Test&SP7155

双倍经验针不戳

一个KMP的应用。首先可以发现珂以把三个串拼接起来来达到最短长度,当然如果某个串是另一个串的子串那么直接吃了就可以了。那么就先用 B 去匹配 A,如果 B 不是 A 的子串那么就让 C 先匹配 A 再从 A,B 的相交位置开始匹配 B。

枚举六种顺序就可以了,再多几个串也是能做的,应该递归一下就行。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,inf=1145141919810;
char s[4][N];
ll len[4],nxt[4][N];
ll query(ll a,ll b,ll c){
	ll la=len[a],lb=len[b],lc=len[c];
	ll t1=0,t2=0;
	for(int i=1;i<=la;++i){
		while(t1&&s[b][t1+1]!=s[a][i]) t1=nxt[b][t1];
		if(s[b][t1+1]==s[a][i]) ++t1;
		if(t1==lb){
			t1=-1; //b是a的子串 
			break;
		}
	}
	if(t1==-1){
		t2=0;
		for(int i=1;i<=la;++i){
			while(t2&&s[c][t2+1]!=s[a][i]) t2=nxt[c][t2];
			if(s[c][t2+1]==s[a][i]) ++t2;
			if(t2==lc){
				t2=-1;
				break;
			}
		}
		if(t2==-1) return la;
		else return la+lc-t2;
	}
	else{ //先用c匹配a再从a,b的相交位置开始匹配b 
		t2=0;
		for(int i=1;i<=la;++i){
			while(t2&&s[c][t2+1]!=s[a][i]) t2=nxt[c][t2];
			if(s[c][t2+1]==s[a][i]) ++t2;
			if(t2==lc){
				t2=-1;
				break;
			}
		}
		if(t2!=-1){
			for(int i=t1+1;i<=lb;++i){
				while(t2&&s[c][t2+1]!=s[b][i]) t2=nxt[c][t2];
				if(s[c][t2+1]==s[b][i]) ++t2;
				if(t2==lc){
					t2=-1;
					break;
				}
			}
			if(t2==-1) return la+lb-t1;
			else return la+lb+lc-t1-t2;
		}
		else return la+lb-t1;
	}
}
int main(){
	while(scanf("%s%s%s",s[1]+1,s[2]+1,s[3]+1)!=EOF){
		for(int i=1;i<=3;++i){
			len[i]=strlen(s[i]+1);
			for(int j=0;j<=len[i];++j)
				nxt[i][j]=0;
		}
		for(int id=1;id<=3;++id){
			ll pos=0,l=len[id];
			for(int i=2;i<=l;++i){
				while(pos&&s[id][i]!=s[id][pos+1]) pos=nxt[id][pos];
				if(s[id][i]==s[id][pos+1]) ++pos;
				nxt[id][i]=pos;
			}
		}
		ll ans=inf;
		ans=min(ans,query(1,2,3));
		ans=min(ans,query(1,3,2));
		ans=min(ans,query(2,1,3));
		ans=min(ans,query(2,3,1));
		ans=min(ans,query(3,1,2));
		ans=min(ans,query(3,2,1));
		cout<<ans<<'\n';
	}
	return 0;
}

P1470 [USACO2.3] 最长前缀 Longest Prefix

我不晓得这个放字符串题还是 dp 题里面,KMP(还是AC自动机?)的题拿 dp 做真是太智慧了。

我们设 dp[i] 为是否存在满足题意的长度为 i 的前缀。有个结论:如果当前的字符串从后往前截了有一段集合中的串且截去这段串后的字串是合法的话那么当前串也是合法的。

至于枚举的话可以使用 set 优化,因为我们只需要把一个字串和长度与他相等的子串的位置和他进行比较就行了。

我觉得这个题不止普及吧……

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810;
string st,all=" ";
ll len1,len2,nxt[N],maxl;
ll dp[N];
set <string> s[21]; //今日智慧:字符串题dp
int main(){
	while(cin>>st){
		if(st==".") break;
		s[st.size()].insert(st);
		maxl=max(maxl,(ll)st.size());
	}
	dp[0]=1; ll ans=0;
	while(cin>>st) all=all+st;
	for(int i=1;i<all.size();++i)
		for(int j=min(i*1ll,maxl);j>=1;--j){
			string t=all.substr(i-j+1,j);
			if(s[t.size()].count(t)==1&&dp[i-j]){
				ans=i;
				dp[i]=1;
				break;
			}
		}
	cout<<ans;
	return 0;
}

P3435 [POI2006] OKR-Periods of Words

字符串题都这么智慧吗?

我们既然希望题中所说的周期长度最长,那么我们就可以去找到原串中减去这个周期前缀所得的最短的后缀,也就是最短的 border ,整张图来说明:

这样的话这个串中的最长周期长度就是 6,也就是 8minnext8。那么现在怎么来求所有的周期长度呢?我们珂以根据KMP的本质来一直向前递推去找最短 border,并且用类似记忆化的手段将当前的起点的 next 值也赋成所得答案。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810;
char s[N];
ll n,nxt[N],ans;
int main(){
	scanf("%lld%s",&n,s+1);
	ll pos=0;
	for(int i=2;i<=n;++i){
		while(pos&&s[i]!=s[pos+1]) pos=nxt[pos];
		if(s[i]==s[pos+1]) ++pos;
		nxt[i]=pos;
	}
	pos=2;
	for(int i=2;i<=n;++i){
		pos=i;
		while(nxt[pos]) pos=nxt[pos];
		if(nxt[i]) nxt[i]=pos;
		ans+=i-pos;
	}
	cout<<ans;
	return 0;
}

P2375 [NOI2014] 动物园

和上一道题有一样的地方,但是这里变成了在一个 log 次数内地往前跳,因为这个特殊的 border 的长度是小于等于串长度的一半的,这个特殊的 border 必定是在左半边的,如果直接暴力往前跳在 5e6 的数据下是很危险的。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810,mod=1e9+7;
char s[N];
ll n,T,nxt[N],ans,num[N];
int main(){
	scanf("%lld",&T);
	while(T--){
		ll pos=0; ans=1;
		scanf("%s",s+1); n=strlen(s+1);
		for(int i=0;i<=n;++i) nxt[i]=0,num[i]=0;
		num[1]=1;
		for(int i=2;i<=n;++i){
			while(pos&&s[i]!=s[pos+1]) pos=nxt[pos];
			if(s[i]==s[pos+1]) ++pos;
			nxt[i]=pos; num[i]=num[pos]+1;
		}
		pos=0;
		for(int i=1;i<=n;++i){
			while(pos&&s[i]!=s[pos+1]) pos=nxt[pos];
			if(s[i]==s[pos+1]) ++pos;
			while((pos<<1)>i) pos=nxt[pos]; //好像经常有这样往前跳的操作 
			ans=ans*(num[pos]+1ll)%mod;
		}
		cout<<ans<<'\n';
	}
	return 0;
}

E. Compress Words

珂以用 KMP,珂以用扩展 KMP,当然我是为了练哈希的。

边读边处理哈希值,题目所要求的是当前串最长的同时是上一个串的后缀的前缀,我们考虑用哈希求。考虑提前处理一下每一位的哈希值,然后用当前位数为 i 去替换答案的后 i 位,如果哈希值相等就可以替换。

习惯用双哈希了。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3e6+5,M=1919810;
const ll base1=1331,base2=13331;
const ll mod1=19260817,mod2=19491001;
//东方神秘力量助我哈希卡过 
ll n,last; char s[N],ans[N];
struct xx{
	ll h1,h2;
}h[N];
ll pre1[N],pre2[N];
int main(){
	srand(time(0));
	scanf("%lld",&n);
	pre1[0]=pre2[0]=1;
	for(int i=1;i<=1e6;++i){
		pre1[i]=pre1[i-1]*base1%mod1;
		pre2[i]=pre2[i-1]*base2%mod2;
	}
	for(int k=1;k<=n;++k){
		scanf("%s",s+1);
		ll len=strlen(s+1),pos=0,h1=0,h2=0;
		for(int i=1;i<=len&&i<=last;++i){
			h1=(h1*base1%mod1+(ll)s[i])%mod1;
			h2=(h2*base2%mod2+(ll)s[i])%mod2;
			if(h[last].h1==(h1+h[last-i].h1*pre1[i])%mod1
			&&h[last].h2==(h2+h[last-i].h2*pre2[i])%mod2)
				pos=i;
		}
		for(int i=pos+1;i<=len;++i){
			ans[++last]=s[i];
			h[last].h1=(h[last-1].h1*base1+(ll)s[i])%mod1;
			h[last].h2=(h[last-1].h2*base2+(ll)s[i])%mod2;
		}
	}
	printf("%s",ans+1);
	return 0;
}

P4421 [COCI2017-2018#1] Lozinke

用 map,先把每个哈希值放进 map 里,然后枚举每个串的子串,然后判断是否在map出现过,有的话就加上出现次数并且打一个标记防止重复计数,每次查询完要清空。

单哈希没卡过,双哈希启动!但是好像珂以直接以 string 为键值的

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*11451,M=1919810;
const ll base1=114514,base2=13331;
const ll mod1=998244353,mod2=19491001,prime=233317;
ll n; string s[N];
struct xx{
	ll h1,h2;
	bool operator <(const xx &lxl)const{
		return h1==lxl.h1?h2<lxl.h2:h1<lxl.h1;
	}
	bool operator ==(const xx &lxl)const{
		return h1==lxl.h1&&h2==lxl.h2;
	}
}; //双哈希启动! 
map <xx,ll> m;
map <xx,bool> f;
ll hash1(string s){
	ll len=s.size(),ans=0;
	for(int i=0;i<len;++i)
		ans=(ans*base1+(ll)s[i])%mod1+prime;
	return ans;
}
ll hash2(string s){
	ll len=s.size(),ans=0;
	for(int i=0;i<len;++i)
		ans=(ans*base2+(ll)s[i])%mod2+prime;
	return ans;
}
xx st[N]; ll top;
ll query(string s){
	ll ans=-1,n=s.size(); //自己也在map里,答案会多一
	for(int i=0;i<n;++i)
		for(int j=i;j<n;++j){
			string t=s.substr(i,j-i+1);
			xx h;
			h.h1=hash1(t),h.h2=hash2(t);
			if(m[h]&&!f[h]){
				ans+=m[h],st[++top]=h;
				f[h]=1;
			}
		}
	for(int i=1;i<=top;++i) f[st[i]]=0;
	top=0;
	return ans;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>s[i];
		++m[(xx){hash1(s[i]),hash2(s[i])}];
	}
	ll ans=0;
	for(int i=1;i<=n;++i) ans+=query(s[i]);
	cout<<ans;
	return 0;
}

UVA1328 Period&SP263

双倍经验针不戳

这个题也是一个比较智慧的题,代码简短结论神奇,但是证明起来的话有点思维难度(要不然为啥评蓝?)

首先看到我们要求每个前缀是不是周期串,既然是前缀还是有周期那么就可以考虑往 KMP 的方向去想。我们考虑怎么运用 next 数组,如果当前的前缀是周期串的话,那么他的 next 记录的当前的最长 border 也就是他到上一个最小循环节的距离,那么最小循环节的长度就是 inext[i],数量就是 i/(inext[i])

如果 i%(inext[i])0 的话就代表这里面有其他的子串不是循环节,就不输出。

code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810;
char s[N];
ll nxt[N],n,cnt,T;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin>>T;
	while(T--){
		++cnt;
		cin>>n;
		cin>>(s+1);
		ll pos=0;
		for(int i=1;i<=n;++i) nxt[i]=0;
		cout<<"Test case #"<<cnt<<'\n';
		for(int i=2;i<=n;++i){
			while(pos&&s[i]!=s[pos+1]) pos=nxt[pos];
			if(s[i]==s[pos+1]) ++pos;
			nxt[i]=pos;
			if(nxt[i]&&i%(i-nxt[i])==0) //如果整除则有一个能通过循环得到这个前缀的前缀 
				cout<<i<<" "<<i/(i-nxt[i])<<'\n';
		}
		cout<<'\n';
	}
	return 0;
} 

P5546 [POI2000] 公共串

SA 做的。先把所有串拼起来并在两个中间插个字符,把 height 求出来。珂以考虑二分答案,check 的话可以直接枚举 height,并且通过一个记录长度的 sum 数组二分 sa[i] 的值来找到当前是第几个串,然后标记已阅。如果 height 小于当前二分到的答案的话就把 vis 清零,每次判断一下就行,复杂度 O(nlog2nT),这个做法当 T 大了就死了。
还有为什么这个主题的 LaTxeygqp 的正体看不到下半截啊/fn,欸用 \tt 的字体就行了 ygpq

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*1145140,M=1919810;
//完了SA都记不到了
ll T,n,lim=256;
char s[N]; string a;
ll sa[N],rk[N],bu[N],w2[N],h[N];
ll sum[N];
void work(){
	for(int i=1;i<=n;++i) rk[i]=(ll)s[i],++bu[rk[i]];
	for(int i=2;i<=lim;++i) bu[i]+=bu[i-1];
	for(int i=n;i>=1;--i) sa[bu[rk[i]]--]=i;
	for(int k=1;k<=n;k<<=1){
		ll cnt=0;
		for(int i=1;i<=k;++i) w2[++cnt]=n-k+i;
		for(int i=1;i<=n;++i)
			if(sa[i]>k) w2[++cnt]=sa[i]-k;
		for(int i=1;i<=lim;++i) bu[i]=0;
		for(int i=1;i<=n;++i) ++bu[rk[i]];
		for(int i=2;i<=lim;++i) bu[i]+=bu[i-1];
		for(int i=n;i>=1;--i) sa[bu[rk[w2[i]]]--]=w2[i];
		swap(rk,w2);
		cnt=1,rk[sa[1]]=1;
		for(int i=2;i<=n;++i)
			rk[sa[i]]=(w2[sa[i-1]]==w2[sa[i]]&&w2[sa[i-1]+k]==w2[sa[i]+k]?cnt:++cnt);
		if(cnt==n) break;
		lim=cnt;
	}
}
void LCP(){
	ll k=0;
	for(int i=1;i<=n;++i) rk[sa[i]]=i;
	for(int i=1;i<=n;++i){
		if(rk[i]==1) continue;
		if(k) --k;
		ll j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
		h[rk[i]]=k;
	}
}
bool vis[6];
bool check(ll mid){
	//这个雀食比啥单调队列自然很多 
	for(int i=1;i<=n-T;++i){
		if(h[i]<mid) memset(vis,0,sizeof(vis));
		ll k=lower_bound(sum+1,sum+T+1,sa[i])-sum;
		vis[k]=1;
		bool f=1;
		for(int j=1;j<=T;++j)
			if(!vis[j]) f=0;
		if(f) return 1;
	}
	return 0;
}
int main(){
	cin>>T;
	for(int i=1;i<=T;++i){
		cin>>a;
		sum[i]=a.size(),sum[i]+=sum[i-1]+1;
		for(int j=0;j<a.size();++j) s[++n]=a[j];
		s[++n]=char(122+i);
	}
	work(),LCP();
	ll l=0,r=0,ans;
	//n-T,多了T个字符 
	for(int i=1;i<=n-T;++i) r=max(r,h[i]);
	while(l<=r){
		ll mid=l+r>>1;
		if(check(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	cout<<ans;
	return 0;
}

P2408不同字串个数

随便用个后缀算法,我用SA。
求出 height,考虑其定义为两个排名相邻的后缀的公共前缀数量,那么先求出所有子串的数量再减去这些 height 就行了。

posted @   和蜀玩  阅读(7)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示