zoukankan      html  css  js  c++  java
  • 经典AC自动机DP

    CF808G Anthem of Berland

    给定 (s,t) 串,(s) 串中有问号,问 (t)(s) 中的最大出现次数。

    (|s| imes |t| leq 10^7)

    题解

    AC自动机经典题,直接 (dp(i,j)) 表示前 (i) 个点,AC自动机状态是 (j) 的最大出现次数。

    枚举出边转移就完事。时间复杂度 (O(26nm))

    有个常数优化,可以把 (O(26)) 去掉。

    注意到AC自动机每个状态的出边只有一条能转移到儿子,其余的都是沿着fail跳了若干步再转移。

    那么我们可以把跳fail的过程放到DP里。方法是每次chkmax(dp[i-1][fail[j]],dp[i-1][j])然后只转移那条能到儿子的边。

    CO int N=1e5+10;
    char s[N],t[N];
    int fa[N],dp[N];
    
    int main(){
    	scanf("%s",s+1);
    	int n=strlen(s+1);
    	scanf("%s",t+1);
    	int m=strlen(t+1);
    	for(int i=2;i<=m;++i){
    		int j=fa[i-1];
    		while(j and t[j+1]!=t[i]) j=fa[j];
    		fa[i]=j+(t[j+1]==t[i]);
    	}
    	fill(dp,dp+m+1,-1),dp[0]=0;
    	for(int i=1;i<=n;++i){
    		for(int j=m;j>=0;--j)if(dp[j]!=-1){
    			if(j>0) dp[fa[j]]=max(dp[fa[j]],dp[j]);
    			if(j<m and (s[i]=='?' or s[i]==t[j+1])) dp[j+1]=dp[j];
    			if(j>0) dp[j]=-1;
    		}
    		if(dp[m]!=-1) ++dp[m];
    	}
    	printf("%d
    ",*max_element(dp,dp+m+1));
    	return 0;
    }
    

    密码

    众所周知,密码在信息领域起到了不可估量的作用。对于普通的登陆口令,唯一的破解 方法就是暴力破解一逐个尝试所有可能的字母组合,但这是一项很耗时又容易被发现的工 作。所以,为了获取对方的登陆口令,在暴力破解密码之前,必须先做大量的准备工作。经 过情报的搜集,现在得到了若干有用信息,形如:

    “我观察到,密码中含有字符串***。”

    例如,对于一个10位的密码以及串hello与world,可观察到的字符能的密码组合为 helloworld与worldhello;而对于6位的密码以及观察到的字符串good与day,可能的 密码组合为gooday。

    有了这些信息,就能够大大地减少尝试的次数了。请编一个程序,计算所有密码组合的可能。密码中仅可能包含a - z之间的小写字母。

    对于100%的数据,1<=L<=25,1<=N<=10,每个观察到的字符串长不超过10,并且保证输出结果小于2^63。

    LadyLex的题解

    我们首先考虑:对于串(i)(j),如果(j)(i)的子串,那么我们根本不用考虑最初单独插入进来的(j)串,因为只要(i)串存在,(j)串就一定存在

    那么我们可以在构建出AC自动机之后,把每个节点从fail指针能达到的节点都设为”不是单词节点“,最后再给单词节点重新编号即可。

    那么接下来,我们考虑dp的过程。由于节点数,串数和串长都很小,所以我们考虑状态压缩来解决这个问题。

    我们定义状态数组(f[i][j][k])表示当前串长为(i),位于(j)号节点,模式串出现情况为(k)的方案数。

    (这种"走(i)步到达(j)节点”也是AC自动机上的常见套路之一)

    那么我们事先把单词节点对应的串用二进制压好,转移到时候我们只需要这样处理即可:

    f[i+1][ch[j][u]][k|val[ch[j][u]]]+=f[i][j][k];
    

    这样我们就可以搜出方案数,接下来我们考虑输出小于42的具体方案。

    首先我们可以得到一个性质:若总方案数不超过42,那么最终串一定仅由给定串拼接而成。

    因为如果随机字母可以存在,哪怕只有1个模式串,并且仅有1个随机字母,合法方案数在这种最小情况下也有2×26=52种>42

    因此我们只需要用搜索进行一个dp的逆过程,看合法方案由哪个节点转移过来,并且记录一路上经过的字符,最后排序输出即可。

    这真是一道很的题目,方式以及套路很经典,对于状压和搜索的应用都很灵活!
    UPD:这题强行组合了两种套路。

    时间复杂度(O(26 L N^2 2^N)),算出来是7e7。

    co int N=11,L=26,K=(1<<10)+10;
    int l,n;
    char s[N][N];
    namespace AC
    {
    	int tot,num;
    	int ch[N*N][26],fail[N*N];
    	int val[N*N],meaning[N*N];
    	
    	void ins(char s[],int n)
    	{
    		int u=0;
    		for(int i=0;i<n;++i)
    		{
    			int k=s[i]-'a';
    			if(!ch[u][k])
    				ch[u][k]=++tot;
    			u=ch[u][k],meaning[u]=k;
    		}
    		val[u]=1;
    	}
    	
    	void getfail()
    	{
    		std::queue<int>Q;
    		for(int i=0;i<26;++i)
    			if(ch[0][i])
    				Q.push(ch[0][i]);
    		while(Q.size())
    		{
    			int u=Q.front();Q.pop();
    			for(int i=0;i<26;++i)
    			{
    				if(ch[u][i])
    				{
    					fail[ch[u][i]]=ch[fail[u]][i];
    					Q.push(ch[u][i]);
    				}
    				else
    					ch[u][i]=ch[fail[u]][i];
    			}
    		}
    		for(int i=1;i<=tot;++i)
    			for(int u=fail[i];u;u=fail[u])
    				val[u]=0;
    		for(int i=1;i<=tot;++i)
    			if(val[i])
    				val[i]=(1<<num++);
    	}
    	
    	ll f[L][N*N][K];
    	struct sol
    	{
    		char s[L];
    		
    		sol()
    		{
    			memset(s,0,sizeof s);
    		}
    		
    		void print()
    		{
    			puts(s);
    		}
    		
    		bool operator<(co sol&b)co
    		{
    			for(int i=0;i<l;++i)
    				if(s[i]!=b.s[i])
    					return s[i]<b.s[i];
    			return 0;
    		}
    	}stack;
    	std::vector<sol>str;
    	
    	void dfs(int len,int i,int state,int now)
    	{
    		stack.s[len-1]=now+'a';
    		if(len==1)
    		{
    			str.push_back(stack);
    			return;
    		}
    		for(int j=0;j<=tot;++j)
    			if(f[len-1][j][state]&&ch[j][now]==i)
    				dfs(len-1,j,state,meaning[j]);
    		if(val[i])
    			for(int j=0;j<=tot;++j)
    				if(f[len-1][j][state^val[i]]&&ch[j][now]==i)
    					dfs(len-1,j,state^val[i],meaning[j]);
    	}
    	
    	void getsolution()
    	{
    		for(int i=1;i<=tot;++i)
    			if(f[l][i][(1<<num)-1])
    				dfs(l,i,(1<<num)-1,meaning[i]);
    	}
    	
    	void solve()
    	{
    		f[0][0][0]=1;
    		for(int i=0;i<l;++i)
    			for(int j=0;j<=tot;++j)
    				for(int k=0;k<(1<<num);++k)
    					if(f[i][j][k])
    		for(int u=0;u<26;++u)
    			f[i+1][ch[j][u]][k|val[ch[j][u]]]+=f[i][j][k];
    		ll ans=0;
    		for(int j=0;j<=tot;++j) // edit 1:0
    			ans+=f[l][j][(1<<num)-1];
    		printf("%lld
    ",ans);
    		if(ans<=42)
    		{
    			getsolution();
    			sort(str.begin(),str.end());
    			assert(str.size()==ans);
    			for(int i=0;i<ans;++i)
    				str[i].print();
    		}
    	}
    }
    
    int main()
    {
    	read(l),read(n);
    	for(int i=1;i<=n;++i)
    	{
    		scanf("%s",s[i]);
    		AC::ins(s[i],strlen(s[i]));
    	}
    	AC::getfail();
    	AC::solve();
    	return 0;
    }
    

    文本生成器

    给出若干个由大写字母构成的单词,问长度为 m ,由大写字母构成的字符串中,包含至少一个单词的数目.对 10007 取模.

    jklover的题解

    可以先求出不包含任意一个单词的字符串数目,再用总数目26m减去.

    将单词建成一个 AC 自动机,类似上题,合并权值即可求出一个节点是否能被走到.

    (f[i][j]) 表示已经走了 (i) 步,走到了节点 (j) 时的方案数. (O(n^2)) dp 即可.

    AC自动机上面dp才是AC自动机的精髓。

    co int mod=1e4+7;
    
    int add(int x,int y)
    {
    	return (x+y)%mod;
    }
    
    int mul(int x,int y)
    {
    	return x*y%mod;
    }
    
    int qpow(int x,int k)
    {
    	int res=1;
    	while(k)
    	{
    		if(k&1)
    			res=mul(res,x);
    		x=mul(x,x),k>>=1;
    	}
    	return res;
    }
    
    co int N=7777,S=26;
    int n,m;
    namespace AC
    {
    	int idx;
    	int ch[N][S],fail[N],val[N];
    	int f[101][N];
    	
    	void init()
    	{
    		memset(f,-1,sizeof f);
    	}
    	
    	void ins(char*s,int len)
    	{
    		int u=0;
    		for(int i=0;i<len;++i)
    		{
    			int k=s[i]-'A';
    			if(!ch[u][k])
    				ch[u][k]=++idx;
    			u=ch[u][k];
    		}
    		val[u]=1;
    	}
    	
    	void getfail()
    	{
    		std::queue<int>Q;
    		for(int i=0;i<S;++i)
    			if(ch[0][i])
    				Q.push(ch[0][i]);
    		while(Q.size())
    		{
    			int u=Q.front();Q.pop();
    			for(int i=0;i<S;++i)
    			{
    				if(ch[u][i])
    				{
    					fail[ch[u][i]]=ch[fail[u]][i];
    					Q.push(ch[u][i]);
    				}
    				else
    					ch[u][i]=ch[fail[u]][i];
    			}
    			val[u]|=val[fail[u]];
    		}
    	}
    	
    	int dfs(int i,int j)
    	{
    		if(f[i][j]!=-1)
    			return f[i][j];
    		if(val[j])
    			return 0;
    		if(i==m)
    			return 1;
    		int&res=f[i][j]=0;
    		for(int k=0;k<S;++k)
    			res=add(res,dfs(i+1,ch[j][k]));
    		return res;
    	}
    	
    	void solve()
    	{
    		int ans=qpow(26,m);
    		ans=add(ans,mod-dfs(0,0));
    		printf("%d
    ",ans);
    	}
    }
    char buf[N];
    
    int main()
    {
    	AC::init();
    	read(n),read(m);
    	for(int i=1;i<=n;++i)
    	{
    		scanf("%s",buf);
    		AC::ins(buf,strlen(buf));
    	}
    	AC::getfail();
    	AC::solve();
    	return 0;
    }
    

    Rescue the Rabbit

    现在有n个基因片段(用包含A、G、T、C的字符串表示),每个基因片段有一个权值,现在求长为L的基因的最大权值(每个基因片段重复出现算一次,不用计算多次)?

    n (1 ≤ n ≤ 10),l (1 ≤ l ≤ 100)

    分析

    未知定长串中不同已知模板串的出现次数问题,一般做法是AC自动机上dp。

    考虑背包,(dp(i,j,k))表示当前串长为(i),在AC自动机上对应节点(j),已匹配的模板串的状态为(k)的情况是否出现。用刷表法向后转移。先枚举不定串长度,再枚举AC自动机上节点,然后枚举已知状态,最后枚举字母边转移。

    时间复杂度(O(l cdot MaxNode cdot 2^n cdot SigmaSize))。第一维可以滚动,空间复杂度(O(MaxNode cdot 2^n))

    const int MAXN=1010;
    const int SigmaSize=4;
    
    bool dp[2][MAXN][1100];
    int mp[15];
    
    struct Trie
    {
    	int next[MAXN][SigmaSize];
    	int fail[MAXN];
    	int end[MAXN];
    	int root,ncnt;
    	int newnode()
    	{
    		for(int i=0;i<SigmaSize;++i)
    			next[ncnt][i]=-1;
    		end[ncnt++]=0;
    		return ncnt-1;
    	}
    	void init()
    	{
    		ncnt=0;
    		root=newnode();
    	}
    	int id(char c)
    	{
    		if(c=='A')
    			return 0;
    		else if(c=='G')
    			return 1;
    		else if(c=='T')
    			return 2;
    		else
    			return 3;
    	}
    	void insert(char*str,int v)
    	{
    		int now=root;
    		int len=strlen(str);
    		for(int i=0;i<len;++i)
    		{
    			int c=id(str[i]);
    			if(next[now][c]==-1)
    				next[now][c]=newnode();
    			now=next[now][c];
    		}
    		end[now]|=(1<<v);
    	}
    	void getfail()
    	{
    		queue<int>Q;
    		fail[root]=root;
    		for(int i=0;i<SigmaSize;++i)
    		{
    			if(next[root][i]==-1)
    				next[root][i]=root;
    			else
    			{
    				fail[next[root][i]]=root;
    				Q.push(next[root][i]);
    			}
    		}
    		while(!Q.empty())
    		{
    			int now=Q.front();
    			Q.pop();
    			end[now]|=end[fail[now]];
    			for(int i=0;i<SigmaSize;++i)
    			{
    				if(next[now][i]==-1)
    					next[now][i]=next[fail[now]][i];
    				else
    				{
    					fail[next[now][i]]=next[fail[now]][i];
    					Q.push(next[now][i]);
    				}
    			}
    		}
    	}
    	void solve(int n,int l)
    	{
    		memset(dp,0,sizeof(dp));
    		dp[0][0][0]=1; // dp[len][node][state]
    		int cur=1;
    		for(int i=1;i<=l;++i)
    		{
    			memset(dp[cur],0,sizeof(cur));
    			for(int j=0;j<ncnt;++j)
    			{
    				for(int k=0;k<(1<<n);++k)
    				{
    					for(int q=0;q<SigmaSize;++q)
    					{
    						int nxt=next[j][q];
    						dp[cur][nxt][k|end[nxt]]=(dp[cur][nxt][k|end[nxt]]||dp[cur^1][j][k]);
    					}
    				}
    			}
    			cur^=1;
    		}
    		int ans=-INF;
    		for(int i=0;i<ncnt;++i)
    			for(int j=0;j<(1<<n);++j)
    			{
    				if(dp[cur^1][i][j])
    				{
    					int sum=0;
    					for(int k=0;k<n;++k)
    						if(j&(1<<k))
    							sum+=mp[k];
    					ans=max(ans,sum);
    				}
    			}
    		if(ans<0)
    			printf("No Rabbit after 2012!
    ");
    		else
    			printf("%d
    ",ans);
    	}
    }AC;
    
    char s[110];
    
    int main()
    {
    	int n,l,w;
    	while(~scanf("%d %d",&n,&l))
    	{
    		AC.init();
    		for(int i=0;i<n;++i)
    		{
    			scanf("%s %d",s,&w);
    			AC.insert(s,i);
    			mp[i]=w;
    		}
    		AC.getfail();
    		AC.solve(n,l);
    	}
        return 0;
    }
    
  • 相关阅读:
    结对作业(测试版)
    回答自己的提问
    阅读一个程序员的生命周期有感
    阅读13到17章提出问题
    读8 9 10章提出问题
    5.2 5.3测试与封装
    5.1 四则运算单元测试j
    阅读5.5章6章7章提出疑问
    做汉堡
    阅读第1到第5章过程的疑问
  • 原文地址:https://www.cnblogs.com/autoint/p/12582408.html
Copyright © 2011-2022 走看看