最小树形图——朱刘算法学习小记

参考资料:
https://www.cnblogs.com/hdu-zsk/p/8167687.html
https://www.luogu.com.cn/blog/xiaojiji/solution-p4716


一张无向图,你要用边权和最小的边来使所有点联通,显然用最小生成图。
现在的问题是,给你一张有向图并且钦定一个起点 r r r,你要选边权和最小的边使 r r r可以到达任意点。
这就是最小树形图问题。


上面的第一篇博客有演示过程,讲得比较详细。所以这里就随便胡一下:

  1. 对于 r r r之外的每个点 x x x,找到连向 x x x的边权最小的边(自环除外),记为 m n x mn_x mnx
  2. 如果存在点 x x x满足没有这样的边连向它,那么它被孤立了,不可能有解。
  3. 将这些边都选出来,如果没有环,意味着最小树形图已经被找出来了;否则进入下一步的缩环操作。
  4. 对于每个在环中的点 x x x,连向 x x x的边中,如果起点不在这个环内,则用其边权减去连向 m n x mn_x mnx的边权。然后将整个环缩成一个点。
  5. 对于每个点 x x x,将 m n x mn_x mnx的权值加入答案。

一直循环操作直到退出。
如果要得出具体的边是什么,稍微处理一下 m n x mn_x mnx应该就可以了(设连向 x x x的边记为 e f x ef_x efx。在处理 m n x mn_x mnx的时候,由于 x x x可能是被缩过的,找到 m n x mn_x mnx连向的具体的点 y y y,然后覆盖 e f y ef_y efy。)

这还是挺容易感性理解的。
边权减去 m n x mn_x mnx的边权,因为 m n x mn_x mnx的权值已经加入答案了。如果后面要选择这条边,就应该要替换 m n x mn_x mnx。所以要用其边权减去 m n x mn_x mnx的边权。

这样时间复杂度是 O ( n m ) O(nm) O(nm),因为每轮至少会少一个连通块。


模板

洛谷和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;
			}
		}
		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(){
	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;
}

Acceleration

上面第二篇博客有介绍,这里复述一下:
换一下写朱刘算法的姿势:
枚举点 x x x,找与 x x x相连的最小边 m n x mn_x mnx,将其加进来。如果这个时候出现了环,就像上面那样缩环并且修改外面连向环中的点的边的边权,重复操作。
考虑优化这个过程,给每个点开个左偏树,左偏树中存所有连向这个点的边。左偏树支持合并,并且这里还要支持对整个左偏树进行整体减。缩环的时候将环上所有点的左偏树进行整体减,然后合并到一起。
另外用并查集来维护每个具体的点被缩到了哪个点,还要用并查集来查是否出现环。详见代码。
时间复杂度变成了 O ( ( n + m ) lg ⁡ m ) O((n+m)\lg m) O((n+m)lgm)

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;
}
















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值