题目描述
众所周知,密码在信息领域起到了不可估量的作用。对于普通的登陆口令以,唯一的破解方法就是暴力破解——逐个尝试所有可能的字母组合,但这是一项很耗时又容易被发现的工作。所以,为了获取对方的登陆口令,在暴力破解密码之前,必须先做大量的准备工作。经过情报的搜集,现在得到了若干有用信息,形如:
“我观察到,密码中含有字符串*。”
例如,对于一个10位的密码以及观察到的字符串hello与world,可能的密码组合为helloworld与worldhello;而对于6位的密码以及到的字符串good与day,可能的密码组合为gooday。
有了这些信息,就能够大大地减少尝试的次数了。请编一个程序,计算所有密码组合的可能。密码中仅可能包含a-z之间的小写字母。
输入格式
输入数据首先输入两个整数L,N,分别表示密码的长度与观察到子串的个数。
接下来N行,每行若干个字符,描述了每个观察到的字符串。
输出格式
输出数据第一行为一个整数,代表了满足所有观察条件字符串的总数。
若这个数字小于等于42,则按字典顺序输出所有密码的可能情况,每行一个,否则,只输出满足所有观察条件字符串的总数即可。
题解:
看数据范围应该就能看出来是状压DP,而且最后又要包含所有的单词,那么我们可以直接设dp[i][j][S] 表示已经处理到的长度为i,在AC自动机上j结点上,已经包含的单词数量为S的方案数。
然后考虑一个问题,如果一个字符串是另一个字符串的子串呢?比如处理了’abc‘,里面已经包含了串‘bc’,而我们这样状压是会认为没有出现’bc’的,但实际上它是出现过的,因此我们需要对互相包含的字符串进行一个去重,将重复的字符串直接删掉,然后统计答案的时候就记录没有被删掉的单词的集合state就好…
然后就是输出方案直接预处理出每两个字符串的重叠部分,然后爆搜即可
AC代码:
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#include<ext/rope>
using namespace std;
using namespace __gnu_cxx;
#define LL long long
#define pii pair<int,int>
#define mp(a,b) make_pair(a,b)
const int MAXN = 55;
const int MOD = 1000000007;
const int INF = 0x3f3f3f3f;
char a[MAXN][MAXN],ans[MAXN][MAXN];
LL dp[2][150][(1<<10)+20];
int len[MAXN],bor[MAXN][MAXN],nxt[150][26],fail[150],sta[150];
int del[MAXN],g[MAXN],vis[MAXN],id[MAXN],L,n,m,ans_tot,cnt,tot;
inline void Insert(int x){
int rt=0;
for(int i=1;i<=len[x];i++){
if(!nxt[rt][a[x][i]-'a']) nxt[rt][a[x][i]-'a']=++tot;
rt = nxt[rt][a[x][i]-'a'];
}
sta[rt]=(1<<(x-1));
}
inline void Build(){
queue<int> que;
for(int i=0;i<26;i++) if(nxt[0][i]) que.push(nxt[0][i]);
while(!que.empty()){
int u=que.front(); que.pop();
for(int i=0;i<26;i++){
if(nxt[u][i]){
fail[nxt[u][i]]=nxt[fail[u]][i];
que.push(nxt[u][i]);
}else{
nxt[u][i]=nxt[fail[u]][i];
}
}
}
}
inline int border(int x,int y){//计算x,y串的重叠部分
for(int i=min(len[x],len[y]);i>=0;i--){//枚举重叠的长度判断即可
bool ok = true;
for(int j=1;j<=i;j++)
if(a[x][len[x]-i+j]!=a[y][j])
{ ok=false; break; }
if(ok) return i;
}
}
inline bool check(int x,int y){//判断x是否为y的子串
for(int i=0;i<=len[y]-len[x];i++){
bool ok = true;
for(int j=1;j<=len[x];j++)
if(a[x][j]!=a[y][i+j])
{ ok=false; break; }
if(ok) return true;
}
return false;
}
inline void unique(){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(len[j]>len[i] && !del[j] && !del[i] && check(i,j))
del[i]=1,m--;//如果i串是j串的子串,就把i串删掉
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
bor[i][j]=border(i,j);//暴力处理出每两个字符串的重叠部分
}
void dfs(int x){//爆搜即可
if(x>m){
cnt++; int t=0;
for(int i=1;i<=ans_tot;i++)
for(int j=bor[g[i-1]][g[i]]+1;j<=len[g[i]];j++)
ans[cnt][++t]=a[g[i]][j];
if(t!=L) cnt--;
return;
}
for(int i=1;i<=n;i++){
if(!del[i] && !vis[i]){
vis[i]=1; g[++ans_tot]=i;
dfs(x+1);
vis[i]=0; --ans_tot;
}
}
}
inline bool cmp(int x,int y){//按字典序排序
for(int i=1;i<=L;i++)
if(ans[x][i]!=ans[y][i])
return ans[x][i]<ans[y][i];
return false;
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("C:\\Users\\Administrator\\Desktop\\in.txt","r",stdin);
#endif // ONLINE_JUDGE
scanf("%d%d",&L,&m); n = m;
for(int i=1;i<=m;i++) scanf("%s",a[i]+1),len[i]=strlen(a[i]+1);
unique();
for(int i=1;i<=n;i++) if(!del[i]) Insert(i);
Build();
dp[0][0][0]=1;
for(int i=1;i<=L;i++){//BZOJ内存限制小,需要滚动数组优化,洛谷不用
memset(dp[i&1],0,sizeof(dp[i&1]));
for(int j=0;j<=tot;j++)
for(int s=0;s<(1<<n);s++)
if(dp[(i+1)&1][j][s])
for(int k=0;k<26;k++)
dp[i&1][nxt[j][k]][s|sta[nxt[j][k]]] += dp[(i+1)&1][j][s];
}
LL res=0,s=0;
for(int i=1;i<=n;i++) if(!del[i]) s+=(1<<(i-1));//只统计没被删掉的单词
for(int i=0;i<=tot;i++) res+=dp[L&1][i][s];
printf("%lld\n",res);
if(res<=42){
dfs(1);
for(int i=1;i<=cnt;i++) id[i]=i;
sort(id+1,id+cnt+1,cmp);
for(int i=1;i<=cnt;i++){
for(int j=1;j<=L;j++) putchar(ans[id[i]][j]);
puts("");
}
}
return 0;
}