生成树+次小生成树

生成树(MST)

克鲁斯卡尔(kruskal)算法

算法简介

  • 时间复杂度 \(O(E \log E)\)

算法流程

  • 给边的边权排序,然后依次加入,如果加入这条边后形成了就不加入这条边
Code
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>

using namespace std;

const int MAXN = 5000 + 10, MAXM = 2e5 + 10;

struct Qwe{
  int x, y, w;
}b[MAXM];

int n, m, ANS = 0, cnt = 0;
int fa[MAXN];

bool cmp(Qwe i, Qwe j){
  return i.w < j.w;
}

int Find(int x){
  if(fa[x] > 0){
    return fa[x] = Find(fa[x]);
  }
  return x;
}

bool as(int i){
  int X = Find(b[i].x), Y = Find(b[i].y);
  if(X != Y){
    fa[X] = Y;
    return 1;
  }
  return 0;
}

int main(){
  cin >> n >> m;
  for(int i = 1; i <= m; i++){
    cin >> b[i].x >> b[i].y >> b[i].w;
  }
  sort(b + 1, b + 1 + m, cmp);
  for(int i = 1; i <= m; i++){
    if(as(i)){
      ANS += b[i].w, cnt++;
    }
  }
  if(cnt != n - 1){
    cout << "orz";
  }else{
    cout << ANS;
  }
  return 0;
}

kruskal 重构树

每次合并时新建一节点(点权为边权),左右儿子是两个根,

设两点之间路径长度为经过的最大值,若需要最小化,则变为重构树两点 LCA 的点权。

https://www.luogu.com.cn/problem/CF1416D

https://www.luogu.com.cn/problem/P4197

https://www.luogu.com.cn/problem/P5168

应用

洛谷 - P2619 [国家集训队] Tree I

给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(need\) 条白色边的生成树(输出边权和)。题目保证有解。每条边的颜色分 \(0\)\(1\)

边权均为 \([1,100]\) 中的整数。\(1 \le V \le 5 \cdot 10^4\)\(1 \le need \le E \le 10^5\)

时间限制 2.00s,内存限制 500.00MB

解题思路

巧妙利用 kruskal 的边权排序。显然边权越小越靠前,考虑让黑白边等级分化

于是就会想到偏移量,且边权只有 \(100\),枚举感觉会挂,那就二分吧!(但是细节多了)

二分出偏移量 \(d\) 后偏移边权,在做模板 kruskal。

可以会有一种情况:偏移量为 \(d\) 时 白边数量超了,偏移量为 \(d + 1\) 时 白边数量少了。

题目保证有解,那必定是有偏移后边权相同的黑白边,特殊处理一下即可。

Code
#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int MAXE = 1e5 + 3, MAXV = 1e5 + 3;

struct Edge{
  int u, v, w, col;
}eg[MAXE];

int n, m, need;
int fa[MAXV];

int Getf(int x){
  return fa[x] == x ? x : fa[x] = Getf(fa[x]);
}

pair<int, int> kruskal(int D, int op){ // op = 0 使得 sumwight 更大,op = 1 反之 
  for(int i = 1; i <= m; i++) eg[i].w += (eg[i].col == 0 ? D : 0);
  for(int i = 1; i <= n; i++) fa[i] = i;
  sort(eg + 1, eg + 1 + m, [&op](Edge i, Edge j){ return i.w == j.w ? (i.col ^ op) < (j.col ^ op) : i.w < j.w; });
  int ret = 0, sumwight = 0;
  for(int i = 1; i <= m; i++){
    int fx = Getf(eg[i].u), fy = Getf(eg[i].v);
    if(fx != fy){
      fa[fx] = fy;
      sumwight += (eg[i].col == 0);
      ret += eg[i].w;
    }
  }
  for(int i = 1; i <= m; i++) eg[i].w -= (eg[i].col == 0 ? D : 0);
  return {ret, sumwight};
}

