【Coel.学习笔记】【图论终章!】朱刘算法与 Prufer 编码
结束啦!接下来就到数据结构了~
朱刘算法
有向图上的最小生成树被称为最小树形图。树形图具有两个特点:不存在环,且除了根节点外每个点的入度都为 \(1\)。
用于求解最小树形图的算法被称为朱刘算法,由朱永津和刘振宏在 1965 年首次提出而得名。后来 Jack Edmonds 也独立发现了这个算法,所以又叫做 Edmonds 算法。
朱刘算法的基本过程如下:
- 对于每个点(根除外),找到边权最小的入边。
- 判断刚才选择的边是否会组成一个环。若不存在环,则算法结束。
- 若存在环则对所有环缩点。对于环内部的边直接删除;终点在环内的边则把权值变为原有权值减去环内边的权值;其他边不变。
- 重复上述操作,直到算法结束。
每次缩点后点数至少会减少 \(1\),所以迭代次数为 \(O(m)\);每次用 tarjan 算法缩点和找环的时间复杂度为 \(O(n)\),因此总的时间复杂度为 \(O(nm)\)。Tarjan 提出了一个时间复杂度为 \(O(m+n\log n)\) 的优化算法,由于朴素的 \(O(nm)\) 算法已经足够日常使用(而且朱刘算法非常少见),此处不再赘述。
代码如下:
洛谷传送门
// Problem: P4716 【模板】最小树形图
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4716
// Memory Limit: 250 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <algorithm>
#include <cstring>
#include <iomanip>
#include <iostream>
using namespace std;
const int maxn = 150;
const int inf = 0x3f3f3f3f;
int n, m, r;
bool f[maxn][maxn];
int dis[maxn][maxn], bdis[maxn][maxn];
int pre[maxn], bpre[maxn];
int dfn[maxn], low[maxn], stk[maxn], tot, top;
int bel[maxn], idx;
bool vis[maxn], jud[maxn];
void dfs(int u) {
jud[u] = true;
for (int i = 1; i <= n; i++)
if (dis[u][i] != inf && !jud[i]) dfs(i);
}
bool check() { //判断图是否连通
dfs(r);
for (int i = 1; i <= n; i++)
if (!jud[i]) return false;
return true;
}
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
stk[++top] = u, vis[u] = true;
int v = pre[u];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (vis[v])
low[u] = min(low[u], dfn[v]);
if (low[u] == dfn[u]) {
idx++;
do {
v = stk[top--];
vis[v] = false;
bel[v] = idx;
} while (v != u);
}
}
int solve() {
int res = 0;
while (true) {
for (int i = 1; i <= n; i++) { //初始化每个点的前继
pre[i] = i;
for (int j = 1; j <= n; j++)
if (dis[pre[i]][i] > dis[j][i]) pre[i] = j;
}
memset(dfn, 0, sizeof(dfn));
tot = idx = 0;
for (int i = 1; i <= n; i++) //找缩点
if (!dfn[i]) tarjan(i);
if (idx == n) { //强连通分量数等于点数,意味着无环
for (int i = 2; i <= n; i++) res += dis[pre[i]][i];
break;
}
for (int i = 1; i <= n; i++) { //统计答案,注意省略根节点
if (i == r) continue;
if (bel[pre[i]] == bel[i]) res += dis[pre[i]][i];
}
for (int i = 1; i <= idx; i++)
for (int j = 1; j <= idx; j++) bdis[i][j] = inf;
for (int i = 1; i <= n; i++) //备份图
for (int j = 1; j <= n; j++)
if (dis[i][j] < inf && bel[i] != bel[j]) {
int u = bel[i], v = bel[j];
if (bel[pre[j]] == bel[j])
bdis[u][v] =
min(bdis[u][v], dis[i][j] - dis[pre[j]][j]);
else
bdis[u][v] = min(bdis[u][v], dis[i][j]);
}
n = idx;
memcpy(dis, bdis, sizeof(dis)); //用备份图更新原图
r = bel[r]; //根换成强连通分量
}
return res;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
memset(dis, 0x3f, sizeof(dis));
cin >> n >> m >> r;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
if (u != v) dis[u][v] = min(dis[u][v], w); //点数很少,用邻接矩阵更方便
/*注意这里要特判重边和自环*/
}
if (!check())
cout << -1;
else
cout << solve();
return 0;
}
Prufer 编码
Prufer 序列可以将一个带标号 \(n\) 个结点的树用 \(1\) 到 \(n\) 中的 \(n-2\) 个整数表示,相当于完全图生成树与数列的双射。
给定一个无根树,每次找到一个编号最小的叶子结点。输出和该结点相连的父节点,并删除叶子节点。如此反复,直到只剩两个结点,这样就构造出了一个Prufer 编码。
如下图,无根树的 Prufer 编码就是 \(3,5,4,5\)。
使用堆可以非常方便地以 \(O(n\log n)\) 的时间复杂度实现 Prufer 编码与树相互转换,但使用一个指针代替堆寻找最小值,可以实现 \(O(n)\) 的算法。
代码如下:
洛谷传送门
// Problem: P6086 【模板】Prufer 序列
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6086
// Memory Limit: 500 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <iostream>
#define int long long
const int maxn = 5e6 + 10;
using namespace std;
int n, m;
int f[maxn], d[maxn], p[maxn];
int solve_prufer() {
long long res = 0;
for (int i = 1; i < n; i++) {
cin >> f[i];
d[f[i]]++;
}
for (int i = 1, j = 1; i <= n - 2; i++, j++) {
while (d[j]) j++;
p[i] = f[j];
while (i <= n - 2 && --d[p[i]] == 0 && p[i] < j)
p[i + 1] = f[p[i]], i++;
}
for (int i = 1; i <= n - 2; i++) res ^= 1LL * i * p[i];
return res;
}
int solve_tree() {
long long res = 0;
for (int i = 1; i <= n - 2; i++) {
cin >> p[i];
d[p[i]]++;
}
p[n - 1] = n;
for (int i = 1, j = 1; i < n; i++, j++) {
while (d[j]) j++;
f[j] = p[i];
while (i < n && --d[p[i]] == 0 && p[i] < j) f[p[i]] = p[i + 1], i++;
}
for (int i = 1; i < n; i++) res ^= 1LL * i * f[i];
return res;
}
signed main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
if (m == 1)
cout << solve_prufer();
else
cout << solve_tree();
return 0;
}