【数据结构】最小生成树

最小生成树

前置知识

  • 并查集

  • 图论

概念

条件

最小生成树的满足条件为:

  • 在无向图中选取总权值最少的边让所有点连通。

  • 要求结果是一棵树,边数比点数少 \(1\)

当然,最小生成树的结果可能不唯一。

特性

  • 图中任意一条非树边都会和树边构成一个环。
  • 非树边一定是环中最大的边。否则可以替换掉最大的边,得到一个更小生成树。

其他

最小生成树有两种算法:Kruskal 和 Prim。

Kruskal 是在并查集的基础上展开。

Prim 是在最短路的基础上展开。

Kruskal

介绍

我们可以从选边的角度来构造生成树,通过点的连通性来对边的选取进行判断。

我们按权值由小到大选择不成环的边加入树中。(由树的特性可知,成环时该边一定是环中最大的边,不应该选取)。如果选出的边比点数少 \(1\),说明最小生成树存在,否则不存在。

那么如何快速的判断是否成环?我们可以使用并查集来合并点及判断点的连通性。

步骤

  1. 读入所有边并初始化并查集(\(f_i = i\))。

  2. 按权值排序。

  3. 合并所有边并计算答案。

  4. 输出。

Prim

可以从选点的角度来构造生成树,不断将新的点拉入生成树中。

步骤

  1. 读入所有边。

  2. 把所有点的距离设为 \(\infty\),并将任意一点距离设为 \(0\)

  3. 寻找没有进入生成树且距离最小的点进入生成树并累加距离到答案。

  4. 更新相邻点距离。

  5. 重复 \(3, 4\) 步直到所有点进入生成树。

  6. 输出。

证明

假定当前的生成树为 \(A\),且 \(x\) 是离生成树最近的点。如果 \(x\)\(A\) 的边在最终的生成树中,那么现在可以将 \(x\) 拉入生成树中。如果 \(x\)\(A\) 相连的边不在最终的生成树中,那么 \(x\) 必然要通过另一个当前与 \(A\) 直接相连的点 \(y\),间接与 \(A\) 相连。那么 \(xA,yA,xy\) 构成环,且 \(xA\)\(yA\) 更短,替换后可以得到更小生成树。

例题 1(洛谷 P3366 【模板】最小生成树)

给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz

原题:https://www.luogu.com.cn/problem/P3366

【Kruskal】

代码是以前写的,可能不好看。

#include <bits/stdc++.h>
using namespace std;
#define qwq ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
const int N = 200000 + 20, M = 5000 + 50;

struct node {
  int x, y, z;
  friend bool operator<(const node &a, const node &b) {
    return a.z < b.z;
  }
};

node edge[N];
int n, m, x, y, z, f[M], l[M], ans = 0;
bool b = 1;

int find(int i) {
  if (f[i] == i)
    return i;
  return f[i] = find(f[i]);
}

void merge(int u, int v) {
  u = find(u), v = find(v);
  if (l[u] > l[v]) {
    swap(l[u], l[v]);
  }
  f[u] = v;
  l[v] += l[u];
}

int main() {
  qwq;
  cin >> n >> m;
  for (int i = 1; i <= n; ++i) {
    f[i] = i, l[i] = 1;
  }
  for (int i = 1; i <= m; ++i) {
    cin >> edge[i].x >> edge[i].y >> edge[i].z;
  }
  sort(edge + 1, edge + m + 1);
  for (int i = 1; i <= m; ++i) {
    if (find(edge[i].x) == find(edge[i].y))
      continue;
    merge(edge[i].x, edge[i].y);
    ans += edge[i].z;
    if (l[find(edge[i].x)] == n || l[find(edge[i].y)] == n) {
      b = 0;
      break;
    }
  }
  if (b)
    cout << "orz";
  else
    cout << ans;
  return 0;
}

【Prim】

(摘自洛谷题解)

#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#define R register int
using namespace std;

int k, n, m, cnt, sum, ai, bi, ci, head[5005], dis[5005], vis[5005];