int main(){
  cin >> n >> m >> need;
  for(int i = 1; i <= m; i++){
    cin >> eg[i].u >> eg[i].v >> eg[i].w >> eg[i].col;
    eg[i].u++, eg[i].v++;
  }
  int l = -202, r = 202, mid;
  while(l <= r){
    mid = (l + r) >> 1;
    if(kruskal(mid, 0).second < need){
      r = mid - 1;
    }else if(kruskal(mid, 1).second > need){
      l = mid + 1;
    }else break;
  }
  cout << kruskal(mid, 0).first - need * mid;
  return 0;
}

普里姆(prim)算法

算法简介

  • 类似广搜 和 dijkstra
  • 暴力时间复杂度 \(O(V^2 + E)\)
  • 二叉堆优化后时间复杂度 \(O((V + E) \log V)\)

算法流程

  • 确定起点(可以随机确定,因为生成树最终是一个连通图
  • 加入所有一段与起点相连、另一段不与起点相连的边(这里的相连是指有连通)
  • 在所有加入了的边中,选择边权最小的一条边
  • 可以使用二叉堆优化
Code
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>

using namespace std;

const int MAXN = 5000 + 5;
const int MAXM = 2e5 + 5;

struct Edge{ int u, v, w; };
struct cmp{
  bool operator() (Edge i, Edge j) const {
    return i.w > j.w;
  }
};

int n, m, ans = 0;
bool vis[MAXN];
vector<Edge> e[MAXN];
priority_queue<Edge, vector<Edge>, cmp> E;

int Prim(){
  int _ans = 0, cnt = 0;
  vis[1] = 1, cnt = 1;
  for(Edge j : e[1]){
    if(1 != j.v && vis[j.v] == 0) E.push({1, j.v, j.w});
  }
  while(!E.empty()){
    Edge i = E.top();
    E.pop();
    if(vis[i.v] != vis[i.u]) _ans += i.w;
    vis[i.v] = 1, vis[i.u] = 1;
    for(Edge j : e[i.v]){
      if(i.v != j.v && vis[j.v] == 0) E.push({i.v, j.v, j.w});
    }
  }
  return _ans;
}

int main(){
  cin >> n >> m;
  for(int i = 1, U, V, W; i <= m; i++){
    cin >> U >> V >> W;
    e[U].push_back({0, V, W}), e[V].push_back({0, U, W});
  }
  ans = Prim();
  for(int i = 1; i <= n; i++){
    if(vis[i] == 0) ans = -1;
  }
  if(ans == -1){
    cout << "orz";
  }else{
    cout << ans;
  }
  return 0;
}

boruvka 算法

算法简介

  • 将 prim 和 kruskal 结合起来。
  • 用于求解无向图的最小生成森林
  • 时间复杂度:当原图连通时,每次迭代连通块数量至少减半,算法只会迭代不超过 \(O(\log V)\) 次。当原图不联通时就是一些子问题了。故时间复杂度 \(O(E \log V)\)

算法流程

  • 初始时将每个点视作一个连通块。
  • 不断重复一下流程:
    • 对每个连通块找到一个最近的连通块,即两个连通块之间可以选择一条边相连,距离即为所选边的边权。
    • 为每个块找到最小边后,连接这些边(可以任意顺序)。
    • :不过在连边的时候,任然需要判断是否两端点在同一连通块。
  • 最后所有所选边构成一个最小生成森林。
Code
#include <bits/stdc++.h> // brovka

using namespace std;
using LL = long long;
using PII = pair<int, int>;

const int MAXN = 2e5 + 3;

struct Edge{
  int u, v, w;
}e[MAXN];

int n, m, ans = 0;
int fa[MAXN];
PII mi[MAXN];

int Getf(int x){
  return fa[x] == x ? x : fa[x] = Getf(fa[x]);
}

bool work(){
  for(int i = 1; i <= n; i++){
    mi[i] = {0, 0};
  }
  for(int i = 1; i <= m; i++){
    int fx = Getf(e[i].u), fy = Getf(e[i].v);
    if(fx != fy){
      if(mi[fx].first == 0 || mi[fx].second > e[i].w){
        mi[fx] = {fy, e[i].w};
      }
      if(mi[fy].first == 0 || mi[fy].second > e[i].w){
        mi[fy] = {fx, e[i].w};
      }
    }
  }
  bool ret = 0;
  for(int i = 1; i <= n; i++){
    if(Getf(i) != i || mi[i].first == 0) continue;
    int fx = Getf(i), fy = Getf(mi[i].first);
    if(fx != fy){
      fa[fx] = fy, ans += mi[i].second;
      ret = 1;
    }
  }
  return ret;
}

int main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1, U, V, W; i <= m; i++){
    cin >> U >> V >> W;
    e[i] = {U, V, W};
  }
  for(int i = 1; i <= n; i++) fa[i] = i;
  while(work());
  cout << ans;
  return 0;
} 

