转自:http://www.zlinkin.com/?p=63
图论是ACM竞赛中比较重要的组成部分,其模型广泛存在于现实生活之中。因其表述形象生动,思维方式抽象而不离具体,因此深受各类喜欢使劲YY的Acmer的喜爱。这篇文章引述图论中有关有向图最小生成树的部分,具体介绍朱刘算法的求解思路,并结合一系列coding技巧,实现最小树型图O(VE)的算法,并在最后提供了该算法的模版,以供参考。
关于最小生成树的概念,想必已然家喻户晓。给定一个连通图,要求得到一个包含所有顶点的树(原图的子图),使之所构成的边权值之和最小。在无向图中,由于边的无向性质,所以求解变得十分容易,使用经典的kruskal与prim算法已经足够解决这类问题。而在有向图中,则定义要稍微加上一点约束,即以根为起点,沿给定有向边,可以访问到所有的点,并使所构成的边权值之和最小,于是问题开始变得不一样了,我们称之为最小树型图问题。
该问题是由朱永津与刘振宏在上个世纪60年代解决的,值得一提的是,这2个人现在仍然健在,更另人敬佩的是,朱永津几乎可以说是一位盲人。解决最小树型图的算法后来被称作朱刘算法,也是当之无愧的。
首先我们来阐述下算法的流程:算法一开始先判断从固定根开始是否可达所有原图中的点,若不可,则一定不存在最小树形图。这一步是一个很随便的搜索,写多搓都行,不加废话。第二步,遍历所有的边,从中找出除根结点外各点的最小入边,累加权值,构成新图。接着判断该图是否存在环。若不存在,则该图便是所求最小树型图,当前权为最小权。否则对环缩点,然后回到第二步继续判断。
这里存在一系列细节上的实现问题,以确保能够达到VE的复杂度。首先是查环,对于新图来说只有n-1条入边,对于各条入边,其指向的顶点是唯一的,于是我们可以在边表中添加from,表示该边的出发点,并考虑到如果存在环,则对环上所有边反向,环是显然同构的,于是最多作V次dfs就能在新图中找到所有的环,并能在递归返回时对顶点重标号进行缩点,此步的重标号可以用hash数组映射。然后就是重要的改边法,对于所有不在环上的边,修改其权为w-min(v),w为当前边权,min(v)为当前连接v点的最小边权。其数学意义在于选择当前边的同时便放弃了原来的最小入边。我们可以知道,每次迭代选边操作O(E),缩点操作O(V),更新权操作O(E),至少使一个顶点归入生成树,于是能在V-1步内完成计算,其复杂度为O(VE)。
以上为定根最小树型图,对于无固定根最小树型图,只要虚拟一个根连所有的点的权为边权总和+1,最后的结果减去(边权+1)即可。另外对于求所定的根标号,只要搜索被选中的虚边就可以判断了。
以下为代码上的一些实现:
#include <iostream>
using namespace std;
const int MAXN=1010;
const int MAXM=10010;
typedef struct{int v,next,rt,cost,bt,bv;}edge;
int N,M,eid;
int rt,w;//不定根
int p[MAXN],In[MAXN],ind[MAXN],hash[MAXN];;
edge e[MAXM];
bool vist[MAXN];
int clen;
inline void init(){eid=0;memset(p,-1,sizeof(p));}
inline void insert(int from , int to , int cost)
{
if (from==to) return;
e[eid].next=p[from];
e[eid].v=to;
e[eid].rt=from;
e[eid].bv=to;
e[eid].bt=from;
e[eid].cost=cost;
p[from]=eid++;
}
// 定根
// inline void dfs(int pos)
// {
// int j;
// for (j=p[pos];j!=-1;j=e[j].next)
// {
// if(!vist[e[j].v])
// {
// vist[e[j].v]=true;
// dfs(e[j].v);
// }
// }
// }
int Cnt;
int st;
inline bool c_dfs(int pos)
{
int x=ind[pos];
if (x==-1)
{
return false;
}
int v=e[x].rt;
if (!vist[v])
{
vist[v]=true;
if(c_dfs(v))
{
hash[v]=Cnt;
return true;
}
vist[v]=false;
}
else
{
return st==v;
}
return false;
}
int fit;
inline int Rtree()
{
int i;
//以下为不定根
++w;
rt=N;
for (i=0;i<N;++i) insert(rt,i,w);
++N;
///
//定根专用
//rt=0
//memset(vist,0,sizeof(vist));
//vist[rt]=true;
//dfs(rt);
//for (i=0;i<N;++i) if(!vist[i]) {return -1;}
///
int res=0;
for (i=0;i<N;++i) hash[i]=i;
bool flag=true;
int bbt=rt;
int ct;
fit=INT_MAX;
while(1)
{
ct=clen=Cnt=0;
for (i=0;i<N;++i) In[i]=INT_MAX,ind[i]=-1;
//找最小边
for (i=0;i<eid;++i)
{
int ff=e[i].rt;
int tt=e[i].v;
if (ff==tt)
{
continue;
}
if (e[i].cost<In[tt])
{
In[tt]=e[i].cost;
ind[tt]=i;
}
}
In[rt]=0;
ind[rt]=-1;
for (i=0;i<N;++i)
{
int x=ind[i];
if(x==-1) continue;
if(e[x].bt==bbt)
{
++ct;
if(e[x].bv<fit) fit=e[x].bv;
}
}
memset(vist,0,sizeof(vist));
//找环
for (i=0;i<N;++i)
{
if(!vist[i])
{
vist[i]=true;
hash[i]=Cnt;
st=i;
if(c_dfs(i))
{
++clen;
++Cnt;
continue;
}
vist[i]=false;
++Cnt;
}
}
for (i=0;i<N;++i)
{
res+=In[i];
}
if (clen==0)
{
break;
}
//缩点并改边
for (i=0;i<eid;++i)
{
int ff,tt;
ff=e[i].rt;
tt=e[i].v;
e[i].rt=hash[e[i].rt];
e[i].v=hash[e[i].v];
if (e[i].rt!=e[i].v)
{
e[i].cost-=In[tt];
}
}
N=Cnt;
rt=hash[rt];
}
if(ct!=1) return -1;
return res-w;
}