struct Edge
{
  int v, w, next;
} e[400005];

void add(int u, int v, int w)
{
  e[++k].v = v;
  e[k].w = w;
  e[k].next = head[u];
  head[u] = k;
}

typedef pair<int, int> pii;
priority_queue<pii, vector<pii>, greater<pii>> q;

void prim()
{
  dis[1] = 0;
  q.push(make_pair(0, 1));
  while (!q.empty() && cnt < n)
  {
    int d = q.top().first, u = q.top().second;
    q.pop();
    if (vis[u])
      continue;
    cnt++;
    sum += d;
    vis[u] = 1;
    for (R i = head[u]; i != -1; i = e[i].next)
      if (e[i].w < dis[e[i].v])
        dis[e[i].v] = e[i].w, q.push(make_pair(dis[e[i].v], e[i].v));
  }
}

int main()
{
  memset(dis, 127, sizeof(dis));
  memset(head, -1, sizeof(head));
  scanf("%d%d", &n, &m);
  for (R i = 1; i <= m; i++)
  {
    scanf("%d%d%d", &ai, &bi, &ci);
    add(ai, bi, ci);
    add(bi, ai, ci);
  }
  prim();
  if (cnt == n)
    printf("%d", sum);
  else
    printf("orz");
}

例题 2(洛谷 P1550 [USACO08OCT] Watering Hole G)

Farmer John 决定将水引入到他的 \(n\) 个农场。他准备通过挖若干井,并在各块田中修筑水道来连通各块田地以供水。在第 \(i\) 号田中挖一口井需要花费 \(W_i\) 元。连接 \(i\) 号田与 \(j\) 号田需要 \(P_{i,j}\)\(P_{j,i}=P_{i,j}\))元。

每次输入 \(w_i\) 时,把 \(i\)\(n + 1\) 连接,边权为 \(w_i\)

然后在输入 \(p\) 数组后,套一个双层循环,如果 \(i \neq j\),把 \(i\)\(j\) 连接
,边权为 \(p_{i, j}\)

存完边之后按照边权排序。排序后跑一遍 Kruskal 即可。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>

using namespace std;

using ll = long long;

#define mtest for (cin >> t; t; -- t)

const int kMaxN = 1e5 + 10, kMaxM = 1010, kInf = (((1 << 30) - 1) << 1) + 1;
const ll kLInf = 9.22e18;

int n, w[kMaxN], f[kMaxN], p[kMaxM][kMaxM], ans = 0;

struct node {
  int u, v, w;
} e[kMaxN];

int F(int x) {
  return f[x] == x? x : f[x] = F(f[x]);
}

bool cmp(node a, node b) {
  return a.w < b.w; // 注意是 <
}

void U(int u, int v, int w) {
  int x = F(u), y = F(v);
  if (x != y) {
    f[f[u]] = f[v];
    ans += w;
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n;
  for (int i = 1; i - 1 <= n; ++ i) { // 初始化
    f[i] = i;
  }
  int id = 0;
  for (int i = 1; i <= n; ++ i) { // 村边
    e[++ id].u = i;
    e[id].v    = n + 1;
    // i & n + 1 连接
    cin >> e[id].w;
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      cin >> p[i][j];
    }
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      if (i == j) {
        continue;
      }
      e[++ id].u = i;
      e[id].v = j;
      // i & j 连接
      e[id].w = p[i][j];
    }
  }
  sort(e + 1, e + id + 1, cmp); // 按边权排序
  for (int i = 1; i <= id; ++ i) { // 跑 Kruskal 
    U(e[i].u, e[i].v, e[i].w); 
  }
  cout << ans << '\n';
  return 0;
}

关联习题

  1. 洛谷 P1194 / P1195 / P1396 / P2330 / P2700 / P4047。

  2. CF / AT 上面关于最小生成树的题。

posted @ 2023-12-23 17:08  beautiful_chicken233  阅读(177)  评论(0)    收藏  举报