最小生成树复习&总结

前言

  1. 重新整理自己的模板。
  2. 参考博客:
    1. oi-wiki 最小生成树
    2. 繁凡さん

一、前置知识——并查集

模板

int fa[N];
/*初始化*/
//一定记着初始化
void init(int n) {
    for (int i = 0; i <= n; i++) fa[i] = i;
}
/*查询*/
int find(int x) { return (fa[x] == x) ? x : fa[x] = find(fa[x]); }
/*合并*/
//能用并查集操作的东西,还不少!
void merge(int x, int y) {
    x = find(x), y = find(y);
    if (x != y) fa[x] = y;
}

拓展

  1. 并查集结构find(x)表示x所在的并查集的根节点,在两个不同并查集合并的时候,find(x)find(y)分别表示两个并查集的根节点,fa[find(x)]=find(y)即表示find(y)为合并之后的并查集的根节点。

维护并查集大小:简单的很,在merge的时候操作就ok了

// 维护并查集的大小的时候的merge
void init(int n) {
    for (int i = 1; i <= n; i++) sz[i] = 1;
}
void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if (fx != fy) {
        sz[fy] += sz[fx];
        sz[fx] = 0;
        fa[fx] = fy;
    }
}

按秩优化:虽然没啥用,但最好知道原理

//按秩优化,虽然没啥用,但我最好知道其原理
void init(int n) {
    for (int i = 1; i <= n; i++) dep[i] = 0;
}
void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if (dep[fx] < dep[fy])
        fa[fx] = fy;  //小的挂在大的上,不然大的还是会增加
    else {
        fa[fy] = fx;
        if (fx == fy) return;  //本来就是一个东西,dep就不要增加了
        if (dep[fx] == dep[fy]) dep[fx]++;  //谁是根节点得分清楚
    }
}

二、基础算法

1.克鲁斯卡尔:加n-1条边

  1. 时间复杂度 O ( e log ⁡ n ) O(e\log n) O(elogn)
  2. 操作:先对边进行从小到达排序,然后遍历加边(并查集相同则不加),直到加到 n − 1 n-1 n1条边为止。
  3. 代码:略了,很简单的。

2.prim:加n-1个点(不算第一个点)

  1. 时间复杂度 O ( n 2 ) O(n^2) O(n2)
  2. 空间复杂度 O ( n 2 ) O(n^2) O(n2)
  3. 模板
int vis[maxn], d[maxn];
int prim() {
    for (int i = 1; i <= n; i++) vis[i] = 0, d[i] = 9e18;  //初始化
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (vis[j] == 0 && (t == -1 || d[j] < d[t])) t = j;
        }
        vis[t] = 1;              //标记
        if (i > 1) ans += d[t];  //第一个点不加,i==1是d[t]=1e9
        for (int j = 1; j <= n; j++)
            d[j] = min(d[j], mp[t][j]);  //二维数组注意空间复杂度
    }
    return ans;
}

3.Boruvka算法(结合prim与克鲁斯卡尔)

  1. 时间复杂度是严格的 O ( e log ⁡ n ) O(e\log n) O(elogn)。每次最少可以连连通块数量少一半,最多调用 log ⁡ n \log n logn次Boruvka,每次复杂度为 O ( e log ⁡ n ) O(e\log n) O(elogn) log ⁡ n \log n logn为并查集)。
  2. 与prim的差别:这里是多连通块拓展增广图,prim是单连通块拓展。
  3. 据(oi-wiki评论)说这个算法可拓展性很强(但是好像快退役了都没怎么遇到过)。
  4. 目前还每遇到一题可以用克鲁斯卡尔/prim但是恰恰不能用Boruvka。
  5. 题目P3366 【模板】最小生成树
  6. 模板
