Skip to content

算法刷题【洛谷P4180 BJWC2010】严格次小生成树

信息学竞赛

2022-04-16

题目描述

小 C 最近学了很多最小生成树的算法,Prim 算法、Kruskal 算法、消圈算法等等。正当小 C 洋洋得意之时,小 P 又来泼小 C 冷水了。小 P 说,让小 C 求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说:如果最小生成树选择的边集是 EME_M,严格次小生成树选择的边集是 ESE_S,那么需要满足:(value(e)value(e) 表示边 ee 的权值) eEMvalue(e)<eESvalue(e)\sum_{e \in E_M}value(e) < \sum_{e \in E_S}value(e)

这下小 C 蒙了,他找到了你,希望你帮他解决这个问题。

输入格式

第一行包含两个整数 NNMM,表示无向图的点数与边数。

接下来 MM 行,每行 33 个数 x,y,zx,y,z 表示,点 xx 和点 yy 之间有一条边,边的权值为 zz

输出格式

包含一行,仅一个数,表示严格次小生成树的边权和。

输入输出样例

In 1:

5 6
1 2 1
1 3 2
2 4 3
3 5 4
3 4 3
4 5 6

Out 1:

11

数据范围

数据中无向图不保证无自环

对于 50%50\% 的数据, N2000N\le 2000M3000M\le 3000

对于 80%80\% 的数据, N5×104N\le 5\times 10^4M105M\le 10^5

对于 100%100\% 的数据, N105N\le 10^5M3×105M\le 3\times10^5,边权 [0,109]\in [0,10^9],数据保证必定存在严格次小生成树。

题解

写这道题之前首先让我们梳理一些易得的结论:

  • 最小生成树可能不唯一
  • 最小生成树中边的数量一定,为节点数 n1n-1
  • 次小生成树的形态(即不考虑边权的情况下)可能和最小生成树相同或不同

由这两条结论我们可以发现一个最基本的思路——做这道题时我们需要先建出最小生成树,再去依次尝试删去一条边加入一条原本不在最小生成树中的边,最后取权值和大于最小生成树的结果中最小的。这里的描述必须严谨,因为有可能出现多个权值和相等的最小生成树。

建最小生成树很简单,kruscal算法跑一遍就行了,但这次需要注意把没有用到的边记录下来,方便等会枚举尝试。

下面我们来谈谈,如果我拿到一条不在最小生成树中的边 ee,那么我该怎么处理来求得生成的次小生成树的权值大小。首先,根据树的性质我们可以知道,ee 的起点 e.frome.from 和终点 e.toe.to 两个点一定存在最近的公共父节点,画个图解释一下:

(画的不好请见谅)

如图,假设我们拿到的边 ee 有起点 e.from=Ce.from=C,终点 e.to=Ee.to=E,显然 CCEE 两个点的最近公共父节点是 BB,而加上边 ee 后形成了闭环 BDECBDEC

显然,如果我们加入边 ee 后还要保证图为一棵生成树,那么要删去一条边,且这一条边能且只能是闭环 BDECBDEC 中的边。这个结论显然,无需再证,在这里不能说服自己也不必较真。

根据最小生成树的性质,我们又知道,ee 的边权一定大于或等于闭环中除此之外的任意一条边。那么既然我们要求次小生成树,加入边 ee 后要删去的那条边一定就是闭环中除 ee 之外的长度最长的那条边。

至此我们可以梳理建立最小生成树后的流程了:

  1. 枚举拿到不在最小生成树中的边 ee
  2. 通过寻找 ee 的起点和终点的最近公共父节点,求出加入 ee 后图中出现的闭环
  3. 找到闭环中原本就存在的最大的一条边 emaxe_{max},设最小生成树的权值和为 wsumw_{sum},则建出的次小生成树权值和为 wsumemax.w+e.ww_{sum} - e_{max}.w + e.w

写到这里博主要去写数学作业了还要上课,LCA相关内容下次有空补上!有人看的话记得催更

又臭又长的代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 100001, M = 500001;

int ecnt, memcnt;
long long tot;

inline void read(int &a) {
    a = 0;
    char c = getchar();
    while (c < '0' || c > '9') {
        c = getchar();
    }
    while (c >= '0' && c <= '9') {
        a = (a << 3) + (a << 1) + c - '0';
        c = getchar();
    }
}

class BCJ {  // 供kruscal使用的并查集,建类可以更好地处理变量名
   private:
    int f[M];

   public:
    void init() {
        for (int i = 0; i < M; i++) f[i] = i;
    }
    int find(int x) {
        if (f[x] == x) return x;
        return f[x] = find(f[x]);
    }
    void merge(int x, int y) {
        x = find(x);
        y = find(y);
        f[y] = x;
    }
} bcj;

struct Edge {
    int from, to, w;
} edge[M], e[M];  // edge为所有边的信息,e为不在最小生成树中的边

bool cmp(Edge x, Edge y) {
    // 对所有边进行sort排序以供kruscal算法使用
    return x.w < y.w;
}

