题解——P4045 [JSOI2009]密码


题目传送门

题意

给定 \(n\) 个模式串,求长度为 \(L\) 且包含所有模式串的文本串个数,若个数小于等于 \(42\),则按字典序升序输出全部文本串。

思路+实现

(1)AC自动机

首先多模式串匹配问题自然会想到AC自动机。

其次,当一个状态 \(u\)
失配指针 \(f\) 包含某个模式串,那么 \(u\) 也包含该模式串,于是在构建失配指针时,可以预处理出每个节点所包含的模式串状态。

struct AC_Automaton{
	int tr[105][26],tot,fail[105],mark[105];
	inline void insert(int k){
		int u=0,l=strlen(s[k]+1);
		for(int i=1;i<=l;++i){
			if(!tr[u][s[k][i]-'a']) tr[u][s[k][i]-'a']=++tot;
			u=tr[u][s[k][i]-'a'];
		}
		mark[u]|=(1<<(k-1));
	}
	inline void build(){
		queue q;
		for(int i=0;i<26;i++){
			if(tr[0][i]) q.push(tr[0][i]);
		}
		while(!q.empty()){
			int u=q.front();
			q.pop();
			for(int i=0;i<26;i++){
				if(tr[u][i]){
					fail[tr[u][i]]=tr[fail[u]][i];
					q.push(tr[u][i]);
					mark[tr[u][i]]|=mark[tr[fail[u]][i]];
				}
				else tr[u][i]=tr[fail[u]][i];
			}
		}
	}
}ac;

(2)状压dp

与P4052 文本生成器的思路相似,我们考虑dp。

简单概述一下文本生成器那题的思路,因为“至少包含一个模式串”与“不包含任何模式串”是“互补”关系,所以我们求不包含任何模式串的方案数即可;不过本题要求包含所有模式串的方案数,需要在转移中附带当前状态(如果是不包含直接只选择没有标记的节点即可)。

设置 \(dp(i,j,u)\),标记长度为 \(i\),当前已经包含的模式串状态为 \(j\),当前节点是 \(u\) 的方案数。我们令 \(u\) 向下的一个节点为 \(v\),用刷表法,可以得到:\(dp(i+1,j|mark(v),v)=\sum dp(i,j,u)\),最后统计 \(\sum_{i=0}^{tot} dp(L,(1<,即为答案。

dp[0][0][0]=1;
for(int i=0;i<=len;i++){
	for(int j=0;j<(1<

(3)输出方案

因为当答案小于等于 \(42\) 时才输出方案,且转移过程中不便记录方案,我们可以考虑最后 \(dfs\) 输出方案。

观察 \(42\) 这个数,貌似没有特别之处,但恰恰因为它很小,所以所有文本串都不会存在空余位置,比如模式串为 \(\text{down}\)\(\text{need}\),要求长度为 \(8\),因为存在重叠的首尾,所以某些文本串可以写作形如 \(\text{needownX}\) 的样子,而同时这个 \(\text{X}\) 也可以放在文本串首,那么此时就已经有 \(52\) 种选择了,是一定不在输出方案的范围内的。

换言之,文本串一定是“紧凑”的,所以我们只需要预处理出任意两个模式串的首尾重复长度,\(dfs\) 判断长度是否等于 \(L\) 并按字典序输出即可。

inline void dfs(int x){
	if(x==n+1){
		string now="";
		for(int i=1;i<=n;i++){
        	//l[i][j]表示第i个模式串的后缀与第j个模式串前缀的重合长度
			int pos=(i==1)?0:l[ord[i-1]][ord[i]];
			for(int j=pos+1;j<=strlen(s[ord[i]]+1);j++){
				now+=s[ord[i]][j];
			}
		}
		if(now.size()==len){
			_out.push_back(now);
		}
		return;
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			vis[i]=1,ord[x]=i;
			dfs(x+1);
			vis[i]=0,ord[x]=0;
		}
	}
}