bool Boruvka() {
    // best[u]=id记录u-v,id为u-v的下标。其中u,v不在同一连通块,且u-v是所有满足条件的最小权值边。
    int best[maxn];
    memset(best, 0, sizeof(best));
    //枚举所有边,更新best。(与Prim差不多,不同的是这里是多个连通块拓展增广路,prim是一个连通块)
    for (int i = 1; i <= m; i++) {
        int fx = find(s[i].u), fy = find(s[i].v);
        if (fx == fy) continue;
        if (s[i].w < s[best[fx]].w || best[fx] == 0) best[fx] = i;
        if (s[i].w < s[best[fy]].w || best[fy] == 0) best[fy] = i;
    }
    //枚举所有s[best[i]]边,判断是否能够连线(与克鲁斯卡尔完全一样的操作)
    bool f = false;
    for (int i = 1; i <= n && cx + 1 < n; i++) {
        int fx = find(s[best[i]].u), fy = find(s[best[i]].v);
        if (fx == fy) continue;
        merge(fx, fy);
        ans += s[best[i]].w;
        cx++;
        f = true;  //表示还可能继续更新,如果!f一定不能继续更新
    }
    return f;

三、最小生成森林

  1. 题目P1195 口袋的天空
  2. 题意:给定 1 ≤ n ≤ 1 e 3 1\le n\le 1e3 1n1e3朵云, 1 ≤ m ≤ 1 e 4 1\le m\le 1e4 1m1e4条线(下标为 u u u v v v的云相连的代价为dis),求这 n n n朵云通过这些线连接成 k k k朵的最小花费。
  3. 题解:克鲁斯卡尔。遍历所有边如果连线条数为 n − k n-k nk则说明有答案,否则不能连成 k k k条边。
  4. 代码
#include <bits/stdc++.h>
 
// #define int long long
#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
#define dbg(x) cout << #x << ">>>" << x << endl;
using namespace std;
const int maxn = 1e4 + 10;
 
struct node {
    int u, v, dis;
    node() {}
    node(int _u, int _v, int _dis) { u = _u, v = _v, dis = _dis; }
    void out(int i) {
        cout << ">>>" << i << ":::" << u << " " << v << " " << dis << endl;
    }
    bool operator<(node b) { return dis < b.dis; }
} s[maxn];
int n, m, k;
 
int fa[maxn], dep[maxn];
int find(int x) { return (x == fa[x]) ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if (dep[fx] < dep[fy])
        fa[fx] = fy;
    else {
        fa[fy] = fx;
        if (dep[fx] == dep[fy]) dep[fy]++;
    }
}
 
signed main() {
    read(n), read(m), read(k);
    for (int i = 1; i <= m; i++) {
        read(s[i].u), read(s[i].v), read(s[i].dis);
    }
    sort(s + 1, s + 1 + m);
    for (int i = 1; i <= n; i++) fa[i] = i;
    int ans = 0, cx = 0;
    for (int i = 1; i <= m && cx + k < n; i++) {
        int fu = find(s[i].u), fv = find(s[i].v);
        if (fu != fv) merge(fu, fv), ans += s[i].dis, cx++;
    }
    if (k > n || cx + k != n)
        puts("No Answer");
    else
        print(ans, '\n');
    return 0;
}

四、判断最小生成树的唯一性

解决方法1:求非严格次小生成树看是否和最小生成树相等,如果不相等那么就唯一

解决方法2:如果在选一个点加进来的时候,有两条及以上的路径可以选择,那么这个最小生成树就就是不唯一的

在这里插入图片描述

  1. 例题POJ 1679 The Unique MST
  2. 题意:给定 n ≤ 100 n\le 100 n100个点,m条带权边。问最小生成树MST是否唯一。
  3. 代码
#include <string.h>
 
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
 
#define int long long
#define pb push_back
#define dbg(x) cout << #x << ">>>" << x << endl;
#define mst(a, x) memset(a, x, sizeof(a))
using namespace std;
const int maxn = 1e2 + 10;
int n, m, u, v, w;
int g[maxn][maxn];
int vis[maxn], d[maxn];
int ans, cnt[maxn];
bool Unique_MST_prim() {
    bool f = true;
    for (int i = 1; i <= n; i++) vis[i] = cnt[i] = 0, d[i] = 9e18;  //初始化
    for (int i = 1; i <= n; i++) {
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (vis[j] == 0 && (t == -1 || d[j] < d[t])) t = j;
        }
        vis[t] = 1;                 //标记
        if (cnt[t] > 1) f = false;  //有多个选择到达点t
        if (i > 1) ans += d[t];     //第一个点不加,i==1是d[t]=1e9
        for (int j = 1; j <= n; j++) {
            if (vis[j] || g[t][j] == 9e18) continue;
            if (d[j] == g[t][j]) cnt[j]++;
            if (d[j] > g[t][j])
                d[j] = g[t][j], cnt[j] = 1;  //恢复为只有一条路径到达点j
        }
    }
    return f;
}
void init() {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) g[i][j] = 9e18;
    for (int i = 1; i <= n; i++) g[i][i] = 0;
}
signed main() {
    int T;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        init();
        for (int i = 1; i <= m; i++) {
            cin >> u >> v >> w;
            g[u][v] = g[v][u] = w;
        }
        ans = 0;
        if (!Unique_MST_prim())
            cout << "Not Unique!" << endl;
        else
            cout << ans << endl;
    }
    return 0;
}
  1. 操作也简单,理解也容易,唯一不好的就是复杂度得像prim一样是 O ( n 2 ) O(n^2) O(n2)

