题意
众所周知,密码在信息领域起到了不可估量的作用。对于普通的登陆口令,唯一的破解 方法就是暴力破解一逐个尝试所有可能的字母组合,但这是一项很耗时又容易被发现的工 作。所以,为了获取对方的登陆口令,在暴力破解密码之前,必须先做大量的准备工作。经 过情报的搜集,现在得到了若干有用信息,形如:
“我观察到,密码中含有字符串***。”
例如,对于一个10位的密码以及观察到的字符串hello与world,可能的密码组合为 helloworld与worldhello;而对于6位的密码以及观察到的字符串good与day,可能的 密码组合为gooday。
有了这些信息,就能够大大地减少尝试的次数了。请编一个程序,计算所有密码组合的 可能。密码中仅可能包含a - z之间的小写字母。
对于100%的数据,1<=L<=25,1<=N<=10,每个观察到的字符串长不超过10,并且保 证输出结果小于2^63。
分析
参照LadyLex的题解。
这道题是我今天刷到的比较好的一道题……不仅考到了dp,还考到了搜索基本功。
我们首先考虑:对于串\(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的逆过程,看合法方案由哪个节点转移过来,并且记录一路上经过的字符,最后排序输出即可。
这真是一道很不错的题目,方式以及套路很经典,对于状压和搜索的应用都很灵活!
时间复杂度\(O(26 L N^2 2^N)\),算出来是7e7。
代码
#include<bits/stdc++.h>
#define rg register
#define il inline
#define co const
template<class T>il T read()
{
rg T data=0;
rg int w=1;
rg char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-')
w=-1;
ch=getchar();
}
while(isdigit(ch))
{
data=data*10+ch-'0';
ch=getchar();
}
return data*w;
}
template<class T>il T read(rg T&x)
{
return x=read<T>();
}
typedef long long ll;
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\n",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()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
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;
}