BZOJ 1977 Tree 次小生成树 (kruskal st表 倍增lca)

该博客介绍了如何求解无向图的次小生成树问题,使用kruskal算法配合st表和倍增lca技术。在保证无自环的数据情况下,通过枚举边并找到路径上最大和次大边权,更新答案以找到严格次小生成树的边权和。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1977: [BeiJing2010组队]次小生成树 Tree

Time Limit: 10 Sec Memory Limit: 512 MB
Submit: 3121 Solved: 791
[Submit][Status][Discuss]
Description

小 C 最近学了很多最小生成树的算法,Prim 算法、Kurskal 算法、消圈算法等等。 正当小 C 洋洋得意之时,小 P 又来泼小 C 冷水了。小 P 说,让小 C 求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说: 如果最小生成树选择的边集是 EM,严格次小生成树选择的边集是 ES,那么需要满足:(value(e) 表示边 e的权值) 这下小 C 蒙了,他找到了你,希望你帮他解决这个问题。

Input

第一行包含两个整数N 和M,表示无向图的点数与边数。 接下来 M行,每行 3个数x y z 表示,点 x 和点y之间有一条边,边的权值为z。

Output

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

Sample Input

5 6

1 2 1

1 3 2

2 4 3

3 5 4

3 4 3

4 5 6
Sample Output

11
HINT

数据中无向图无自环; 50% 的数据N≤2 000 M≤3 000; 80% 的数据N≤50 000 M≤100 000; 100% 的数据N≤100 000 M≤300 000 ,边权值非负且不超过 10^9 。

思路:
要求次小生成树,肯定先要求最小生成树,考虑到次小生成树肯定是最小生成树更换一条边而得(加上一条边后图中肯定会出现一个环,用这条边替换掉其余边中最大的一条),所以枚举每条不在最小生成树上的边(u, v),因为是严格次小,所以求u和v路径上的最大边权和次大边权。
如果最大边权和(u, v)的边权相等,那么减去次大边的边权,加上(u, v)的边权,更新答案。
如果最大边权比(u, v)的边权要小,那么减去最大边的边权,加上(u, v)的边权,更新答案。
用st表记录max [ x ][ i ]表示x的从x向上走2^i步所经过路径上的最大值、次大值就可以做到O(nlogn)。

#include <cstdio>  
#include <cstring>  
#include <iostream>  
#include <algorithm>
#define LL long long
using namespace std;  

const int maxn=100010, maxm=300010, maxk=22;
const int INF = 1e9+7; 

int n, m, delta, idc;
int head[maxn], acc[maxn][maxk], dep[maxn], fat[maxn], fst[maxn][maxk], snd[maxn][maxk];
LL ans;  
bool in[maxm];  

struct Edge{
    int x, y, v;
}ed[maxm];  

struct Edge1{
    int to, next, w;
}ed1[maxm];

bool cmp(Edge a, Edge b){
    return a.v < b.v;
}   

void adde(int u, int v, int w){//记录x->y的路径 
    ed1[++idc].to = v;
    ed1[idc].next = head[u];
    head[u] = idc;
    ed1[idc].w = w;
}  

int findfa(int x){//并查集 
    return fat[x]==x ? x:fat[x]=findfa(fat[x]);
}  

void dfs(int u){//初始化st表 
    for(int i=1; i<=20; i++){  
        acc[u][i] = acc[acc[u][i-1]][i-1];//维护到达位置 
        int no1 = fst[u][i-1], no2 = fst[acc[u][i-1]][i-1];  
        fst[u][i] = max(no1, no2);//维护最长边的边长 
        snd[u][i] = max(snd[u][i-1], snd[acc[u][i-1]][i-1]);
        if(no1 != no2) snd[u][i] = max(snd[u][i], min(no1, no2));//维护次长边的边长 
    }  
    for(int k=head[u]; k; k=ed1[k].next){
        int v = ed1[k].to;
        if(v != acc[u][0]){
            dep[v] = dep[u] + 1;
            acc[v][0] = u;  
            fst[v][0] = ed1[k].w;
            dfs( v );  
        }  
    }
}  

void kruskal(){  
    sort(ed+1, ed+1+m, cmp);
    int cnt = 0;  
    for(int i=1; i<=n; i++) fat[i] = i;//初始化father  
    for(int i=1; i<=m; i++){
        if(cnt == n-1) break;//完成 
        int u = ed[i].x, v = ed[i].y, w = ed[i].v;  
        if(findfa(u) == findfa(v)) continue;  
        cnt++;
        fat[findfa(u)] = findfa(v);
        in[i] = 1;//记录它是否在最小生成树中 
        adde(u, v, w);
        adde(v, u, w);//构造最小生成树,保存两点间路径 
        ans += w;
    }  
}  

int lca(int u, int v){//倍增求lca 
    if(dep[u] < dep[v]) swap(u, v);  
    int h = dep[u] - dep[v];
    for(int i=20; i>=0 && h; i--)
        if( h & (1<<i) ) u = acc[u][i];//跳到同一深度 
    if(u == v) return u;
    for(int i=20; i>=0; i--)
        if(acc[u][i] != acc[v][i]){
            u = acc[u][i];
            v = acc[v][i];  
        }
    return acc[u][0];  
}  

void query(int u, int lc, int w){  
    int max1 = 0, max2 = 0;  
    for(int i=20,h=dep[u]-dep[lc]; i>=0; i--) 
        if( h & (1<<i) ){
            if(fst[u][i] > max1){
                max2 = max1;
                max1 = fst[u][i];   
            }
            max2 = max(max2, snd[u][i]);
            h -= (1<<i);  
        }  
    if(w == max1) delta = min(delta, w-max2);  
    else delta = min(delta, w-max1);  
}

void solve(int x){  
    int u = ed[x].x, v = ed[x].y, w = ed[x].v, lc = lca(u, v);  
    query(u, lc, w); query(v, lc, w);//在整条链上维护最长路与新边的差的最小值(相等的话就是次小边) 
}  

int main(){
    delta = INF;
    scanf("%d%d", &n, &m);  
    for (int i=1; i<=m; i++){
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        ed[i] = (Edge){x, y, z};
    } 
    kruskal();
    dfs( 1 );  
    for(int i=1; i<=m; i++)
        if ( !in[i] ) solve( i );
    printf("%lld\n", ans+delta);  
    return 0;  
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值