You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 356 次小生成树

一、题目描述

给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树

设最小生成树的边权之和为 sum,严格次小生成树就是指边权之和大于 sum 的生成树中最小的一个。

输入格式 第一行包含两个整数 NM

接下来 M 行,每行包含三个整数 xyz,表示点 x 和点 y 之前存在一条边,边的权值为 z

输出格式 包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)

数据范围 N≤10^5,M≤3×10^5,1≤x,y≤N,0≤z≤10^6

输入样例

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

输出样例

11

二、题目分析

本题要求 严格次小生成树,之前在AcWing 1148 秘密的牛奶运输 里也曾求过次小生成树,但是本题的 数据范围更大

解释:本题目N=1e5,太大,无法使用 AcWing 1148. 秘密的牛奶运输 的办法(开二维数组,记录x->y间的 最大边权次大边权),直接 MLE了。 AcWing 1148 的那个dfs+kruskal求严格次小生成树的办法,时间复杂度TLE,空间MLE,需要一个更牛的 求严格次小生成树 的办法。

秘密的牛奶运输的的性能为什么可以优化呢?这是因为每次执行dfs求最小生成树上两点路径中的 最大边权次大边权 的时间复杂度是O(n),总的时间复杂度就是O(n^2),求任意两点间的最短距离可以想办法优化。

通过 倍增算法,可以高效的记录任意两点间的最长边和严格次长边,而且是O(log_2N),比上面的O(n)快。

本题总的解题流程与秘密的牛奶运输那题基本一致,只是在求 最长边、严格次长边 的方法上有所不同。

思路

d[i][k]

树上节点i向上走2^k步到达的节点,状态转移方程

\large d[i][k] = d[d[i][k-1]][k-1]

j=d[i][k-1],状态转移方程

\large d[i][k] = d[j][k-1]

f[i][k]

id[i][k]节点路径上的最大边权、次大边权

f[i][k].first的求解很显然:

\large f[i][k].first = max(f[i][k-1].first,f[j][k-1].first)

f[i][k].second的求解分情况讨论:

if(f[i][k-1].first == f[j][k-1].first)

\large f[i][k].second = max(f[i][k-1].second,f[j][k-1].second)

f[i][k-1].first > f[j][k-1].first

\large f[i][k].second = max(f[i][k-1].second,f[j][k-1].first)

f[i][k-1].first < f[j][k-1].first

\large f[i][k].second = max(f[i][k-1].first,f[j][k-1].second)

求出了树上任意一点向上走2^k步路径中的 最大边权次大边权 并不是求解本题的终点 ,我们需要的是求解树上任意两点间的最大边权和次大边权。

回忆下求节点a和节点bLCA的过程,我们先将深度较大的a节点不断向上跳,直到跳到与b节点 同一深度为止,如果此时ab不重合,则继续将ab以同样的步数向上跳,直到ab的父节点是同一个为止。

LCA的树上倍增的过程也可以用来 求最大和次大边权,只需要在跳的过程中 同步更新最大边权和次大边权 即可。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 100010, M = 300010;
const int INF = 0x3f3f3f3f;

int f[N][16];    // f[i][k]表示树上的某节点i向上走2^k步到达的节点
PII dist[N][16]; // d[i][k]表示树上的某节点i向上走2^k步到达的节点最长距离和次长距离
int depth[N];    // 深度数组

// Kruskal结构体
struct Edge {
    int a, b, c; // 从a到b边权为c
    bool flag;   // 是不是最小生成树的树边
    const bool operator<(const Edge &t) const {
        return c < t.c;
    }
} edge[M];

// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

// 并查集
int p[N];
int find(int x) {
    if (x == p[x]) return x;
    return p[x] = find(p[x]);
}

// 树上倍增求任意点到2^0,2^1,2^2,...的距离,注意,不是任意两点间最长距离和次长距离,是半成品,不是成品!
int bfs(int root) {
    queue<int> q;
    q.push(root);
    depth[root] = 1;
    while (q.size()) {
        int u = q.front();
        q.pop();
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (!depth[v]) {
                depth[v] = depth[u] + 1; // 记录深度
                q.push(v);
                f[v][0] = u; // 记录2^0->t,描述父节点

                //-->下面是与普通的倍增不一样的代码<--
                // v->u 的最大距离:w[i],与次大距离:-INF,递推初始化数据
                dist[v][0] = {w[i], -INF};

                for (int k = 1; k <= 15; k++) { // 倍增
                    int x = f[v][k - 1];        // 设v跳2 ^(k-1)到达的是x点
                    f[v][k] = f[x][k - 1];      // x点跳2^(k-1)到达的终点就是v跳2^k的终点

                    //-->下面是与普通的倍增不一样的代码<--
                    // ①最大边权
                    dist[v][k].first = max(dist[v][k - 1].first, dist[x][k - 1].first);
                    // ②次大边权
                    if (dist[v][k - 1].first == dist[x][k - 1].first)                          // 如果前半部分最大距离等于后半部分最大距离
                        dist[v][k].second = max(dist[v][k - 1].second, dist[x][k - 1].second); // 整体次大=max(前半次大,后半次大)
                    else if (dist[v][k - 1].first < dist[x][k - 1].first)                      // 如果前半最大小于后半最大
                        dist[v][k].second = max(dist[v][k - 1].first, dist[x][k - 1].second);  // 整体次大=max(前半最大,后半次大)
                    else                                                                       // 如果前半最大大于后半最大
                        dist[v][k].second = max(dist[v][k - 1].second, dist[x][k - 1].first);  // 整体次大=max(前半次大,后半最大)
                }
            }
        }
    }
}