class Mem {
   public:
    struct POINT {
        // 点的相关信息
        int head, deep, fa[21];
        long long max1[21], max2[21];
    } p[N];
    int size;  // 边的数量
    struct EDGE {
        // 边的相关信息
        int to, w, next;
    } e[N * 2];
    void init() {
        size = 0;
        for (int i = 0; i < N; i++) {
            p[i].head = p[i].deep = 0;
            memset(p[i].fa, 0, sizeof(p[i].fa));
            memset(p[i].max1, 0, sizeof(p[i].max1));
            memset(p[i].max2, LONG_LONG_MAX, sizeof(p[i].max2));
        }
    }
    void add(Edge t) {
        ++size;
        e[size].next = p[t.from].head;
        e[size].to = t.to;
        e[size].w = t.w;
        p[t.from].head = size;
        ++size;
        e[size].next = p[t.to].head;
        e[size].to = t.from;
        e[size].w = t.w;
        p[t.to].head = size;
    }
} mem;

void dfs(int cur, int fa) {
    // 深度优先搜索,处理每个点的父节点信息和最大值信息
    mem.p[cur].deep = mem.p[fa].deep + 1;
    for (int i = 0; i <= 19; i++) {
        mem.p[cur].fa[i + 1] = mem.p[mem.p[cur].fa[i]].fa[i];
        mem.p[cur].max1[i + 1] =
            max(mem.p[cur].max1[i], mem.p[mem.p[cur].fa[i]].max1[i]);
        if (mem.p[cur].max1[i] == mem.p[mem.p[cur].fa[i]].max1[i])
            mem.p[cur].max2[i + 1] =
                max(mem.p[cur].max2[i], mem.p[mem.p[cur].fa[i]].max2[i]);
        else if (mem.p[cur].max1[i] < mem.p[mem.p[cur].fa[i]].max1[i])
            mem.p[cur].max2[i + 1] =
                max(mem.p[cur].max1[i], mem.p[mem.p[cur].fa[i]].max2[i]);
        else
            mem.p[cur].max2[i + 1] =
                max(mem.p[cur].max2[i], mem.p[mem.p[cur].fa[i]].max1[i]);
    }
    for (int i = mem.p[cur].head; i; i = mem.e[i].next) {
        // 对所有子节点进行dfs处理
        if (mem.e[i].to == fa) continue;
        mem.p[mem.e[i].to].fa[0] = cur;
        mem.p[mem.e[i].to].max1[0] = mem.e[i].w;
        mem.p[mem.e[i].to].max2[0] = INT_MIN;
        dfs(mem.e[i].to, cur);
    }
}

int lca(int x, int y) {
    // lca算法求两点的最近公共祖先
    long long ans = 0;
    if (mem.p[x].deep < mem.p[y].deep) swap(x, y);
    for (int i = 20; i >= 0; i--) {
        if (mem.p[mem.p[x].fa[i]].deep >= mem.p[y].deep) x = mem.p[x].fa[i];
        if (x == y) return x;
    }
    for (int i = 20; i >= 0; i--) {
        if (mem.p[x].fa[i] != mem.p[y].fa[i]) {
            x = mem.p[x].fa[i];
            y = mem.p[y].fa[i];
        }
    }
    return mem.p[x].fa[0];
}

long long qmax(int x, int y, int w) {
    // 求两点间的最大边
    long long ans = LONG_LONG_MIN;
    for (int i = 20; i >= 0; i--) {
        if (mem.p[mem.p[x].fa[i]].deep >= mem.p[y].deep) {
            if (w != mem.p[x].max1[i]) {
                ans = max(ans, mem.p[x].max1[i]);
            } else {
                ans = max(ans, mem.p[x].max2[i]);
            }
            x = mem.p[x].fa[i];
        }
    }
    return ans;
}

int main() {
    bcj.init();
    mem.init();
    int n, m;
    read(n);
    read(m);
    for (int i = 1; i <= m; i++) {
        read(edge[i].from);
        read(edge[i].to);
        read(edge[i].w);
    }
    sort(edge + 1, edge + m + 1, cmp);
    for (int i = 1; i <= m; i++) {
        if (bcj.find(edge[i].from) != bcj.find(edge[i].to)) {
            bcj.merge(edge[i].from, edge[i].to);
            tot += edge[i].w;
            mem.add(edge[i]);
        } else {
            // 记录未加入最小生成树的边
            ecnt++;
            e[ecnt] = edge[i];
        }
    }

    long long ans = LONG_LONG_MAX;
    dfs(1, 0);
    for (int i = 1; i <= ecnt; i++) {
        if (e[i].from == e[i].to) continue;
        int t = lca(e[i].from, e[i].to);
        long long res =
            max(qmax(e[i].from, t, e[i].w), qmax(e[i].to, t, e[i].w));
        if (res < 0) continue;
        ans = min(ans, tot + e[i].w - res);
    }
    cout << ans << endl;
    return 0;
}

Ukraine 在俄罗斯对乌克兰发动的野蛮的侵略战争中矢志不渝地支持乌克兰