朱刘算法
1 问题
我们知道带权无向图上有一个经典问题:最小生成树。那么如果换成带权有向图呢?
对于一个带权有向图,从中选出一个子图,使得该子图中无环,且存在一个点可以到达其他所有点,则这个子图就是一个树形图。而求出所有树形图中选出边权和最小的一种选法,就是最小树形图问题。
容易想到,解决最小生成树的两种方法都是基于无向成立的,因此不能套用到该问题上。我们就需要一种新的算法。
2 朱刘算法
2.1 算法流程
我们先考虑给定最小树形图的根
我们先考虑一个较简单的问题:求出一个 DAG 的最小树形图。那么显然我们只需要选出所有点的入边中最小的一条即可。
假如我们对任意图进行上述操作,会发现一个问题:我们最后选出的子图很可能带环。那么此时环上一定会有一个点,它的真正入边不再是环上对应的那条入边,而是环外指向他的一条边。而此时假设原先的入边是
那么我们可以这样来做:我们对于每一个点先找出他们的最小入边并相连。接下来考虑每一个点,如果它指向的节点是一个环上的节点,设这条边边权为
那么显然经过这样的操作,我们选出来的价值一定和最小树形图是等价的。
现在只需要考虑一个问题,即如何找环。Tarjan 实际上有些复杂,我们有更简单的做法。
2.1.1 找环
令
我们遍历每一个节点
- 如果走到了
,那么它一定不在环上。 - 如果走到了一个点
的 ,则说明一定出现了环。此时还要继续讨论:- 如果
,则说明这是一个新环,遍历环上节点更新 即可。 - 否则说明这个环已经被更新过了,一个节点不可能同时是两个环的节点,所以不必操作。
- 如果
- 如果走到了一个在环上的点,和上面一样的道理,我们不必操作。
这样就可以找到环并将它们缩点了。具体代码如下:
for(int i = 1; i <= n; i++) {
int x = i;
while(id[x] != i && bel[x] == -1 && x != rt) {//没有走到根,没有走到环上点,没有走到前驱是自己的点
id[x] = i;//沿途更新
x = pre[x];
}
if(x != rt && bel[x] == -1) {//找到新环
bel[x] = ++cnt;//更新 bel
for(int j = pre[x]; j != x; j = pre[j]) bel[j] = cnt;
}
}
有了这个关键的技术,剩下的部分就不难完成了。
上面我们探讨的都是给定根的情况,如果不指定根呢?很简单,我们新建超级源点,向所有点连一条
2.2 完整代码
模板题:【模板】最小树形图。代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 1e13;
int n, m, rt;
struct node {
int u, v, w;
}e[Maxn];
int cnt, bel[Maxn], pre[Maxn], mn[Maxn], id[Maxn];
int Chu_Liu() {
int ans = 0;
while(1) {
for(int i = 1; i <= n; i++) mn[i] = Inf, pre[i] = 0;
for(int i = 1; i <= m; i++) {//找最小入边
if(e[i].u != e[i].v && e[i].w < mn[e[i].v]) {
mn[e[i].v] = e[i].w;
pre[e[i].v] = e[i].u;
}
}
for(int i = 1; i <= n; i++) if(i != rt && mn[i] == Inf) return -1;
//如果有点没有最小入边,则一定不可能联通
for(int i = 1; i <= n; i++) id[i] = bel[i] = -1;
cnt = 0;
mn[rt] = 0;
for(int i = 1; i <= n; i++) {//找环
int x = i;
ans += mn[i];//直接求和
while(id[x] != i && bel[x] == -1 && x != rt) {
id[x] = i;
x = pre[x];
}
if(x != rt && bel[x] == -1) {
bel[x] = ++cnt;
for(int j = pre[x]; j != x; j = pre[j]) bel[j] = cnt;
}
}
if(cnt == 0) break;
for(int i = 1; i <= n; i++) if(bel[i] == -1) bel[i] = ++cnt;
for(int i = 1; i <= m; i++) {//重建边
int tmp = e[i].v;
e[i].u = bel[e[i].u];
e[i].v = bel[e[i].v];
if(e[i].u != e[i].v) e[i].w -= mn[tmp];//修改边权
}
n = cnt;
rt = bel[rt];
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> rt;
for(int i = 1; i <= m; i++) {
cin >> e[i].u >> e[i].v >> e[i].w;
}
cout << Chu_Liu();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律