【TJOI2018】碱基序列【kmp,hash,SAM】

2 篇文章 0 订阅
1 篇文章 0 订阅

传送门

这是一道非常好的字符串多算法练习题。

你可以用很多算法做这道题。

题意:给定一个字符串,再给定k组字符串,每组最多10个。要求按顺序从k组字符串中各选择一个共k个字符串,要求这个大字符串是初始给定字符串的一个子串。求方案数。(只要位置不同或选择不同都算不同方案)

天然的无后效性,每组只选一个,天然的状态设置,促使我们设置如下状态。

f[i][j]表示我选完了前i组字符串,形成的串在原串以j为末尾匹配上的方案数。

所以方程很明显:f[i][j]=\sum(f[i-1][k])[k\to j == ch\subset S[k]]

就是说要求第k个字符到第j个字符跟第i组字符串中的某个匹配上。

我们惊讶的发现甚至可以滚动数组去掉第一维i。

 

 

SAM

第一反应是SAM因为近来这种题摸的最多。对于SAM来说,方程的j意义变化为:走到自动机的j号结点。

很明显,新加入一个子串得到的串必须仍然是原串子串。

那把SAM建出来,枚举上一个串在SAM上走到的位置,这个串就老老实实走,如果走出了0,说明失败了。反之说明可以弄上。那就转移。

因为后缀自动机的性质(从起点走出任意一个子串),我们不需要在一开始把每一个j都方案初始化为1,只需要把起点初始为1就行。

当然,位置不同算不同方案,所以要把答案乘上fail树的子树size。

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define int long long
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;ch=getchar();
	}return cnt*f;
}
struct node{
	int ch[26],fail,len;
}t[20003];int cnt=1,last=1;
int size[20003];
int first[20003],nxt[20003],to[20003],tot;
void add(int a,int b){
	nxt[++tot]=first[a];first[a]=tot;to[tot]=b;
}
void insert(int x){
	int p=last,now=++cnt;last=cnt;t[now].len=t[p].len+1;
	for(;p&&!t[p].ch[x];p=t[p].fail)t[p].ch[x]=now;
	if(!p)t[now].fail=1;
	else{
		int q=t[p].ch[x];if(t[q].len==t[p].len+1)t[now].fail=q;
		else{
			int tem=++cnt;t[tem]=t[q];t[tem].len=t[p].len+1;t[q].fail=t[now].fail=tem;
			for(;p&&t[p].ch[x]==now;p=t[p].fail)t[p].ch[x]=tem;
		}
	}
}
int n;
char ch[20003];
void dfs(int u){
	size[u]=1;
	for(int i=first[u];i;i=nxt[i]){
		int v=to[i];dfs(v);size[u]+=size[v]; 
	}
}
int f[2][20003];int len;
const int mod=1e9+7;
int get(int u){
	for(int i=1;i<=len;i++){
		u=t[u].ch[ch[i]-'A'];if(!u)return 0;
	}return u;
}
signed main(){
	n=in;
	scanf("%s",ch+1);len=strlen(ch+1);
	for(int i=1;i<=len;i++)insert(ch[i]-'A');
	for(int i=2;i<=cnt;i++)add(t[i].fail,i);
	dfs(1);int m;f[0][1]=1;
	for(int x=1;x<=n;x++){
		m=in;int i=x&1;memset(f[i],0,sizeof(f[i]));
		for(int g=1;g<=m;g++){
			scanf("%s",ch+1);len=strlen(ch+1);
			for(int j=1;j<=cnt;j++){
				if(f[i^1][j]){
					int id=get(j);if(id)f[i][id]=(f[i^1][j]+f[i][id])%mod;
				}
			}
		}
	}int ans=0;
	for(int i=1;i<=cnt;i++){
		ans=(ans+f[n&1][i]*size[i]%mod)%mod;
	}cout<<ans;
	return 0;
}

kmp

也是常用的字符串匹配工具。

我们对于当前走到的位置,要判断是否和原串匹配。

那我们直接给当前串求nxt数组,然后按顺序该怎么跳怎么跳。如果跳出来发现长度跟自己一样了,说明弄上了,加方案。

注意:初始化要给每个节点初始为1,因为从任何地方开始的子串都算方案。

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define int long long
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;
		ch=getchar();
	}return cnt*f;
}
int nxt[10003];
char s[10003],ch[10003];
int n,m,len;
int f[2][10003];
const int mod=1e9+7;
signed main(){
	n=in;scanf("%s",s+1);len=strlen(s+1);
	for(int i=0;i<=len;i++)f[0][i]=1;
//	f[0][0]=1;
	int i=0;
	while(n--){
		i^=1;
		memset(f[i],0,sizeof(f[i]));
		m=in;
		while(m--){
			scanf("%s",ch+1);int lenc=strlen(ch+1);
			for(int x=2,j=0;x<=lenc;x++){
				while(j&&ch[j+1]!=ch[x])j=nxt[j];
				j+=(ch[j+1]==ch[x]);nxt[x]=j;
			}
			for(int x=1,y=0;x<=len;x++){
				while(y&&s[x]!=ch[y+1])y=nxt[y];
				y+=ch[y+1]==s[x];
				if(y==lenc){
					f[i][x]=(f[i][x]+f[i^1][x-lenc])%mod;
				}
			}
		}
	}
	int ans=0;
	for(int x=0;x<=len;x++)ans=(ans+f[i][x])%mod;
	cout<<ans;
	return 0;
}

Hash

本题首选哈希。因为代码很简单,思想很好懂。

和前面一样,我们要判断前k位是否和本串相同,这是很基础的hash操作。

没什么好说的了,,代码也很短。

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define int long long
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;
		ch=getchar();
	}return cnt*f;
}
const int mod=1e9+7;
int n,m;char s[10003],ch[10003];
int f[2][10003];int lens,len;
int now;
int Hash[10003];
int hh[10003];
int jz[10003];
signed main(){
	n=in;scanf("%s",s+1);lens=strlen(s+1);jz[0]=1;
	for(int i=1;i<=lens;i++)Hash[i]=(Hash[i-1]*13+s[i]-'A')%mod,jz[i]=jz[i-1]*13%mod;
	for(int i=0;i<=lens;i++)f[0][i]=1;
	while(n--){
		now^=1;memset(f[now],0,sizeof(f[now]));
		m=in;
		while(m--){
			scanf("%s",ch+1);len=strlen(ch+1);
			for(int i=1;i<=len;i++)hh[i]=(hh[i-1]*13+ch[i]-'A')%mod;
			for(int i=len;i<=lens;i++){
				if((Hash[i]-hh[len]+mod)%mod==Hash[i-len]*jz[len]%mod)
				f[now][i]=(f[now][i]+f[now^1][i-len])%mod;
			}
		}
	}
	int ans=0;
	for(int i=0;i<=lens;i++)ans=(ans+f[now][i])%mod;
	cout<<ans;
	return 0;
} 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值