最小树形图——朱刘算法学习小记
参考资料:
https://www.cnblogs.com/hdu-zsk/p/8167687.html
https://www.luogu.com.cn/blog/xiaojiji/solution-p4716
一张无向图,你要用边权和最小的边来使所有点联通,显然用最小生成图。
现在的问题是,给你一张有向图并且钦定一个起点\(r\),你要选边权和最小的边使\(r\)可以到达任意点。
这就是最小树形图问题。
上面的第一篇博客有演示过程,讲得比较详细。所以这里就随便胡一下:
- 对于\(r\)之外的每个点\(x\),找到连向\(x\)的边权最小的边(自环除外),记为\(mn_x\)。
- 如果存在点\(x\)满足没有这样的边连向它,那么它被孤立了,不可能有解。
- 将这些边都选出来,如果没有环,意味着最小树形图已经被找出来了;否则进入下一步的缩环操作。
- 对于每个在环中的点\(x\),连向\(x\)的边中,如果起点不在这个环内,则用其边权减去连向\(mn_x\)的边权。然后将整个环缩成一个点。
- 对于每个点\(x\),将\(mn_x\)的权值加入答案。
至于如何求方案:把上面的那个做法改成递归,回溯的时候,现在求出了下一层的最小树形图。下一层最小树形图中的边也包含在这一层中。把下一层最小树形图中的边在这一层中实际连向的点做标记,把这一层的所有\(mn_x\)对应的边(如果\(x\)没有被标记的话)加入最小树形图中。
这还是挺容易感性理解的。
边权减去\(mn_x\)的边权,因为\(mn_x\)的权值已经加入答案了。如果后面要选择这条边,就应该要替换\(mn_x\)。所以要用其边权减去\(mn_x\)的边权。
这样时间复杂度是\(O(nm)\),因为每轮至少会少一个连通块。
模板
这题是完整的板子(输出方案):https://www.cnblogs.com/jz-597/p/14398155.html
洛谷和LOJ上都有板题。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 110
#define M 10010
#define INF 1000000000
int n,m,r;
struct edge{int u,v,w;} ed[M];
int pre[N],mn[N];
int id[N],cnt;
int vis[N];
int zhu_liu(){
int res=0;
while (1){
for (int i=1;i<=n;++i)
mn[i]=INF;
for (int i=1;i<=m;++i){
int u=ed[i].u,v=ed[i].v,w=ed[i].w;
if (u!=v && w<mn[v])
mn[v]=w,pre[v]=u;
}
for (int i=1;i<=n;++i)
if (i!=r && mn[i]==INF)
return -1;
memset(vis,0,sizeof(int)*(n+1));
memset(id,0,sizeof(int)*(n+1));
cnt=0;
// for (int i=1;i<=n;++i){
// if (i==r)
// continue;
// res+=mn[i];
// int x=i;
// for (;vis[x]!=i && !id[x] && x!=r;x=pre[x])
// vis[x]=i;
// if (x!=r && !id[x]){
// id[x]=++cnt;
// for (int y=pre[x];y!=x;y=pre[y])
// id[y]=cnt;
// }
// }
for (int i=1;i<=n;++i){
if (i==r)
continue;
res+=mn[i];
if (vis[i])
continue;
int x=i;
for (;vis[x]==0 && !id[x] && x!=r;x=pre[x])
vis[x]=i;
if (x!=r && !id[x] && vis[x]==i){
id[x]=++cnt;
for (int y=pre[x];y!=x;y=pre[y])
id[y]=cnt;
}
}
if (cnt==0)
break;
for (int i=1;i<=n;++i)
if (!id[i])
id[i]=++cnt;
for (int i=1;i<=m;++i){
int u=ed[i].u,v=ed[i].v,w=ed[i].w;
ed[i]={id[u],id[v],w-(id[u]!=id[v]?mn[v]:0)};
}
n=cnt;
r=id[r];
}
return res;
}
int main(){
freopen("in.txt","r",stdin);
scanf("%d%d%d",&n,&m,&r);
for (int i=1;i<=m;++i){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
ed[i]=(edge){u,v,w};
}
printf("%d\n",zhu_liu());
return 0;
}
upd2021.3.6:模板中有一处错误纠正了,原版本见注释。这个错误可能会导致时间\(O(n^3)\)。
Acceleration
上面第二篇博客有介绍,这里复述一下:
换一下写朱刘算法的姿势:
枚举点\(x\),找与\(x\)相连的最小边\(mn_x\),将其加进来。如果这个时候出现了环,就像上面那样缩环并且修改外面连向环中的点的边的边权,重复操作。
考虑优化这个过程,给每个点开个左偏树,左偏树中存所有连向这个点的边。左偏树支持合并,并且这里还要支持对整个左偏树进行整体减。缩环的时候将环上所有点的左偏树进行整体减,然后合并到一起。
另外用并查集来维护每个具体的点被缩到了哪个点,还要用并查集来查是否出现环。详见代码。
时间复杂度变成了\(O((n+m)\lg m)\)
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 110
#define M 1010
#define INF 1000000000
int n,m,r;
struct Node* null;
struct Node{
Node *l,*r;
int d;
int u,w,tag;
void gt(int c){w+=c,tag+=c;}
void pd(){l->gt(tag),r->gt(tag),tag=0;};
};
Node *newnode(int u,int w){
Node *nw=new Node;
*nw={null,null,1,u,w,0};
return nw;
}
Node *merge(Node *a,Node *b){
if (a==null) return b;
if (b==null) return a;
if (a->w>b->w) swap(a,b);
a->pd();
a->r=merge(a->r,b);
if (a->l->d<a->r->d)
swap(a->l,a->r);
a->d=a->r->d+1;
return a;
}
void pop(Node *&t){
if (t==null) return;
Node *tmp=merge(t->l,t->r);
delete t;
t=tmp;
}
Node *in[N];
int fa[N],bel[N];
int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
int getbel(int x){return bel[x]==x?x:bel[x]=getbel(bel[x]);}
int pre[N],mn[N];
int ZLA(){
int res=0;
for (int i=1;i<=n;++i)
bel[i]=i,fa[i]=i;
for (int i=1;i<=n;++i)
if (i!=r){
int x=getbel(i);
while (1){
while (in[x]!=null && getbel(in[x]->u)==x)
pop(in[x]);
if (in[x]==null)
return -1;
pre[x]=getbel(in[x]->u);
res+=mn[x]=in[x]->w
int y=pre[x];
if (x!=getfa(y)){
fa[x]=getfa(y);
break;
}
in[x]->gt(-mn[x]);
for (;y!=x;y=getbel(pre[y])){
bel[y]=x;
in[y]->gt(-mn[y]);
in[x]=merge(in[x],in[y]);
}
}
}
return res;
}
int main(){
// freopen("in.txt","r",stdin);
scanf("%d%d%d",&n,&m,&r);
null=new Node;
*null={null,null,0,0,0,0};
for (int i=1;i<=n;++i)
in[i]=null;
for (int i=1;i<=m;++i){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
in[v]=merge(in[v],newnode(u,w));
}
printf("%d\n",ZLA());
return 0;
}