【hdu3247-Resource Archiver】位压DP+AC自动机+SPFA

时间:2021-10-03 10:07:57

题意:给定n个文本串,m个病毒串,文本串重叠部分可以合并,但合并后不能含有病毒串,问所有文本串合并后最短多长。

(2 <= n <= 10, 1 <= m <= 1000)

题解:

首先可以想出一个简单的位压DP : d[s][i] = min(d[ss][j] - 合并i、j的重叠部分长度)

问题就集中在了如何求出两个串x、y合并后的最短长度并且合并后不能包含病毒串。

引用一个题解:来自http://blog.csdn.net/woshi250hua/article/details/8021283

解题思路:综合题,需要用到AC自动机+状态压缩DP+Spfa。

总体思路是利用代码串和病毒串建立自动机,他们在自动机上的差别是一个末节点标记,一个不标记。然后将每个代码串尾节点看做图上的一个节点,利用自动机计算每个串其他所有串的不重叠的最短长度即两两节点间的最短距离。最后转变成TSP问题,状态压缩DP解之。

第一眼看到那个n,小等于10,soga,状态压缩,稍微思考下就能将问题转换成这样一个模型:n个串必须都选且选一次,求这n个串的排列使得组合成的串不包含病毒串并且长度最小。啊哈,这不是TSP问题吗?是的,你没有看错,转换成了TSP问题。

转换成TSP问题之后,我们想的是怎么让长度尽量小,考虑将两个代码串重叠起来。两个代码串a,b的前缀和后缀可能相等,他们组成的最短不包含病毒串的字符串c,前面部分为a,后面部分为b,这时候再来个d代码串要和前面两个合体,那么就成c和d的重叠问题了。

接下来我们要做的怎么让代码串a和代码串b组成的串c长度最小且不包含病毒串呢?我一开始用kmp来找两个串的相等前缀、后缀,然后组成串去ac自动机中匹配。然后一瞬间我就觉得我自己脑残了,这不是让ac自动机退化成kmp和字典树了吗!因为ac自动机上的一个节点到根的路径代表一个字符串,假设串a的末尾节是p,b的末尾节点是q,接着我们要做是在p点利用next数组转移到q,我们得到一个结论:从p到q所走的路径便是b除开与a重叠部分的那个后缀,如a为aaabb,b为bbaaa,那么路经就代表串b的aaa子串。我们怎么保证从p点走到q点,中间走过的路径表示的串一定是串b的后缀呢?两种情况:1、a是b的子串,这时候我们不会用到fail指针,显然可以 2、我们需要用到fail指针,每次用fail指针找到下一个匹配的位置假设是failx,failx节点到根节点所表示的串便是我们走过路径的最长后缀,这样一直找找到节点q,点q到根节点所表示的串遍是我们走过路径的最长后缀,然后上面的结论便得证。

总而言之,我们在ac自动机上走过的路径可以表示一个串,设为S,到达点p,那么点p到根节点这条路径所表示的串s,s为S的后缀。为用路径代表一个串是ac自动机优美之处。

我们从上面说的p点走到q点会有很多路径,要保证走过的路径长度最小即b串于a串的不重叠部分最短,要用到spfa,其实本题就退化成普通的Bfs,因为没有松弛操作。这样得到就可以得到各串相互之间的最短距离,然后就变成了很普通的TSP。

关键就在于:将文本串和病毒串建在同一个自动机上,然后从一个文本串i的末尾节点x走到另一个文本串j的末尾节点y,只能顺着next走,中途不经过任何病毒末端节点,并且路径最短。

从x走到y,这就相当于保证了文本串i、j必然存在于新构造出来的字符串中(也就是路径)。

走的时候路径上不是可能有不是i、j的字符串吗?

是的!会有可能走到其他的串,然后通过next走到了根节点,然后就相当于不加限制地走到其他串中了。

但是这样只会比最优解更长,答案根本不会取到它。

为什么是顺着next走呢?

