题目大意:
白书练习题
就是给了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;
}