最小树形图
1.算法分析
有向图上的最小生成树(Directed Minimum Spanning Tree)称为最小树形图。常用的算法是朱刘算法(也称 Edmonds 算法),可以在 O ( n m ) O(nm) O(nm) 时间内解决最小树形图问题。
该算法最后能够找到一个从根出发,能够走到任意一个点的一个最小权值的有向的树。
流程
- 对于每个点,选择它入度最小的那条边
- 如果没有环,算法终止;否则进行缩环并更新其他点到环的距离。
- 缩点时:对于环内部的边,删去;对于终点在环内的,边权变为w外-w环内。
2.模板
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 100 + 5;
const int mod = 1000000000 + 7;
const double eps = 1e-8;
typedef pair<int, int> PII;
int id[N], vis[N], pre[N], pos; // id记录每个点属于哪个环,vis用来判断几个点是否处于一个环内,
// pre记录每个点的前一个节点,pos记录虚节点
double dis[N], INF = 1e17; // dis记录每个点到最小生成树的距离
PII point[N];
// 定义边的结构
struct node {
int u, v;
double cost;
} edge[N * N];
int n, m;
// 朱刘算法
// root:根节点(记得下标从0开始)
// V点数0~V-1
// E边数0~E-1
double zhuliu(int root, int V, int E) {
double sum = 0; // 计算当前最小树形图的边权值和
while (true) {
// 把每个点到最小生成树的距离置为无穷
for (int i = 0; i < V; i++) {
dis[i] = INF;
}
//找最小入边
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
if (u != v && dis[v] > edge[i].cost) {
dis[v] = edge[i].cost;
pre[v] = u;
if (u == root) {
pos = i;
}
}
}
//某点不存在入边,算法结束
for (int i = 0; i < V; i++) {
if (dis[i] == INF && i != root) return -1;
}
int cnt = 0; // 记录当前这张图内点的个数
memset(id, -1, sizeof id);
memset(vis, -1, sizeof vis);
dis[root] = 0; // 把根节点放入最小树形图
//找环
for (int i = 0; i < V; i++) {
int v = i;
sum += dis[i];
while (id[v] == -1 && vis[v] != i && v != root) {
vis[v] = i;
v = pre[v];
}
//找到环的时候缩点编号
if (id[v] == -1 && v != root) {
for (int u = pre[v]; u != v; u = pre[u]) {
id[u] = cnt; // 把u点放入编号为cnt的环内
}
id[v] = cnt++;
}
}
//如果没有环,则以找到最小树形图,算法结束
if (!cnt) break;
//把余下的不在环里的点编号
for (int i = 0; i < V; i++) {
if (id[i] == -1) id[i] = cnt++;
}
//更新距离
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
edge[i].u = id[u];
edge[i].v = id[v];
if (id[u] != id[v]) edge[i].cost -= dis[v];
}
V = cnt; // 新图的点数为cnt
root = id[root]; // 新图的根为原来的根所在的缩点集
}
return sum;
}
double get_dist(int x, int y) {
return sqrt(((double)point[x].first - point[y].first) * ((double)point[x].first - point[y].first) + ((double)point[x].second - point[y].second) * ((double)point[x].second - point[y].second) );
}
int main() {
while(scanf("%d%d", &n, &m) != EOF) {
for (int i = 1; i <= n; ++i) cin >> point[i].first >> point[i].second;
for (int i = 0; i < m; ++i) {
int a, b;
cin >> a >> b;
edge[i] = {a - 1, b - 1, get_dist(a, b)};
}
double t = zhuliu(0, n, m);
if (t == -1) cout << "poor snoopy\n";
else printf("%.2lf\n", t);
}
return 0;
}
3.典型例题
3.1 定根最小树形图
acwing2417. 指挥网络
题意: 给定一张n个点m条边的有向图,边的权值为double型,要求找到这张图的最小树形图。 1 < = N < = 100 , 1 < = M < = 1 0 4 , 0 < x i , y i < = 10000 , 1 < = a , b < = N , a ≠ b 1<=N<=100, 1<=M<=10^4,0<xi,yi<=10000,1<=a,b<=N, a \neq b 1<=N<=100,1<=M<=104,0<xi,yi<=10000,1<=a,b<=N,a=b
题解: 模板题
代码:
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 100 + 5;
const int mod = 1000000000 + 7;
const double eps = 1e-8;
typedef pair<int, int> PII;
int id[N], vis[N], pre[N], pos; // id记录每个点属于哪个环,vis用来判断几个点是否处于一个环内,
// pre记录每个点的前一个节点,pos记录虚节点
double dis[N], INF = 1e17; // dis记录每个点到最小生成树的距离
PII point[N];
// 定义边的结构
struct node {
int u, v;
double cost;
} edge[N * N];
int n, m;
// 朱刘算法
// root:根节点(记得下标从0开始)
// V点数0~V-1
// E边数0~E-1
double zhuliu(int root, int V, int E) {
double sum = 0; // 计算当前最小树形图的边权值和
while (true) {
// 把每个点到最小生成树的距离置为无穷
for (int i = 0; i < V; i++) {
dis[i] = INF;
}
//找最小入边
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
if (u != v && dis[v] > edge[i].cost) {
dis[v] = edge[i].cost;
pre[v] = u;
if (u == root) {
pos = i;
}
}
}
//某点不存在入边,算法结束
for (int i = 0; i < V; i++) {
if (dis[i] == INF && i != root) return -1;
}
int cnt = 0; // 记录当前这张图内点的个数
memset(id, -1, sizeof id);
memset(vis, -1, sizeof vis);
dis[root] = 0; // 把根节点放入最小树形图
//找环
for (int i = 0; i < V; i++) {
int v = i;
sum += dis[i];
while (id[v] == -1 && vis[v] != i && v != root) {
vis[v] = i;
v = pre[v];
}
//找到环的时候缩点编号
if (id[v] == -1 && v != root) {
for (int u = pre[v]; u != v; u = pre[u]) {
id[u] = cnt; // 把u点放入编号为cnt的环内
}
id[v] = cnt++;
}
}
//如果没有环,则以找到最小树形图,算法结束
if (!cnt) break;
//把余下的不在环里的点编号
for (int i = 0; i < V; i++) {
if (id[i] == -1) id[i] = cnt++;
}
//更新距离
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
edge[i].u = id[u];
edge[i].v = id[v];
if (id[u] != id[v]) edge[i].cost -= dis[v];
}
V = cnt; // 新图的点数为cnt
root = id[root]; // 新图的根为原来的根所在的缩点集
}
return sum;
}
double get_dist(int x, int y) {
return sqrt(((double)point[x].first - point[y].first) * ((double)point[x].first - point[y].first) + ((double)point[x].second - point[y].second) * ((double)point[x].second - point[y].second) );
}
int main() {
while(scanf("%d%d", &n, &m) != EOF) {
for (int i = 1; i <= n; ++i) cin >> point[i].first >> point[i].second; // 读入点
for (int i = 0; i < m; ++i) {
int a, b;
cin >> a >> b;
edge[i] = {a - 1, b - 1, get_dist(a, b)}; // 读入边
}
double t = zhuliu(0, n, m); // 朱刘算法
if (t == -1) cout << "poor snoopy\n"; // 无解
else printf("%.2lf\n", t); // 最小树形图的最小权值
}
return 0;
}
3.2 不定根最小树形图
hdu2121 Ice_cream’s world II
题意: 有n个城市,问能否在者n个城市中选一个城市作为首都,要求首都都能到其他城市,道路花费要最少,且道路都是单向的。打印最少花费和选出来的首都。
题解: 本题要找一个根,同时要求出最小树形图的权值和。可以虚拟一个虚根,如果以虚根为根生成的最小树形图权值和ans减去不包含虚根的全部权值和sum为大于全部权值和,那么不存在这样一个虚根(因为最多只会有一条虚边);反之,存在,则真正的最小树形图的权值和为 ans - sum,根节点为与虚根相连的那个点pos - m(n节点对应m边,n+1节点对应m+1边)
代码:
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#define LL long long
using namespace std;
const int N = 1000 + 5;
const int mod = 1000000000 + 7;
const double eps = 1e-8;
int id[N], vis[N], pre[N],
pos; // id记录每个点属于哪个环,vis用来判断几个点是否处于一个环内,
// pre记录每个点的前一个节点,pos记录虚节点
LL dis[N], INF = 1e17; // dis记录每个点到最小生成树的距离
// 定义边的结构
struct node {
int u, v, cost;
} edge[N * N];
// 朱刘算法
// root:根节点
// V点数0~V-1
// E边数0~E-1
LL zhuliu(int root, int V, int E) {
LL sum = 0; // 计算当前最小树形图的边权值和
while (true) {
// 把每个点到最小生成树的距离置为无穷
for (int i = 0; i < V; i++) {
dis[i] = INF;
}
//找最小入边
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
if (u != v && dis[v] > edge[i].cost) {
dis[v] = edge[i].cost;
pre[v] = u;
if (u == root) {
pos = i;
}
}
}
//某点不存在入边,算法结束
for (int i = 0; i < V; i++) {
if (dis[i] == INF && i != root) return -1;
}
int cnt = 0; // 记录当前这张图内点的个数
memset(id, -1, sizeof id);
memset(vis, -1, sizeof vis);
dis[root] = 0; // 把根节点放入最小树形图
//找环
for (int i = 0; i < V; i++) {
int v = i;
sum += dis[i];
while (id[v] == -1 && vis[v] != i && v != root) {
vis[v] = i;
v = pre[v];
}
//找到环的时候缩点编号
if (id[v] == -1 && v != root) {
for (int u = pre[v]; u != v; u = pre[u]) {
id[u] = cnt; // 把u点放入编号为cnt的环内
}
id[v] = cnt++;
}
}
//如果没有环,则以找到最小树形图,算法结束
if (!cnt) break;
//把余下的不在环里的点编号
for (int i = 0; i < V; i++) {
if (id[i] == -1) id[i] = cnt++;
}
//更新距离
for (int i = 0; i < E; i++) {
int u = edge[i].u, v = edge[i].v;
edge[i].u = id[u];
edge[i].v = id[v];
if (id[u] != id[v]) edge[i].cost -= dis[v];
}
V = cnt; // 新图的点数为cnt
root = id[root]; // 新图的根为原来的根所在的缩点集
}
return sum;
}
int main() {
int n, m;
LL ans, temp;
while (~scanf("%d%d", &n, &m)) {
temp = 1;
// 读入边的信息
for (int i = 0; i < m; i++) {
scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
temp += edge[i].cost;
}
//以n为虚拟节点
for (int i = m; i < n + m; i++) {
edge[i].u = n;
edge[i].v = i - m;
edge[i].cost = temp;
}
// 得到以虚节点为根节点的最小树形图的边权值和
ans = zhuliu(n, n + 1, n + m);
if (ans == -1 || ans - temp >= temp)
puts("impossible\n");
else
printf("%lld %d\n\n", ans - temp, pos - m);
}
return 0;
}