严格次小生成树

模板题号:P4180
题意:给定一个 \(n\) 个点,\(m\) 条边的图,求它的严格慈孝次小生成树。
严格次小生成树,就是只一个图的边权和大于最小生成树的边权和最小的生成树。我们非常容易想到暴力的做法:枚举枚举选哪 \(n - 1\) 边,再判断是否是原图的生成树,再比大小。但是这样时间复杂度有亿点点高,因此这个方法是不可行的。
既然严格次小生成树与最小生成树有关,那么我们可以先求出原图的最小生成树,再考虑怎么得出严格次小生成树。我们先看样例,可以画出这幅图:

不难得出,它的最小生成树是这样的(标红的边为最小生成树的边):

在这棵最小生成树上,我们必须通过换边的操作来计算严格次小生成树的边权和。
但是换边也不是随便换的,还要满足以下条件:
1.换下来的边一定要比被换边大,不然得出的还是最小生成树。
2.当我们在树上连接被换边的两端,一定会得到一个环,我们换下来的边一定要是环上的边。否则,换边以后得结果就不是一棵树了。
不难得知,这样换边最多只能换一次,不然得到的生成树的边权和一定比只换一次大。
因此,我们可以从不在最小生成树的边中选一条边作为被换边,然后从组成的环中选择一条满足要求的最大的边,计算答案并取最小值即可。
还是那样例来说,加入我们选了起点为 \(2\) 号点,终点为 \(4\) 号点,边权为 \(3\) 的那条边,则连接两条边后的图如下(红色代表已选边):

不难看出,我们需要选择起点为 \(1\),重点为 \(3\),边权为 \(2\) 的边,把它换下来,就可以得到新的生成树:

它的边权和为 \(11\)。在换边完成之后,我们需要把换过的边还原,不然下一次操作一定不是最优解。
同理,我们可以得到,换上另一条非树边后,得到的生成树是这样的:

它的边权和为 \(12\)
因此,我们可以得到原图的严格次小生成树的边权和为 \(11\)
在代码实现上,如果需要换上来的边的起点为 \(u\),终点为 \(v\),边权为 \(w\),我们可以先用倍增求出他们的LCA,设为节点 \(l\)。这时,我们需要求出 \(u\)\(v\) 的路径上的边权最大值和次大值,防止最大边权与 \(v\) 相等。对于着这个操作,我们可以先求出 \(u\)\(l\) 路径上的边权最大值和次大值,再求出 \(v\)\(l\) 路径上的边权最大值与次大值,再合并答案即可。这里设求出来的最大值为 \(max1\),次大值为 \(max2\),最小生成树的边权和为 \(ans\)。然后,如果 \(max1 \neq v\),则返回 \(ans - max1 + v\);如果 \(max1 = v\) 且存在次大值,则返回 \(ans - max2 + v\),否则返回 \(\infty\)
但是这样的时间复杂度太高,需要优化。
我们回顾一下倍增求LCA的过程,就是存储一个节点的第 \(2^x\) 个祖先,最后可用 \(O(logn)\) 的时间复杂度来求解。同理,我们也可以用倍增的方式快速求出一个点到它的某一个祖先的边权中的最大值与次大值。设 \(mx_{u, x}\) 为节点 \(u\) 向上 \(2^x\) 条边的边权的最大值,\(mx2_{u, x}\) 为节点 \(u\) 向上 \(2^x\) 条边的边权的次大值。这时,我们只需要按照与倍增求LCA一样的步骤去求一个节点到它的任意一个祖先的边权最大值与次大值了。
这个方法的代码如下:

#include <bits/stdc++.h>

#define MAXN 111111
#define MAXM 333333
#define LOGMAXN (int)(log2(MAXN) + 5)
#define int long long
#define inf LLONG_MAX

using namespace std;

struct Node {
    int u, v, w;
    bool used; //used存储这条边是否在最小生成树中
}edge[MAXM];

int mx[MAXN][LOGMAXN], mx2[MAXN][LOGMAXN], f[MAXN][LOGMAXN]; //f数组存储向上第2^x个祖先
int fa[MAXN], dep[MAXN]; //dep存储深度
int n, m, ans;

vector <pair<int, int> > g[MAXN]; //存储最小生成树中的边

int find(int x) {return (fa[x] == x ? x : fa[x] = find(fa[x]));}

bool cmp(Node A, Node B) {
    return A.w < B.w;
}

void addedge(int u, int v, int w) {
    g[u].push_back({v, w});
    g[v].push_back({u, w});
}

