Loading

最小生成树(复习总结)

最小生成树的基本应用

对于稠密图

对于稠密图可以采用\(Prime\)算法,其思想就是从原点开始出发,在已确定的点连接未确定的点的边中,选取长度最小的一条,假如到最小生成树中

Code

int g[N][N], d[N], n;
bool st[N];

int prime(int start) {
  memset(d, 0x3f, sizeof d);
  d[start] = 0;
  
  for (int i = 1; i < n; i++) {
    int x = 0;
    for (int j = 1; j <= n; j++) if (!st[j] && (!x || d[j] < d[x]))
      x = j;
    st[x] = 1;
    
    for (int y = 1; y <= n; y++) if (!st[y])
      d[y] = min(d[y], g[x][y]);
  }
  
  int res = 0;
  for (int i = 1; i <= n; i++) res += d[i];
  return res;
}

最短网络

Prime的板子题,什么好说的

稀疏图

对于稀疏图用\(Kruskal\)算法,其原理是从贪心的角度出发,按照边权从小到大选取边且每个点最多只能连接两个边。中间用并查集进行优化,具体过程如下:

  1. 将所有的边从小到大排序,遍历每个边及其顶点
  2. 如果两个点不在同一联通块中,那么将两个点合并到同一联通块中,边权加入最小生成树的权值中
struct Edge {
  int a, b, w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[N];
int n, p[N], k;

int kruskal() {
  int res = 0;
  // 并查集初始化
  for (int i = 1; i <= n; i++) p[i] = i;
  sort(edge, edge + k);
  
  for (int i = 0; i < k; i++) {
    auto [a, b, c] = edge[i];
    int fa = find(a), fb = find(b);
    if (fa == fb) res += w;
    else p[fa] = fb;
  }
  
  return res;
}

局域网

板子题,不想多说

繁忙的都市

板子题,不过求的是最大值

联络员

p==1的边直接加入生成树后,再存下p==2的边,对剩下的边求最小生成树

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 10010;
struct Edge {
  int a, b, w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[N];

int n, m, k, p[N];

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

int main() {
  int res = 0;
  scanf("%d%d", &n, &m);
  for (int i = 1; i <= n; i++) p[i] = i;
  while (m --) {
    static int c, a, b, w;
    scanf("%d%d%d%d", &c, &a, &b, &w);
    if (c == 1) res += w, p[find(a)] = find(b);
    else edge[k++] = {a, b, w};
  }
  
  sort(edge, edge + k);
  
  for (int i = 0; i < k; i++) {
    auto [a, b, w] = edge[i];
    int fa = find(a), fb = find(b);
    if (fa != fb) {
      res += w;
      p[fa] = fb;
    }
  }
  
  printf("%d\n", res);
  
  return 0;
}

连接格点

将整个矩阵展开至一维,则有\((i,j)=i*m+j\)

这样就可以用Kruskal来解决这道题,由于纵向的权值小于横向的,所以处理完所有存在的边后,先对所有纵向的边枚举建边,再枚举横向即可。

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 1110, M = N * N;

int n, m, p[M];
struct Edge {
  int a, b, w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[M];

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

int main() {
  int x1, y1, x2, y2, res = 0;
  scanf("%d%d", &n, &m);
  for (int i = 0; i < m * n; i++) p[i] = i;
  
  while (~scanf("%d%d%d%d", &x1, &y1, &x2, &y2)) {
    int a = (x1 - 1) * m + y1 - 1, b = (x2 - 1) * m + y2 - 1;
    p[find(a)] = find(b);
  }

  for (int i = 0; i < (n - 1) * m; i++) {
    int a = find(i), b = find(i + m);
    if (p[a] != p[b]) {
      p[a] = b;
      res++;
    }
  }

  for (int i = 0; i < n * m; i++) {
    if (i % m < m - 1) {
      int a = find(i), b = find(i + 1);
      if (a != b) {
        p[a] = b;
        res += 2;
      }
    }
  }

  printf("%d\n", res);

  return 0;
}

最小生成树的拓展应用

新的开始 - 虚拟原点+最小生成树

本题有两种操作,一个是直接在图上建立发电站,另一种是从发电站向其他点建立电网。

由于每个点都可以建立电站,那么我们可以引入虚拟原点0,这样每个点单独建立电站就转化成了从虚拟原点0向每个点建立一条边,这样问题就转换成了求最小生成树。

Code

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 1e5 + 100, M = 330;

struct Edge {
  int a, b, w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[N];

int n, k, g[M][M], p[M], res;

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    int x; scanf("%d", &x);
    edge[k++] = {0, i, x};
    p[i] = i;
  }
  
  for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) {
    scanf("%d", &g[i][j]);
    if (j > i) edge[k++] = {i, j, g[i][j]};
  }
  sort(edge, edge + k);
  
  for (int i = 0; i < k; i++) {
    auto [a, b, w] = edge[i];
    int fa = find(a), fb = find(b);
    if (fa != fb) {
      p[fa] = fb;
      res += w;
    }
  }
  
  printf("%d\n", res);
  
  return 0;
}

北极通讯网络 - 贪心

由题意可知,若干个点构成一棵生成树,满足的\(d\)值一定是其中最大的边权。

但是如果有\(k\)条边的限制,\(k\)越多,\(d\)也就越小。若想要让\(d\)最小,那么一定要在一棵最小生成树里寻找边的值;要想让所有点都能连接,那么就应该先满足边权大的边。

所以本题的答案就是求图中的所有点的构成的最小生成树中,第\(k\)大的边。

注意

