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