我的理解是这样的:通过AC自动机,如果一个点没有相应的0孩子或1孩子,已经在求fail的时候把fail所对应的孩子当成是它自己的孩子了。

也就是说,它走到另一个串的前提是自己没有这个孩子,而fail有。

这样走就相当于跳过了两个串的公共部分,走到了另一个串。

我打的时候WA了一次,错在了我让它可以顺着fail到达另一个串(即使它自己本身也有这个孩子)。

这样的错误证明了只能通过next走,因为通过next走保证了它走过的不是病毒串(因为病毒末端我们不走),如果它现在是1,末端是0(病毒末端),通过fail到了串101,则走过的101包含了自己本来的病毒末端。也就是说,这样做保证了走只能走完一个串(除非要走的点这个串没有,那就跳到了另一个串)。

 #include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std; const int N=,M=,S=,INF=(int)1e9;
int n,m,num,com[N][N],last[N],len[N],dis[S],d[][N];
bool in[S];
char s[S];
struct node{
int fail,son[];
bool bk;
}a[S];
queue<int> q; int minn(int x,int y){return x<y ? x:y;}
void clear(int x)
{
a[x].bk=a[x].fail=a[x].son[]=a[x].son[]=;
} int sum=;
void trie(char *c,bool bk,int id)
{
int x=,l=strlen(c);
sum+=l;
for(int i=;i<l;i++)
{
int ind=c[i]-'';
if(!a[x].son[ind])
{
++num;
clear(num);
a[x].son[ind]=num;
}
x=a[x].son[ind];
}
a[x].bk=bk;
if(!bk) last[id]=x,len[id]=l;
} void buildAC()
{
while(!q.empty()) q.pop();
q.push();
while(!q.empty())
{
int x=q.front();q.pop();
int fail=a[x].fail;
for(int i=;i<=;i++)
{
if(a[x].son[i])
{
a[a[x].son[i]].fail=x ? a[fail].son[i] : ;
q.push(a[x].son[i]);
}
else a[x].son[i]=a[fail].son[i];
}
}
} void spfa(int u)
{
while(!q.empty()) q.pop();
memset(dis,,sizeof(dis));
memset(in,,sizeof(in));
int st=last[u];
q.push(st);
dis[st]=;in[st]=;
while(!q.empty())
{
int x=q.front();in[x]=;q.pop();
for(int i=;i<=;i++)
{
int y=a[x].son[i];
if(a[y].bk== && dis[y]>dis[x]+)
{
dis[y]=dis[x]+;
if(!in[y]) in[y]=,q.push(y);
}
}
//有下面这段是错的,要保证走就走完一个串,除非要走的节点这个串没有,否则就可能路径上有病毒末端。
// int fail=a[x].fail;
// if(a[fail].bk==0 && dis[fail]>dis[x])
// {
// dis[fail]=dis[x];
// if(!in[fail]) in[fail]=1,q.push(fail);
// }
}
for(int i=;i<n;i++)
com[u][i]=len[i]-dis[last[i]];
} int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
while()
{
scanf("%d%d",&n,&m);
if(!n && !m) return ;
num=;
clear();
for(int i=;i<n;i++)
{
scanf("%s",s);
trie(s,,i);
}
for(int i=;i<=m;i++)
{
scanf("%s",s);
trie(s,,i);
}
buildAC();
for(int i=;i<n;i++) spfa(i);
int ans=INF;
memset(d,,sizeof(d));
for(int i=;i<n;i++) d[(<<i)][i]=len[i];
for(int s=;s<(<<n);s++)
{
for(int i=;i<n;i++) if((<<i)&s)
{
for(int j=;j<n;j++) if(!((<<j)&s))
{
d[s+(<<j)][j]=minn(d[s+(<<j)][j],d[s][i]+len[j]-com[i][j]);
}
if(s==(<<n)-) ans=minn(ans,d[s][i]);
}
}
printf("%d\n",ans);
}
return ;
}