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

异想之旅:本人原创博客完全手敲,绝对非搬运,全网不可能有重复;本人无团队,仅为技术爱好者进行分享,所有内容不牵扯广告。本人所有文章仅在CSDN、掘金和个人博客(一定是异想之旅域名)发布,除此之外全部是盗文!


洛谷 P4180 [BJWC2010]严格次小生成树

题目描述

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

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

输入格式

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

接下来 M M M 行,每行 3 3 3 个数 x , y , z x,y,z x,y,z 表示,点 x x x 和点 y y y 之间有一条边,边的权值为 z z z

输出格式

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

输入输出样例

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\% 50% 的数据, N ≤ 2000 N\le 2000 N2000 M ≤ 3000 M\le 3000 M3000

对于 80 % 80\% 80% 的数据, N ≤ 5 × 1 0 4 N\le 5\times 10^4 N5×104 M ≤ 1 0 5 M\le 10^5 M105

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

题解

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

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

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

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

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

在这里插入图片描述

(画的不好请见谅)

如图,假设我们拿到的边 e e e 有起点 e . f r o m = C e.from=C e.from=C,终点 e . t o = E e.to=E e.to=E,显然 C C C E E E 两个点的最近公共父节点是 B B B,而加上边 e e e 后形成了闭环 B D E C BDEC BDEC

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

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

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

  1. 枚举拿到不在最小生成树中的边 e e e
  2. 通过寻找 e e e 的起点和终点的最近公共父节点,求出加入 e e e 后图中出现的闭环
  3. 找到闭环中原本就存在的最大的一条边 e m a x e_{max} emax,设最小生成树的权值和为 w s u m w_{sum} wsum,则建出的次小生成树权值和为 w s u m − e m a x . w + e . w w_{sum} - e_{max}.w + e.w wsumemax.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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

异想之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值