// 因为同时需要同步修改最大值和次大值,所以采用了地址符&引用方式定义参数
//  m1:最大值m2次大值
void update(int &m1, int &m2, PII x) {
    if (m1 == x.first)
        m2 = max(m2, x.second);
    else if (m1 < x.first)
        m2 = max(m1, x.second), m1 = x.first;
    else
        m2 = max(m2, x.first);
}

// 功能添加上a->b的边,边权是c,去掉a->b的原来最大或次大比最小生成树多出来多少边权(c- m1 或者 c-m2)
// 思路:利用倍增思想,找出a->b之间的最大距离和次大距离
// 具体是-m1,还是-m2要区别对待如果c=m1,就是-m2,否则-m1
int lca(int a, int b, int c) {
    if (depth[a] < depth[b]) swap(a, b);  // 保证a的深度大于b的深度
    int m1 = -INF, m2 = -INF;             // 最大边,次大边初始化
    for (int k = 15; k >= 0; k--)         // 由小到大尝试
        if (depth[f[a][k]] >= depth[b]) { // 让a向上跳2^k步
            update(m1, m2, dist[a][k]);   // 记录a到向上走2^k步这段范围内遇到的最大和次大长度
            a = f[a][k];                  // 标准lca
        }

    // 当a与b不是同一个点时
    // 此时两者必然是depth一样的情况同时向上查询2^k必然可以找到LCA
    if (a != b) {
        for (int k = 15; k >= 0; k--)
            if (f[a][k] != f[b][k]) {
                update(m1, m2, dist[a][k]); // 记录a到向上走2^k步这段范围内遇到的最大和次大长度
                update(m1, m2, dist[b][k]); // 记录b到向上走2^k步这段范围内遇到的最大和次大长度
                // 注意写在a=f[a][k],b=f[b][k]上方要不a,b就被改了此句就不对了
                a = f[a][k], b = f[b][k];
            }
        // 此时a和b到lca下同一层 所以还要各跳1步=跳2^0步
        update(m1, m2, dist[a][0]);
        update(m1, m2, dist[b][0]);
    }
    // m1,m2中装的是 a->b之间的最大边权和次大边权现在给了一个新边权c,它能替换m1,m2,还是谁也替换不了呢?
    // 因为m1,m2是最小生成树中的最大边权和次大边权c >= m1 > m2
    // if(c==m1) 那么c能去替换m2,获取的收益就是c-m2
    // if(c> m1) 那么c能去替换m1,获取的收益就是c-m1
    return c == m1 ? c - m2 : c - m1;
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);

    // 并查集初始化
    for (int i = 1; i <= n; i++) p[i] = i;
    // 邻接表初始化
    memset(h, -1, sizeof h);

    // Kruskal
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        edge[i] = {a, b, c, 0};
    }

    // 按边权排序+最小生成树
    sort(edge, edge + m);

    LL sum = 0, ans = 1e18;

    // Kruskal
    for (int i = 0; i < m; i++) {
        int a, b, c;
        a = find(edge[i].a), b = find(edge[i].b), c = edge[i].c;
        if (a != b) {
            p[a] = b;

            sum += c;         // 最小生成树的边权总和
            edge[i].flag = 1; // 标识为最小生成树中的边,因为后面要枚举非树边
            // 将最小生成树中的树边单独构建一个图出来
            add(edge[i].a, edge[i].b, c), add(edge[i].b, edge[i].a, c);
        }
    }

    // 倍增预处理,记录任意点向上2^k步的最大值次大值深度等信息后面lca会用到
    // 以任意点为根
    bfs(1);

    // 用非树边去尝试替换最小生成树中的边然后取min
    for (int i = 0; i < m; i++)
        if (!edge[i].flag) { // 枚举非树边
            int a = edge[i].a, b = edge[i].b, c = edge[i].c;
            ans = min(ans, sum + lca(a, b, c));
            // 在最小生成树中将a,b两点间连接一条长度为c的边,相对最小生成树,长度会增加多少呢?
        }
    // 输出
    printf("%lld\n", ans);
    return 0;
}