应用

AT - Built?

CF - Jumping Around

CF - Xor-MST

一个无向完全图,\(n\) 个节点,任意两个节点之间的边权为 \(a_i ⊕ a_j\),求最小生成树。

\(1 \le n \le 2 \cdot 10^5\)\(0 \le a_i < 2^{30}\)

时间限制2.00s 内存限制250.00MB

解法

求完全图的最小生成树,一般使用 brovka 算法。

但是有的题目不需要实际写出 brovka 算法,只需要用到其思想,比如这题。

显然需要建立一个 01-trie,那么在合并连通块的时候,就是有两个儿子的节点左右合并,那么可以直接对于每个有两个儿子的节点求向下的最小异或值。

具体见代码

代码
#include <bits/stdc++.h>
 
using namespace std;
using LL = long long;
using PII = pair<int, int>;
 
const int MAXN = 2e5 + 3;

LL ans = 0;
int n;
int a[MAXN];
int ttop = 1, trie[29 * MAXN][2], cnt[29 * MAXN];
 
inline void Insert(int w, int id){ // 加入 01-trie 
  int p = 1;
  for(int l = 29; l >= 0; l--){
    int col = (w >> l) & 1;
    if(!trie[p][col]){
      trie[p][col] = ++ttop;
    }
    p = trie[p][col];
  }
}

LL C(int x, int y, int dep){ // 找向下延伸的最小异或值 
  if(dep < 0) return 0;
  if(!x || !y) return 1e16;
  if((trie[x][1] && trie[y][1]) || (trie[x][0] && trie[y][0])){
    return min(C(trie[x][1], trie[y][1], dep - 1), C(trie[x][0], trie[y][0], dep - 1));
  }
  return min(C(trie[x][1], trie[y][0], dep - 1), C(trie[x][0], trie[y][1], dep - 1)) + (1ll << dep);
}

void dfs(int x, int dep){ // 深搜便利 
  if(trie[x][0]) dfs(trie[x][0], dep - 1);
  if(trie[x][1]) dfs(trie[x][1], dep - 1);
  if(trie[x][0] && trie[x][1]){                               // 找到有两个儿子的节点 
    ans += C(trie[x][0], trie[x][1], dep - 1) + (1ll << dep); // 当前位不同,剩下的去搜索寻找 即 C 函数 
  }
}
 
int main(){
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for(int i = 1; i <= n; i++){
    cin >> a[i], Insert(a[i], i);
  }
  dfs(1, 29);
  cout << ans;
  return 0;
} 

次小生成树

对于求非严格次小生成树:

  • 枚举不在最小生成树中的一条边,然后求树上两点之间最大边权,然后替换。

对于求严格次小生成树。

  • 一样的做法,只不过需要求最大边权和次大边权。

只找到了 严格次小生成树 的题目:https://www.luogu.com.cn/problem/P4180

posted @ 2023-10-17 21:17  hhhqx  阅读(8)  评论(0编辑  收藏  举报