五、瓶颈生成树

  1. 定义:最大权值最小的生成树
  2. 性质最小生成树一定是瓶颈生成树,瓶颈生成树不一定是最小生成树
  3. 例题POJ-2395-Out of Hay
  4. 代码:略了,很简单的。

六、次小生成树(严格&非严格)

  1. 参考oi-wiki​繁凡さん​
  2. 次小生成树定义:所有生成树权值和第二小的生成树。
  3. 严格次小生成树:权值和大于最小生成树的生成树。
  4. 非严格次小生成树:权值和大于等于最小生成树的生成树(最小生成树不唯一等价于次小生成树为非严格次小生成树)。

解法1:

  1. 先求最小生成树T,然后枚举所有在T上的边,求 m i n ( T − w + w ′ ) min(T-w+w') min(Tw+w) w w w为T上 u − v u-v uv路径上的最大值, w ′ w' w为途中 u − v u-v uv其他所有路径上的最小的最大边(非严格: w ′ ≥ w w'\ge w ww,严格: w ′ > w w'>w w>w)。
  2. 时间复杂度 O ( m l o g m + n m ) O(mlogm+nm) O(mlogm+nm) n m nm nm为求所有 u − v u-v uv路径的最小的最大值的复杂度。一般不是用这种方式,因为不容易优化。

解法2:

  1. 先求最小生成树T,然后枚举所有不在T上的边,求 m i n ( T − w + w ′ ) min(T-w+w') min(Tw+w) w ′ w' w为枚举的这条边, w w w为T上 u − v u-v uv路径上的最大值。
  2. 时间复杂度 O ( m l o g m + n n ) O(mlogm+nn) O(mlogm+nn) n n nn nn为求T上所有 u − v u-v uv路径的最大值的复杂度(每个点作为祖先dfs一边即可),可以LCA倍增优化,优化后时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
  3. 注意:用方法二求严格最小生成树,需要预处理T上 u − v u-v uv路径上的最大值以及严格次大值(不能等于最大值),如果枚举的这条边大于最大值,就替换最大值,等于的话就替换严格次大值。

例题&代码

  1. 题目P4180 [BJWC2010]严格次小生成树
  2. 题意 n n n个点, m m m条线( n , m ≤ 3 e 5 n,m\le 3e5 n,m3e5)。求严格次小生成树大小。(输入不保证无自环)
  3. 题解:见解法一
  4. 代码
#include <bits/stdc++.h>
#define int long long
#define dbg(x) cout << #x << ">>>>" << x << endl;
#define read(x) scanf("%lld", &x)
#define print(a, c) printf("%lld%c", a, c)
#define pb push_back

using namespace std;
const int maxn = 3e5 + 10;
const int mod = 1e9 + 7;
struct node {
    int x, y, z;
    node() {}
    node(int _x, int _y, int _z) { x = _x, y = _y, z = _z; }
    bool operator<(node b) const { return z < b.z; }
} s[maxn];
struct pii {
    int to, w;
    pii() {}
    pii(int _to, int _w) { to = _to, w = _w; }
};
int n, m, x, y, z;
int fa[maxn], dep[maxn], cnt, vis[maxn];
int ans, cx, ret;
vector<pii> g[maxn];
int mx1[maxn][25], mx2[maxn][25], f[maxn][25];  // 1<<20
//并查集路径压缩+按秩优化
int find(int x) { return (x == fa[x]) ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if (dep[fx] < dep[fy])
        fa[fx] = fy;
    else {
        fa[fy] = fx;
        if (dep[fx] == dep[fy]) dep[fy]++;
    }
}
//建最小生成树,顺便标记最小生成树所用的边
void add(int i) {
    int x = s[i].x, y = s[i].y, z = s[i].z;
    merge(x, y), ans += z, cx++;             // kruskal
    g[x].pb(pii(y, z)), g[y].pb(pii(x, z));  //建树
    vis[i] = 1;                              //标记树上的点
}
//为求lca做准备,预处理dep以及mx1(mx1[i][0]表示该点向上2^0步的路径上的最大值)
void dfs(int x, int fa) {
    for (auto i : g[x]) {
        if (i.to == fa) continue;
        dep[i.to] = dep[x] + 1;
        f[i.to][0] = x, mx1[i.to][0] = i.w;
        dfs(i.to, x);
    }
}
// lca预处理,注意mx1,mx2的处理
void lca_init() {
    for (int j = 1; j <= 20; j++)
        for (int i = 1; i <= n; i++) {
            f[i][j] = f[f[i][j - 1]][j - 1];
            //分成两段!!!mx1[i][j],mx2[i][j]此前都为更新,即为0
            mx1[i][j] = max(mx1[i][j - 1], mx1[f[i][j - 1]][j - 1]);
            if (mx1[i][j - 1] == mx1[f[i][j - 1]][j - 1]) {  //两段相等
                mx2[i][j] = max(mx2[i][j - 1], mx2[f[i][j - 1]][j - 1]);
            } else {
                //注意这里是小于,因为不能等于mx1[i][j]。总之要理解
                mx2[i][j] = min(mx1[i][j - 1], mx1[f[i][j - 1]][j - 1]);
                mx2[i][j] = max(mx2[i][j], mx2[i][j - 1]);
                mx2[i][j] = max(mx2[i][j], mx2[f[i][j - 1]][j - 1]);
            }
        }
}
//点x向上2^i步,更新res——res为更新(指换一条边使x-y仍然连接)的代价。
void calc(int &res, int x, int i, int z) {
    if (z == mx1[x][i])
        res = min(res, z - mx2[x][i]);
    else
        res = min(res, z - mx1[x][i]);
}
// lca的基操。
int query(int x, int y, int z) {
    int res = 9e18;
    if (dep[x] < dep[y]) swap(x, y);
    //使dep[x]=dep[y];
    for (int i = 20; i >= 0; i--) {
        if (dep[f[x][i]] >= dep[y]) {
            calc(res, x, i, z);
            x = f[x][i];
        }
    }
    if (x == y) return res;
    for (int i = 20; i >= 0; i--) {
        if (f[x][i] != f[y][i]) {
            calc(res, x, i, z), calc(res, y, i, z);
            x = f[x][i], y = f[y][i];
        }
    }
    calc(res, x, 0, z), calc(res, y, 0, z);
    return res;
}
signed main() {
    /*输入,以及建最小生成树,大小为ans*/
    read(n), read(m);
    for (int i = 1; i <= m; i++) {
        read(x), read(y), read(z);
        if (x == y) continue;  //自环
        s[++cnt] = node(x, y, z);
    }
    sort(s + 1, s + 1 + cnt);  //别忘了
    for (int i = 1; i <= n; i++) fa[i] = i;
    for (int i = 1; i <= cnt && cx + 1 < n; i++) {
        int fu = find(s[i].x), fv = find(s[i].y);
        if (fu != fv) add(i);
    }
    //需要预处理dep以及mx1,mx2
    memset(dep, 0, sizeof(dep)), dep[1] = 1, dfs(1, 0);
    lca_init();
    //更新ret
    ret = 9e18;
    for (int i = 1; i <= cnt; i++) {
        if (vis[i] == 0) {
            ret = min(ret, query(s[i].x, s[i].y, s[i].z));
        }
    }
    ans += ret;
    print(ans, '\n');
    return 0;
}
/*
5 6
1 2 1
1 3 2
2 4 3
3 5 4
3 4 3
4 5 6
*/

七、最小瓶颈路

  1. 定义:图中任意两点之间,最大边权最小的简单路径。
  2. 题目BZOJ-3732-Network
  3. 题意:给定 n n n个点( n ≤ 1.5 e 4 n\le 1.5e4 n1.5e4), m m m条边( m ≤ 3 e 4 m\le 3e4 m3e4)。 q q q次询问,每次询问这个无向图上任意两个点的最小瓶颈路,即这两点之间的所有路径中最大值的最小值(最小生成树)。

解法1:爆改一下上面次小生成树的代码(好像不太会emm,略了)

解法2:kruskal重构树(建最小生成树,然后求两点之间最大边权)

#include <bits/stdc++.h>
#define int long long
#define read(x) scanf("%lld", &x)
#define print(a, c) printf("%lld%c", a, c)
#define pb push_back
#define dbg(x) cout << #x << ">>>" << x << endl
using namespace std;
const int maxn = 3e4 + 10;
struct node {
    int x, y, z;
    node() {}
    node(int _x, int _y, int _z) { x = _x, y = _y, z = _z; }
    bool operator<(node b) const { return z < b.z; }
    void out(int i) { cout << i << ":::" << x << " " << y << " " << z << endl; }
} s[maxn];
int n, m, q, x, y, z;
int fa[maxn], dep[maxn], cnt, cx;
int val[maxn], id;  // kruskal重构树大小为2*n-1
vector<int> g[maxn];
int f[maxn][25];
int find(int x) { return (x == fa[x]) ? x : fa[x] = find(fa[x]); }
void dfs(int x, int fa) {
    for (auto i : g[x]) {
        if (i == fa) continue;
        dep[i] = dep[x] + 1, f[i][0] = x;
        dfs(i, x);
    }
}
int get_lca(int x, int y) {
    if (dep[x] < dep[y]) swap(x, y);
    for (int j = 20; j >= 0; j--)
        if (dep[f[x][j]] >= dep[y]) x = f[x][j];
    if (x == y) return x;
    for (int j = 20; j >= 0; j--)
        if (f[x][j] != f[y][j]) x = f[x][j], y = f[y][j];
    x = f[x][0], y = f[y][0];
    return x;
}
signed main() {
    read(n), read(m), read(q);
    for (int i = 1; i <= m; i++) {
        read(x), read(y), read(z);
        if (x == y) continue;
        s[++cnt] = node(x, y, z);
    }
    sort(s + 1, s + 1 + cnt);
    // for (int i = 1; i <= cnt; i++) s[i].out(i);
    //注意初始化2*n-1个节点
    for (int i = 1; i <= 2 * n - 1; i++) fa[i] = i;
    for (int i = 1; i <= cnt && cx + 1 < n; i++) {
        int fu = find(s[i].x), fv = find(s[i].y);
        if (fu != fv) {
            cx++;
            id = n + cx;       //新节点下标
            val[id] = s[i].z;  //不需要特别排序,就按照加入的顺序
            fa[fu] = id, fa[fv] = id;
            g[id].pb(fu), g[id].pb(fv);
        }
    }
    //注意要以2*n-1为根节点
    dep[2 * n - 1] = 1;
    dfs(2 * n - 1, 0);
    // lca_init:::尽量写在下面????居然忘了调用
    for (int j = 1; j <= 20; j++)
        for (int i = 1; i <= 2 * n - 1; i++) f[i][j] = f[f[i][j - 1]][j - 1];
    while (q--) {
        read(x), read(y);
        print(val[get_lca(x, y)], '\n');
    }
    return 0;
}

八、kruskal重构树

  1. 参考博客Kruskal重构树 学习笔记
  2. 操作:每次在kruskal连边的时候,两个节点的连通块的根节点(路径压缩之后,任意时刻某个点的find()函数,即为该连通块的根节点)分别作为左右子树,新建一个节点,下标为n+cx,权值为连接两条边的权值(因为kruskal是从小到大加入边)。
  3. 性质:任意两点的最小瓶颈路中的最大边权即为他们在kruskal树中lca的点权。

能解决的问题:因为它的一些性质,在解决一些问题的时候有着天然的优势。

求最小瓶颈路

给一个无向图。每次询问给一个点v以及一个值p,问从v点出发,只能走<=p的线段,每次最多可以走多少个点。

重要性质1:而且kruskal结构尤其特殊:深度越大,值越小(我们可以将所有的父亲节点都弄成>n的,这样解决这个问题的时候就可以弄个特定的数组直接查询——二分查询,因为顺序一定)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值