洛谷P4045 [JSOI2009]密码【AC自动机+状压DP】

时空限制 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 &lt; = 10 n&lt;=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;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值