void Kruskal() { //求最小生成树
    int cnt = n;
    ans = 0;
    sort(edge + 1, edge + 1 + m, cmp);
    for (int i = 1; i <= n; ++i) fa[i] = i;
    
    for (int i = 1; i <= m; ++i) {
        int fu = find(edge[i].u), fv = find(edge[i].v);
        if (fu != fv) {
            fa[fu] = fv;
            ans += edge[i].w;
            --cnt;
            edge[i].used = 1;
            
            addedge(edge[i].u, edge[i].v, edge[i].w);
        }
        if (cnt == 1) break;
    }
}

void dfs(int u) { //DFS预处理祖先、最大值、次大值
    for (int i = 1; i <= 18; ++i) { //MAXN <= 2^18
        f[u][i] = f[f[u][i - 1]][i - 1];
        mx[u][i] = max(mx[u][i - 1], mx[f[u][i - 1]][i - 1]);
        if (mx[u][i - 1] == mx[f[u][i - 1]][i - 1]) {
            mx2[u][i] = max(mx2[u][i - 1], mx2[f[u][i - 1]][i - 1]);
        } else {
            mx2[u][i] = min(mx[u][i - 1], mx[f[u][i - 1]][i - 1]);
            mx2[u][i] = max(mx2[u][i], max(mx2[u][i - 1], mx2[f[u][i - 1]][i - 1]));
        }
    }
    
    for (auto i : g[u]) { //用变量i遍历g[u]
        int to = i.first, val = i.second;
        if (to == f[u][0]) continue; //不要重复遍历父亲
        
        f[to][0] = u;
        mx[to][0] = val;
        mx2[to][0] = -inf; //没有次大边
        dep[to] = dep[u] + 1;
        dfs(to);
    }
}

int LCA(int u, int v) { //倍增求LCA
    if (dep[u] < dep[v]) swap(u, v);
    
    for (int i = 18; i >= 0; --i) {
        if (dep[f[u][i]] >= dep[v]) {
            u = f[u][i];
        }
    }
    
    if (u == v) return u;
    
    for (int i = 18; i >= 0; --i) {
        if (f[u][i] != f[v][i]) {
            u = f[u][i];
            v = f[v][i];
        }
    }
    
    return f[u][0];
}

int change(int u, int v, int w) { //换边操作
    int lca = LCA(u, v);
    int max1 = 0, max2 = 0;
    
    for (int i = 18; i >= 0; --i) { //处理u到lca的信息
        if (dep[f[u][i]] >= dep[lca]) {
            if (mx[u][i] > max1) {
                max2 = max1;
                max1 = mx[u][i];
            } else if (mx[u][i] > max2 && mx[u][i] != max1) {
                max2 = mx[u][i];
            }
            if (mx2[u][i] > max1) {
                max2 = max1;
                max1 = mx2[u][i];
            } else if (mx2[u][i] > max2 && mx2[u][i] != max1) {
                max2 = mx2[u][i];
            }
            u = f[u][i];
        }
    }
    
    for (int i = 18; i >= 0; --i) { //处理v到l的信息
        if (dep[f[v][i]] >= dep[lca]) {
            if (mx[v][i] > max1) {
                max2 = max1;
                max1 = mx[v][i];
            } else if (mx[v][i] > max2 && mx[v][i] != max1) {
                max2 = mx[v][i];
            }
            if (mx2[v][i] > max1) {
                max2 = max1;
                max1 = mx2[v][i];
            } else if (mx2[v][i] > max2 && mx2[v][i] != max1) {
                max2 = mx2[v][i];
            }
            v = f[v][i];
        }
    }
    
    if (w > max1) return ans - max1 + w;
    if (w == max1 && max2 != -inf) return ans - max2 + w;
    return inf;
}

signed main() {
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        cin >> edge[i].u >> edge[i].v >> edge[i].w;
        edge[i].used = 0;
    }
    
    for (int i = 1; i <= m; ++i) { //删除所有自环
    	if (edge[i].u == edge[i].v) {
    		swap(edge[i], edge[m]);
    		--m;
		}
	}
	
    Kruskal(); //先Kruskal一遍
    
    mx[1][0] = -inf; //根节点往上的最大边和次大边都不存在
    mx2[1][0] = -inf;
    dep[1] = 1;
    dfs(1);
    
    int ans1 = inf;
    for (int i = 1; i <= m; ++i) {
        if (!edge[i].used) {
            ans1 = min(ans1, change(edge[i].u, edge[i].v, edge[i].w));
        }
    }
    
    cout << ans1 << endl;
    return 0;
}
posted @ 2025-06-26 18:01  langni2013  阅读(54)  评论(0)    收藏  举报