时空限制 1000ms / 128MB
题目描述
众所周知,密码在信息领域起到了不可估量的作用。对于普通的登陆口令以,唯一的破解方法就是暴力破解——逐个尝试所有可能的字母组合,但这是一项很耗时又容易被发现的工作。所以,为了获取对方的登陆口令,在暴力破解密码之前,必须先做大量的准备工作。经过情报的搜集,现在得到了若干有用信息,形如:
我观察到,密码中含有字符串*。
例如,对于一个10位的密码以及观察到的字符串hello与world,可能的密码组合为helloworld与worldhello;而对于6位的密码以及到的字符串good与day,可能的密码组合为gooday。
有了这些信息,就能够大大地减少尝试的次数了。请编一个程序,计算所有密码组合的可能。密码中仅可能包含a-z之间的小写字母。
输入格式:
输入数据首先输入两个整数L,N,分别表示密码的长度与观察到子串的个数。
接下来N行,每行若干个字符,描述了每个观察到的字符串。
输出格式:
输出数据第一行为一个整数,代表了满足所有观察条件字符串的总数。
若这个数字小于等于42,则按字典顺序输出所有密码的可能情况,每行一个,否则,只输出满足所有观察条件字符串的总数即可。
题目分析
求个数的DP方法依然十分套路
d
p
[
i
]
[
j
]
[
s
]
dp[i][j][s]
dp[i][j][s]表示已完成密码前
i
i
i位,当前在AC自动机上
j
j
j结点,且当前字符串使用情况为
s
s
s 的 满足条件的字符串个数
直接从父亲向儿子推即可
重点在于方案输出
先考虑一下满足什么情况总方案数才会小于等于42
首先 密码中不会存在一个字符不属于任何一个观察到的字符串
因为假如存在这样一个字符,那么它可以是a~z中任何一个,且它可以出现在密码开头或结尾
这样就已经有2*26=52中情况了
其次 所有观察到的字符串在衔接时若前后缀出现相同部分,则这部分必定重合在一起衔接
例如上述
g
o
o
d
good
good与
d
a
y
day
day会结合成
g
o
o
d
a
y
gooday
gooday
假如存在一种合法情况是没有完全重合,比如"
∗
∗
g
o
o
d
d
a
y
∗
**goodday*
∗∗goodday∗",那么"
∗
∗
g
o
o
d
a
y
∗
∗
**gooday**
∗∗gooday∗∗"一定也合法
这样空出一个位置可以是任意字符,就转化成了前一种情况,不符合条件
知道密码的构成后观察数据范围 n < = 10 n<=10 n<=10,直接 O ( ! n ) O(!n) O(!n)枚举排列即可
需要注意的是若某个字符串 s s s是字符串 t t t的子串,那么 s s s可以直接筛掉
貌似蒟蒻代码里构造string的方法BZOJ上CE,luogu上过了,反正就是懒得改=_=
#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=110;
int n,L,cnt;
int ch[maxn][30],rem[maxn];
int fail[maxn];
queue<int> q;
lt dp[maxn][maxn][5010],ans;
int len[15],sub[15],cov[15][15];
int pm[15];
char pt[15][maxn];
string res[50];
void ins(char* ss,int len,int num)
{
int u=0;
for(int i=0;i<len;++i)
{
int x=ss[i]-'a';
if(!ch[u][x]) ch[u][x]=++cnt;
u=ch[u][x];
}
rem[u]=num;
}
void ACM()
{
for(int i=0;i<26;++i)
if(ch[0][i]) fail[ch[0][i]]=0,q.push(ch[0][i]);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=0;i<26;++i)
{
if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
else{
fail[ch[u][i]]=ch[fail[u]][i];
q.push(ch[u][i]);
rem[ch[u][i]]|=rem[fail[ch[u][i]]];
}
}
}
}
void DP()
{
dp[0][0][0]=1;
for(int i=1;i<=L;++i)
for(int j=0;j<=cnt;++j)
for(int s=0;s<(1<<n);++s)
{
if(!dp[i-1][j][s]) continue;
for(int k=0;k<26;++k)
{
if(rem[ch[j][k]]) dp[i][ch[j][k]][s|(1<<rem[ch[j][k]]-1)]+=dp[i-1][j][s];
else dp[i][ch[j][k]][s]+=dp[i-1][j][s];
}
}
for(int i=0;i<=cnt;++i)
ans+=dp[L][i][(1<<n)-1];
}
int check(int u,int v)//检查u是否是某个字符串的子串
{
if(len[u]>len[v]) return 0;
else if(len[u]==len[v])
{
for(int i=1;i<=len[u];++i)
if(pt[u][i]!=pt[v][i]) return 0;
return 1;
}
else
{
for(int i=1;i<=len[v]-len[u]+1;++i)
{
int judge=1;
for(int j=1;j<=len[u];++j)
if(pt[u][j]!=pt[v][i+j-1]){ judge=0; break;}
if(judge) return -1;
}
}
}
int calc(int u,int v)//计算u的前缀与v的后缀的最大匹配长度
{
for(int i=max(1,len[v]-len[u]+1);i<=len[v];++i)
{
int judge=1;
for(int j=1;i+j-1<=len[v];++j)
if(pt[u][j]!=pt[v][i+j-1]){ judge=0; break;}
if(judge) return len[v]-i+1;
}
return 0;
}
string qans()
{
string tt=pt[pm[1]]+1;
for(int i=2;i<=n;++i)
{
if(cov[pm[i]][pm[i-1]]==0) tt+=pt[pm[i]]+1;
else tt+=string(pt[pm[i]]+1,cov[pm[i]][pm[i-1]],len[pm[i]]);
}
return tt;
}
void work()
{
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
if(i!=j) cov[i][j]=calc(i,j);
int tt=0;
for(int i=1;i<=n;++i) pm[i]=i;
do{
res[++tt]=qans();
if(res[tt].length()!=L) tt--;
}while(next_permutation(pm+1,pm+1+n));
sort(res+1,res+1+ans);
for(int i=1;i<=ans;++i)
cout<<res[i]<<endl;
}
int main()
{
L=read();n=read();
for(int i=1;i<=n;++i)
{
scanf("%s",pt[i]+1);
len[i]=strlen(pt[i]+1);
}
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
{
if(i==j) continue;
int tt=check(i,j);
if(tt==-1||(tt==1&&i>j)) sub[i]=1;
}
int tt=0;
for(int i=1;i<=n;++i)
{
if(sub[i]) continue;
strcpy(pt[++tt]+1,pt[i]+1);//筛掉作为其他字符串的子串出现的字符串
len[tt]=len[i];
ins(pt[tt]+1,len[tt],tt);
}
n=tt;
ACM(); DP();
printf("%lld\n",ans);
if(ans<=42) work();
return 0;
}