码迷,mamicode.com
首页 > 其他好文 > 详细

最小树形图

时间:2020-08-03 23:11:43      阅读:66      评论:0      收藏:0      [点我收藏+]

标签:复杂度   printf   puts   删除   朱刘算法   lin   生成   ==   左偏树   

最小树形图

定义对于带权有向图\(G=(V,E)\)对于根\(root\)最小树形图为以\(root\)为根的外向树最小边权和

有根树的树形图:朱刘算法

题目给定了\(root\)

朴素版朱刘算法

核心:

推论1:对于有向图上的一个点,对于它的所有入边加减一个权值,答案的树形图形态不变

因为所有非根点必然有一条入边,所以可以对于每个点,取入边边权最小值减去,把减去的部分加入答案

经过这样的操作使得每个点都有一条为0的入边

\[\ \]

推论2:对于有向图上边权为0的有向边生成的环,可以把环上的点收缩

因为无论最后得到的树形图如何连边,一定可以通过断掉环上的一条边来生成一个可行的树形图

算法流程

1.为每个点的入边更改边权

2.收缩0环

? 2.1 存在环 : 回到1

? 2.2 不存在环:结束算法

此时存在两种情况

1.图不连通,无解

2.图联通,每个点一定存在一条为0的入边,取出一个合法边集,

然后依次展开每个被收缩的0环,即可得到一个最小树形图方案

复杂度分析:

每次收缩环需要依次遍历,每次至少缩小一个点,因此复杂度上限为\(O(nm)\)

\[\ \]

Tips:

1.注意不要更改根的入边

2.0边构成的的图不连通

朴素的实现:tarjan强连通缩点

判断无解可以在每次调整边权时看是否有非根节点不存在入边,此时无解

const int N=10010,INF=1e9;

int n,m,rt,ans;
int U[N],V[N],W[N];
vector <int> G[N],E[N];
int id[N],T[N];

void Reweight(){
	rep(i,1,n) if(i!=rt && id[i]==i) {
		int w=INF;
		for(int j:E[i]) cmin(w,W[j]);
		if(w==INF) puts("-1"),exit(0);
		for(int j:E[i]) W[j]-=w;
		ans+=w;
	}
}

int t[N],low[N],ins[N],stk[N],top,dfn,flag;
void Union(int u){
	low[u]=t[u]=++dfn;
	ins[stk[++top]=u]=1;
	for(int i:G[u]) if(W[i]==0) {
		int v=V[i];
		if(!t[v]) Union(v),cmin(low[u],low[v]);
		else if(ins[v]) cmin(low[u],t[v]);
	}
	if(low[u]==t[u]) {
		int v;
		do {
			ins[v=stk[top--]]=0;
			id[v]=u;
			if(u!=v) flag=1;
		} while(u!=v);
	}
}

void ReBuild(){
	rep(i,1,n) G[i].clear(),E[i].clear();
	rt=id[rt];
	rep(i,1,m) {
		U[i]=id[U[i]],V[i]=id[V[i]];
		if(U[i]==V[i]) continue;
		G[U[i]].pb(i),E[V[i]].pb(i);
	}
}

int main(){
	n=rd(),m=rd(),rt=rd();
	rep(i,1,n) id[i]=i;
	rep(i,1,m) {
		U[i]=rd(),V[i]=rd(),W[i]=rd();
		G[U[i]].pb(i),E[V[i]].pb(i);
	}
	while(1) {
		Reweight();
		rep(i,1,n) t[i]=0;
		dfn=flag=0;
		rep(i,1,n) if(!t[i] && id[i]==i) Union(i);
		if(!flag) break;
		ReBuild();
	}
	Reweight();
	printf("%d\n",ans);
}

更好的实现:只记录一条0边,每次循环收缩环(居然细节挺多的。。)

const int N=10010,INF=1e9;

int n,m,rt,ans;
int U[N],V[N],W[N];
int id[N],inw[N],pre[N];

void Reweight(){
	rep(i,1,n) inw[i]=INF,pre[i]=0;
	rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) if(inw[V[i]]>W[i]) inw[V[i]]=W[i],pre[V[i]]=U[i];
	rep(i,1,n) if(i!=rt && id[i]==i) {
		 if(inw[i]==INF) puts("-1"),exit(0);
		 ans+=inw[i];
	}
	rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) W[i]-=inw[V[i]];
}

int vis[N];
int Union(){
	int fl=0;
	rep(i,1,n) vis[i]=0;
	rep(i,1,n) if(id[i]==i && !vis[i]) {
		int u=i;
		while(u && !vis[u]) vis[u]=i,u=pre[u];
		if(vis[u]==i) {
			int v=pre[u];
			fl=1;
			while(v!=u) id[v]=u,v=pre[v];
		}
	}
	return fl;
}

int main(){
	n=rd(),m=rd(),rt=rd();
	rep(i,1,n) id[i]=i;
	rep(i,1,m) U[i]=rd(),V[i]=rd(),W[i]=rd();
	while(1) {
		Reweight();
		if(!Union()) break;
		rep(i,1,m) U[i]=id[U[i]],V[i]=id[V[i]];
		rt=id[rt];
	}
	printf("%d\n",ans);
}

用可并堆优化朱刘算法

笔者没有写过这个东西的代码

涉及到的操作:

1.用可并堆维护合并点集入边的最小权值,并且支持全局减操作,单点删除操作(删除块内边)

2.用并查集维护判断是否出现了环,由于不能直接合并,需要用按秩合并来实现

比较常见的实现是左偏树,因为便于全局修改的标记下传操作,代码也比较好写

用可并堆维护朱刘算法的操作,单次合并操作为\(O(\log m)\),因此复杂度为\(O(n(\log m+\log n)+n\log n)\)

无根树的树形图

建立超级原点\(S\)\(V\)中的点连边权极大的边,以限制每次只选一条这样的边

单次得到答案后减去这个极大值即可,注意如果答案有两个这样的极大值,是无解的

最小树形图

标签:复杂度   printf   puts   删除   朱刘算法   lin   生成   ==   左偏树   

原文地址:https://www.cnblogs.com/chasedeath/p/13429457.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!