  1. 0台设备时等同于1台设备
  2. 当要求的卫星台数大于地点数,\(d=0\)

Code

#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;

typedef pair<int, int> PII;

constexpr int N = 1e6 + 100, M = 550;

PII node[M];
struct Edge {
  int a, b;
  double w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[N];

int n, k, p[N], cnt, m;

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

inline double cal(PII A, PII B) {
  auto [a, b] = A;
  auto [l, r] = B;

  return sqrt(1.0 * (a - l) * (a - l) + (b - r) * (b - r));
}

inline double kruskal() {
  if (n <= k) return 0;
  double s[M];
  int cnt = 0;

  for (int i = 0; i < m; i++) {
    auto [a, b, w] = edge[i];
    int fa = find(a), fb = find(b);
    if (fa != fb) {
      p[fa] = fb;
      s[cnt++] = w;
    }
  }

  return s[cnt - k];
}

int main() {
  scanf("%d %d", &n, &k);
  for (int i = 1; i <= n * n; i++) p[i] = i;
  k = max(1, k);

  for (int i = 1; i <= n; i++) {
    int a, b; scanf("%d%d", &a, &b);
    node[i] = {a, b};
  }

  for (int i = 1; i < n; i++) for (int j = i + 1; j <= n; j++)
    edge[m++] = {i, j, cal(node[i], node[j])};

  sort(edge, edge + m);

  printf("%.2lf\n", kruskal());

  return 0;
}

走廊泼水节

题目描述的很明确,难点在于如何去构造。

考虑Kruskal算法的过程:

  1. 将所有的边按照从小到大的顺序排列
  2. 如果对于一条边\(u\to v\),假如他们不在同一个联通块中,那么他们之间会选择最短的一条边连接

在本题中,除了要连接最短的边\(w\),还要让其他点都相互连接起来,为了保证只有这一个最小生成树,那么其他边的边权应该为\(w+1\)。两个联通块中的所有点相互连一条边,那么会有\(cnt_a\times cnt_b\)条边,排除掉最小的边,新图要添加的边权为\((w+1)\times (cnt_a\times cnt_b-1)\)条边。

Code

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 6600;

int n, p[N], cnt[N];

struct Edge {
  int a, b, w;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[N];

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

int main() {
  int _T;
  scanf("%d", &_T);
  while (_T --) {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) p[i] = i, cnt[i] = 1;
    for (int i = 1; i < n; i++) {
      int a, b, w; scanf("%d%d%d", &a, &b, &w);
      edge[i] = {a, b, w};
    }
    
    sort(edge + 1, edge + n);
    int res = 0;
    
    for (int i = 1; i < n; i++) {
      auto [a, b, w] = edge[i];
      a = find(a), b = find(b);
      if (a != b) {
        p[a] = b;
        res += (w + 1) * (cnt[a] * cnt[b] - 1);
        cnt[b] += cnt[a];
      }
    }
    
    printf("%d\n", res);
  }
  
  return 0;
}

秘密的牛奶运输 - 次小生成树

本题就是让你求一个最小生成树

求出最小生成树后,对生成树的每一条边用非树边进行替换:

  1. 当非树边大于生成树的最大边时,非树边替换最大边,计算权值和,更新答案
  2. 当非树边小于生成树的最大边时,如果非树边大于第二大的边,那么替换后更新一下答案

由于本题的数据量较小,所以可以直接通过遍历图的形式求解,求出任意两点间的最小边和次小边。最终答案为\(\max(Sum+w-d)\)

Code

#include <iostream>
#include <algorithm>
using namespace std;

constexpr int N = 550, M = 20010;

typedef long long LL;
struct Edge {
  int a, b, w;
  bool f;
  bool operator < (const Edge &W) const {
    return w < W.w;
  }
} edge[M / 2];

int n, m, p[N], d1[N][N], d2[N][N];
int h[N], e[M], ne[M], w[M], idx;

int find(int x) {
  if (p[x] != x) p[x] = find(p[x]); return p[x];
}

inline void add(int a, int b, int c) {
  e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}

void dfs(int u, int fa, int max1, int max2, int d1[], int d2[]) {
  d1[u] = max1, d2[u] = max2;

  for (int i = h[u]; i; i = ne[i]) {
    int j = e[i];
    if (j == fa) continue;
    int x = max1, y = max2;

    if (w[i] > x) {
      y = x, x = w[i];
    } else if (w[i] < x && w[i] > y) y = w[i];
    dfs(j, u, x, y, d1, d2);
  }
}

int main() {
  scanf("%d%d", &n, &m);

  for (int i = 1; i <= n; i++) p[i] = i;
  for (int i = 0; i < m; i++) {
    int a, b, w; scanf("%d%d%d", &a, &b, &w);
    edge[i] = {a, b, w};
  }
  sort(edge, edge + m);

  LL sum = 0, res = 1e18;

  for (int i = 0; i < m; i++) {
    auto &[a, b, w, f] = edge[i];
    int fa = find(a), fb = find(b);
    if (fa != fb) {
      add(a, b, w), add(b, a, w);
      p[fa] = fb;
      f = 1;
      sum += w;
    }
  }

  for (int i = 1; i <= n; i++) dfs(i, -1, -1e9, -1e9, d1[i], d2[i]);

  for (int i = 0; i < m; i++) {
    auto [a, b, w, f] = edge[i];
    if (!f) {
      if (w > d1[a][b]) res = min(res, sum + w - d1[a][b]);
      else if (w > d2[a][b]) res = min(res, sum + w - d2[a][b]);
    }
  }

  printf("%lld\n", res);

  return 0;
}
posted @ 2022-03-17 18:05  Frank_Ou  阅读(72)  评论(0编辑  收藏  举报