最小树形图/朱刘算法

最小树形图/朱刘算法

最小生成树是具有最小权值的无向树,最小树形图则是具有最小权值的有向树

算法思想和kruskal类似,也是采用贪心的思想,每次取用权值最小的那条边,只是由于有向图边权带有方向,有一些特殊情况需要我们去解决。

1.是否存在最小树形图;我们知道在无向图中(不考虑负权),只要所有点都是连通的,就一定存在最小生成树,在有向图中也是一样的,每个点都必须存在一条连向它的边,即不存在某个点,它的入度为0 ,如果有某个点入度为0,那就不存在树形图(不连通),更不存在最小树形图。

2.每次取权值最小的边,可能造成环,导致树形图不连通;如果存在一个环外的点 u 和环内的点 v 之间有一条边,假设存在u–>v的一条有向边是最优解(方向是不能反过来的,必须是环外指向环内),那么我们就只要拆除原来连向v的边,这样构成的树形图就是我们需要的。

那么现在的问题就在于如何破环 ,这里我们换一种思维,将破开环再换一条边的操作,换成缩点再选最小边的操作,将成环的部分缩成一个点,这样形成了一个新图,将这个新图当作原来的图做同样的处理。处理过程见代码

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1e5+5;
const int inf  = 0x4f4f4f4f;

struct edge {
	int u,v,w;
}e[maxn];
/*
	cnt:当前图点的数量
	id[i]:表示i结点属于当前图第id[i]个点
	vis[i]:表示i结点所在链的代表元素(类似并查集
	inv[i]:表示以i结点为入边的最短边权
	pre[i]连结到i结点的最短边的u
*/
int pre[maxn],inv[maxn];
int vis[maxn],id[maxn];
int n,m,root;
int zhuliu()
{
	int ans = 0;
	while(true)
	{
		for(int i = 1; i <= n; i++) inv[i] = inf;
		for(int i = 1; i <= m; i++)//贪心,寻找连向每一个点的最小边,
		{
			int u = e[i].u, v = e[i].v;
			if(u != v && e[i].w < inv[v])
				inv[v] =  e[i].w,pre[v] = u;//inv 记录边长,pre记录前置节点
		}
        
		for(int i = 1; i <= n; i++)
			if(i != root && inv[i] == inf) return -1;
        //如果inv==inf 说明该点的入度为0 ,不存在树形图
			
		int cnt = 0;
        //如果可能存在树形图,接下来判断是否存在环
        
		for(int i = 1; i <= n; i++)
			vis[i] = id[i] = 0;
            //vis记录i属于哪一个集合,id对i进行重新编号,请结合后面的代码进行理解
			
		for(int i = 1; i <= n; i++)//计算结果,同时找环
		{
			if(i == root) continue;
			ans += inv[i];	//记录答案
			int v = i;
			while(vis[v] != i && !id[v] && v != root) 
				vis[v] = i, v = pre[v];
            //这一段很值得品味,假如存在环,那么会有两种情况:1.由于vis[v]==i 而退出循环,2.!id[v]为false 代表这个环已经处理过了,处理过程见下面的代码;假如不存在环,那么最后会由于v == root 而退出循环
            
			if(!id[v] && v != root)
                //假如v== root,表示不存在环,如果!id[v]==false说明id[v]所在的环已经处理过了
			{
				id[v] = ++cnt; //把环内的点标记为同一点
				for(int u = pre[v]; u != v; u = pre[u])
					id[u] = cnt;//对环内所有的点赋为同一个id
			}
		}
		//若无环,则得到结果,退出即可
		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 = e[i].u, v = e[i].v;
			e[i].u = id[u], e[i].v = id[v];
			//非重边,修改边权
			if(id[u] != id[v])//同一个id 要么是同一个点,要么是同一个环,这里都能给排除掉
				e[i].w -= inv[v];
		}
		//新图的root
		root = id[root];
		//新图的结点数
		n = cnt;
	}
	return ans;
} 
int main()
{
	cin >> n >> m >> root;
	for(int i = 1; i <= m; i++)
		cin >> e[i].u >> e[i].v >> e[i].w;
	cout << zhuliu()<<endl;
	return 0;
}

最后再解释一下,缩点重构为什么能等价于破环换边,假设存在一个环,构成环的边的权值总和为res1,当前求得的最小树形图权值为ans,重构边权的过程就是遍历每个边,将每条边的权值减去弧头的最小弧的权值(最小弧就是指向该点的边中最短的那条),已知当前ans中存的是环外所有构成最小树形图的最小的权值加上环内的权值res1,现在重构之后,原来ans累加过的边权都变成了0(ans累加的一定是当前指向该点的最小弧)。此时图中的环缩成了一个点,那么环内的权值就不再考虑(实际上已经被累加进ans了),环外原来的最小弧仍然是最小弧(长度都为0),原来环外指向环内的边现在都指向一个点(也许没有指向环内的,这时候就没有答案)。缩点重构之后,把新图看作原图一样做同样的处理,不同的是ans是上一次处理的ans,因为上一次ans累加过的最小弧这时候仍然是最小弧并且等于0 ,新图要添加的边也减去了环内对应点的最小弧的长度,ans这时候累加的其实是(边的权值-最小弧的长度=破环换边)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值