题目大意:
现在有n个(2 <= n <= 10)个需要合并起来的文件串,然后又m个( 1 <= m <= 1000)个病毒串(病毒串的总长度不超过5000),两类串都只包含二进制的0和1,现在要求找到一个最短的串,使得这个串中包含所有的文件串并且不包含病毒串。
样例:
2 2
1110
0111
101
1001
0 0
输出为5,因为最短的串是01110,不包含病毒串
大致思路:
首先将所有的文件串和病毒串都加入到Trie树建立状态转移图,end[ i ]表示编号为i的节点时病毒结尾还是文件结尾还是不是结尾,如果是病毒结尾或者fail[i]指针指向的点的end是-1,则此点也是-1,表示不可走。
所有文件结尾的点用end数组的状压来标记是包含哪几个文件的结尾点。
那么首先从根节点和所有文件结尾点用bfs来得到根节点和文件节点(end > 0)也可能是多个文件的结尾)两两之间的距离。
那么就是一个简单的状压dp了,起始点是根节点,知道到达每一个点可以得到什么样的状态,花费是多少,就是一个常规的状压,状态转移方程见代码
代码如下:
Result : Accepted Memory : 1592 KB Time : 46 ms
/*
* Author: Gatevin
* Created Time: 2014/11/24 18:43:03
* File Name: Kagome.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;
const int inf = 0x3f3f3f3f;
int n, m;
char ts[50010];
/*
* 所有病毒(共1000个的总长度不超过50000按道理来说一个病毒串可以长达49000
* 但是看kuangbin的代码只开了1010来读病毒串也过了...
* 数据弱了么..
*/
struct Trie
{
int next[60010][2], fail[60010], end[60010];
int L, root;
int dis[60010];
int dp[1025][11];//11已经够了,第二位表示end为正的数量不会超过n(从Trie树的性质上不难发现),虽然的确纠结了一下
int mat[11][11];//表示需要到达的点之间的距离,值为-1时时不可到达
int pos[11];
int cnt;
int newnode()
{
for(int i = 0; i < 2; 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 - '0'] == -1)
next[now][*s - '0'] = newnode();
now = next[now][*s - '0'];
}
if(id == -1 || end[now] == -1) end[now] = -1;
else end[now] |= (1 << id);
}
void build()
{
queue <int> Q;
Q.push(root);
fail[root] = root;
while(!Q.empty())
{
int now = Q.front();
Q.pop();
if(end[fail[now]] < 0) end[now] = -1;
else end[now] |= end[fail[now]];//end数组已经反应了不同文件之间的包含关系
for(int i = 0; i < 2; 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;
}
void bfs(int id)
{
queue <int> Q;
Q.push(pos[id]);
memset(dis, -1, sizeof(dis));
dis[pos[id]] = 0;
while(!Q.empty())
{
int now = Q.front();
Q.pop();
/*
int tmp = fail[now];
while(tmp != root)
{
dis[tmp] = dis[now];
Q.push(tmp);
tmp = fail[tmp];
}
这一段没有必要,因为不需要记录fail指针的节点的关系距离为零,这一信息在end数组中已经反映出来了
加上这一段无用功反而会超时
*/
for(int i = 0; i < 2; i++)
{
if(end[next[now][i]] >= 0 && dis[next[now][i]] < 0)
{
dis[next[now][i]] = dis[now] + 1;
Q.push(next[now][i]);
}
}
}
for(int i = 0; i < cnt; i++)
mat[id][i] = dis[pos[i]];
return;
}
void solve()
{
pos[0] = 0;//根节点为起点,也需要加入计算最短距离
cnt = 1;
for(int i = 0; i < L; i++)
if(end[i] > 0) pos[cnt++] = i;
for(int i = 0; i < cnt; i++)
bfs(i);
for(int i = 0; i < (1 << n); i++)
for(int j = 0; j < cnt; j++)
dp[i][j] = inf;
dp[0][0] = 0;
for(int i = 0; i < (1 << n); i++)
for(int j = 0; j < cnt; j++)
if(dp[i][j] < inf)
{
for(int k = 0; k < cnt; k++)
{
if(mat[j][k] < 0) continue;//不可从j到达k
dp[i | end[pos[k]]][k] = min(dp[i | end[pos[k]]][k], dp[i][j] + mat[j][k]);
//常见的状态压缩dp
}
}
int ans = inf;
for(int i = 0; i < cnt; i++)
ans = min(ans, dp[(1 << n) - 1][i]);
printf("%d\n", ans);
return;
}
};
Trie AC;
int main()
{
while(scanf("%d %d", &n, &m), n | m)
{
AC.init();
for(int i = 0; i < n; i++)
{
scanf("%s", ts);
AC.insert(ts, i);
}
for(int i = 0; i < m; i++)
{
scanf("%s", ts);
AC.insert(ts, -1);
}
AC.build();
AC.solve();
}
return 0;
}