UVALive 4126 (LA 4126) Password Suspects AC自动机 + DP + 剪枝dfs

题目大意:

白书练习题

就是给了M(0 <= M <= 10)个长度不超过10的串, 现在问长度为N(N <= 25)的包含所有这M个串作为子串的串有多少种

如果种数<=42按字典序输出所有解


大致思路:

首先吐槽一下看了网上某份题解半小时后发现他是错的....

后来搞清楚了自己哪里错了...递归的时候出了输出所有结果的问题,因为记忆化使得部分结果没有被输出

所有细节都记录在代码注释里了...大家看代码吧


代码如下:

Result  :  Accepted     Memory  :  ? KB     Time  :  568 ms

/*
 * Author: Gatevin
 * Created Time:  2015/2/13 18:40:08
 * File Name: Mononobe_Mitsuki.cpp
 */
#include<iostream>
#include<sstream>
#include<fstream>
#include<vector>
#include<list>
#include<deque>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<bitset>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cctype>
#include<cmath>
#include<ctime>
#include<iomanip>
using namespace std;
const double eps(1e-8);
typedef long long lint;

/*
 * 这道题是个很好的AC自动机上的DP题, 不愧是World Final试题Orz
 * 首先如果只要求计数的话是个很简单的AC自动机上的状压DP
 * 但是对于方案数<=42的要求输出所有方案
 * 这就有点技巧了...刚开始我dfs边计数便输出找到的串
 * 但是一边记忆化一边输出犯了一个致命的错误, 方案输出不完整
 * 例如数据 4 4 a s d f计数是24但只输出了12中方案(被记忆化略去了)
 * 后来才先计数然后利用记忆化的数组重新递归剪枝寻找所有解
 * 因为最多42组解很多分支都不会搜索到, 大大提高效率
 * 像我刚开始直接四重循环计数的...dfs就坑了好久在输出所有方案(一直以为一次就够了..)
 * 另外给出2组易错数据:
 * Input: 4 2
 *        asd
 *        sd
 *        4 4
 *        a s d f
 * Output: Case 1: 52 suspects
 *         Case 2: 24 suspects
 *         adfs
 *         ...
 *         sfda(一共24个排列)
 */
int N, M, cas;
struct Trie
{
    int next[110][26], fail[110], end[110];
    int L, root;
    lint dp[26][110][1025];
    lint vis[26][110][1025];
    char ss[30];
    int newnode()
    {
        for(int i = 0; i < 26; i++)
            next[L][i] = -1;
        end[L++] = 0;
        return L - 1;
    }
    void init()
    {
        L = 0;
        root = newnode();
        return;
    }
    void insert(char *s, int id)
    {
        int now = root;
        for(; *s; s++)
        {
            if(next[now][*s - 'a'] == -1)
                next[now][*s - 'a'] = newnode();
            now = next[now][*s - 'a'];
        }
        end[now] |= (1 << id);
        return;
    }
    void build()//建立状态转移图
    {
        fail[root] = root;
        queue <int> Q;
        Q.push(root);
        while(!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            end[now] |= end[fail[now]];
            for(int i = 0; i < 26; i++)
                if(next[now][i] == -1)
                    next[now][i] = now == root ? root : next[fail[now]][i];
                else
                {
                    fail[next[now][i]] = now == root ? root : next[fail[now]][i];
                    Q.push(next[now][i]);
                }
        }
        return;
    }
    lint dfs(int len, int pos, int buf)//其实这也使一种DP计数..每次确定当前位向下记忆化搜索
    {
        if(vis[len][pos][buf] != -1) return vis[len][pos][buf];
        if(len == N && buf == (1 << M) - 1) return vis[len][pos][buf] = 1;
        if(len >= N) return vis[len][pos][buf] = 0;
        vis[len][pos][buf] = 0;
        for(int i = 0; i < 26; i++)
            vis[len][pos][buf] += dfs(len + 1, next[pos][i], buf | end[next[pos][i]]);
        return vis[len][pos][buf];
    }
    void print(int len, int pos, int buf)
    {
        if(len == N)//这里buf一定等于(1 << M) - 1
        {
            ss[len] = '\0';
            printf("%s\n", ss);
            return;
        }
        for(int i = 0; i < 26; i++)//利用dfs求得的vis数组剪枝
            if(vis[len + 1][next[pos][i]][buf | end[next[pos][i]]])//再往下有解才继续
                ss[len] = i + 'a', print(len + 1, next[pos][i], buf | end[next[pos][i]]);
    }
    /*
     * 用dp[i][j][k]表示当前长度为i,位于AC自动机上点j, 包含个子串的状态为k(已状压)的串不同个数
     * 显然有如下的四重循环的转移式(只用来计数了)
     */
    void solve()
    {
        memset(dp, 0, sizeof(dp));
        dp[0][0][0] = 1;
        for(int i = 0; i < N; i++)//如果只需要计数的话这四重循环就够了
            for(int j = 0; j < L; j++)
                for(int k = 0; k < (1 << M); k++)
                    for(int t = 0; t < 26; t++)
                        dp[i + 1][next[j][t]][k | end[next[j][t]]] += dp[i][j][k];
        lint ans = 0;
        for(int i = 0; i < L; i++)
            ans += dp[N][i][(1 << M) - 1];
        /*
         * 上面的计数可以去掉用ans = dfs(0, root, 0)代替
         * 之所以写出来是这种dp写法用来计数更方便
         */
        printf("Case %d: %lld suspects\n", cas, ans);
        if(ans <= 42LL)//需要找出所有解
        {
            memset(vis, -1, sizeof(vis));
            vis[0][0][0] = dfs(0, root, 0);//为了方便先用dfs求得各个子状态下再往后的可以有的解数
            /* 
             * 其实这个vis[0][0][0]也就是ans, 上面的循环dp只是刚开始想到的方法
             * 但是那个方法不方便于输出所有解, 还是需要dfs扫一下方便接下来的print递归剪枝
             */
            print(0, root, 0);//因为之多只有42个解, 所以递归求解
        }
        return;
    }
};

Trie AC;
char in[12];

int main()
{
    cas = 0;
    while(scanf("%d %d", &N, &M), N || M)
    {
        cas++;
        AC.init();
        for(int i = 0; i < M; i++)
            scanf("%s", in), AC.insert(in, i);
        AC.build();
        AC.solve();
    }
    return 0;
}


你可能感兴趣的:(password,uvalive,AC自动机,la,Suspects,DFS剪枝,4126,4126)