最小树形图学习笔记
前言
最小树形图,简单来说就是有向图中的最小生成树问题,一般有指定的根节点。
常见解决方法是 朱-刘算法 或 tarjan 算法,一般前者已经足够优秀,而且代码很简洁。
主要思想
其实很简单。
贪心的找到除根结点之外,每个节点的最小入边,得到一个边的集合。
如果这些边中没有有向环,那么我们已经找到了最小树形图,否则需要进一步处理。
我们将所有环进行缩点,同时在答案中加上环上所有边的权值,然后用缩点后的图重复上述过程,直至无环。
但是有个问题,缩点后相当于 \(n\) 个点选择了 \(n\) 条边连接,这不满足树形图的性质。
于是有一个等效替换的思想,假设有环 \(S\),其中 \(u\to v\) 是环上的一条边,\(a\to v\) 是环外指向环上一点 \(v\) 的边。
那么显然,如果选择 \(a\to v\),就必须断开 \(u\to v\),那么等价于在重新构图时令 \(val(a\to v)-=val(u\to v)\)。
那么这个问题就解决了。
当然还有无解的情况,若在寻找最短入边时有点没有入边,那么显然这个有向图不连通,即无解。
代码实现
int ans = 0, u, v;
while(true){
for(int i = 1; i <= n; i ++) in[i] = INF;
for(int i = 1; i <= m; i ++){
u = e[i].u;
v = e[i].v;
if(u != v && in[v] > e[i].w){
in[v] = e[i].w;
pre[v] = u;
}
}
for(int i = 1; i <= n; i ++)
if(i != root && in[i] == INF){puts("-1"); return 0;}
int tot = 0;
for(int i = 1; i <= n; i ++) id[i] = vis[i] = -1;
in[root] = 0;
for(int i = 1; i <= n; i ++){
ans += in[i];
v = i;
while(vis[v] != i && v != root && id[v] == -1){
vis[v] = i;
v = pre[v];
}
if(v != root && id[v] == -1){
id[v] = ++ tot;
for(int u = pre[v]; u != v; u = pre[u]) id[u] = tot;
}
}
if(!tot) break;
for(int i = 1; i <= n; i ++)
if(id[i] == -1) id[i] = ++ tot;
for(int i = 1; i <= m; i ++){
u = e[i].u;
v = e[i].v;
e[i].u = id[u];
e[i].v = id[v];
if(id[u] != id[v]) e[i].w -= in[v];
}
n = tot;
root = id[root];
}
时间复杂度
显然最多进行 \(n-2\) 次缩点,每次缩点需要花费 \(O(m)\) 时间。
故总时间复杂度为 \(O(nm)\)。
简单例题
在有向图中给定根节点,以及能承受的最大代价,每条边都有一个代价以及一个大小。
要求选择一些边使得总代价不大于最大承受代价,且使图为树形图,在这种情况下使最小边最大,求这个最大值。
显然是套了个二分的最小树形图问题,没了。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100;
const int M = 10010;
const int INF = 1 << 30;
int n, m, cost, in[N], pre[N], id[N], vis[N];
struct Edge{int u, v, w, kb;} e[M], tmp[M];
int read(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
int Get(int limit){
int now = n, root = 0, u, v;
int ans = 0;
for(int i = 1; i <= m; i ++) tmp[i] = e[i];
while(true){
for(int i = 0; i < now; i ++) in[i] = INF;
for(int i = 1; i <= m; i ++){
if(e[i].kb < limit) continue;
u = e[i].u;
v = e[i].v;
if(u != v && e[i].w < in[v]){
in[v] = e[i].w;
pre[v] = u;
}
}
for(int i = 0; i < now; i ++){
if(i != root && in[i] == INF){
for(int j = 1; j <= m; j ++) e[j] = tmp[j];
return -1;
}
vis[i] = id[i] = -1;
}
in[root] = 0;
int tot = 0;
for(int i = 0; i < now; i ++){
ans += in[i];
v = i;
while(vis[v] != i && id[v] == -1 && 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] = tot;
id[v] = tot ++;
}
}
if(!tot) break;
for(int i = 0; i < now; i ++)
if(id[i] == -1) id[i] = tot ++;
for(int i = 1; i <= m; i ++){
if(e[i].kb < limit) continue;
u = e[i].u;
v = e[i].v;
e[i].u = id[u];
e[i].v = id[v];
if(id[u] != id[v]) e[i].w -= in[v];
}
now = tot;
root = id[root];
}
for(int i = 1; i <= m; i ++) e[i] = tmp[i];
return ans;
}
bool chck(int limit){
int now = Get(limit);
return (now != -1 && now <= cost);
}
int main(){
int T = read();
while(T --){
n = read(), m = read(), cost = read();
int l = 0, r = 0;
for(int i = 1; i <= m; i ++){
e[i].u = read(), e[i].v = read();
e[i].kb = read(), e[i].w = read();
r = max(r, e[i].kb);
}
while(l < r){
int mid = (l + r + 1) >> 1;
if(chck(mid)) l = mid;
else r = mid - 1;
}
if(l == 0) puts("streaming not possible.");
else printf("%d kbps\n", l);
}
return 0;
}