打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
次小生成树
一行小字。这篇博客是我的gg之前写的博客,一直想写博客,但是直到gg,都没写几篇博客。我再把这篇博客翻出来的时候,不知已经过了半年还是一年了。博客园的博客系统做的不错,就是数学公式和一些图片会乱,图片的话手动上传一下还ok。数学公式不影响阅读,大家自行理解就ok嘤嘤~

前置知识

  • 最小生成树Kurskal算法

  • 最近公共祖先LCA!

引入问题

定义

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的次少的边。

模板题

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

解决方法

想法

把所有生成树按照权值之和从小到小排序,求排在第二位的生成树。注意,如果最小生成树不唯一,次小生成树的权值和最小生成树相同。

解法一

次小生成树不会和最小生成树相同,因此可以枚举最小生成树中不在次小生成树中出现的边。 注意最小生成树只有n-1条边,所以只需枚举n-1次。每次在剩下的边里,求一次最小生成树。

步骤

  1. Kruskal算法求出最小生成树

  2. 枚举最小生成树的每一条边,对这条边做标记,再进行一次Kruskal算法,Kruskal算法中,跳过被标记的边,求最小生成树,记录答案。

  3. 去步骤2中所记录答案的最小值即为次小生成树的边权之和。(可直接在步骤2中进行)

复杂度

  1. $O(M\log M+M)$

  2. $O(NM)$

  3. $O(N)$

总复杂度$O(NM)$

有没有更快些的解决方法呢?

补充知识

可行交换与临集

- T为图G的一棵生成树,对于非树边a和树边b,插入边a并且删除边b的操作记为(+a,-b)

- 如果T+a-b仍然是一棵生成树,称(+a,-b)是一个可行交换

- 由T进行一次可行交换以后得到的新的生成树的集合称为T的临集

显然,可得定理:次小生成树在最小生成树的临集中

更好的解法

- 枚举要加入哪条新边,在最小生成树中加入一条边u-v以后,图上会出现一条回路,如果想保持该图为生成树,需删除一条边,删除的边必须是在最小生成树上u到v的路径上,如果想要删除边后的新的生成树有可能为次小生成树,必须要求删除的边这条路径的最长边。

如图,枚举出一条不在最小生成树上的边E1,如果连接E1,E1两端点为(1,4),如果想连接E1后依然保证新图为生成树,需删除路径(1,4)上一条最小生成树上的边。为了求出次小生成树,我们选择删除原树上的尽可能大的边,即删除边(3,4),连接边E1。

如图为新生成的树。

总结一下,新的算法是这样的

  • 求出最小生成树之后,枚举每一个不在树上的边,对于边(u,v),连上此边,删去原来树上路径(u,v)上最长的边。
  • 题目中要求的是严格次小生成树,因此,当原来树上路径(u,v)上最长的边的权值等于新连边(u,v)时,是不符合要求的,我们需要找次长边。

那么枚举每一条边复杂度是$O(M)$,对每一条边用dfs求(u,v)上的最长&次长边,复杂度是$O(N)$,总复杂度是$O(MN)$没有遍低呀~

因此,我没需要对算法进行优化,可以通过dfs$O(N)$来预处理每个点到其k代父节点的路径上的最长边和次长边,再通过lca倍增的方法在$O(\log N)$复杂度下求出u,v两点之间的最长边和次长边。

预处理

对于每一个点u,我们记记录anc[u][k]数组,u是节点编号,k代表其$2^k $代父节点。

记录m1[u][k]数组,其中,u是节点编号,k代表从u节点往上$2k$个点,数组的值是u点到u节点上方$2k $个点间的最长边,可得状态转移方程

$$m1[u][k]=\max(m1[u][k-1],m1[anc[u][k-1]][k-1])$$

记录m2[u][k]数组,u,k含义同m1,数组的值是u点到u节点上方$2^k $个点间的次长边,可得状态转移方程

$$m2[u][k]=\begin{cases} \max(m2[u][k-1],m2[anc[u][k-1][k-1]), & \text{if }m1[u][k-1]\ne m1[anc[u][k-1]][k-1] \ \min(m1[u][k-1],m1[anc[u][k-1][k-1]), & \text{if }m1[u][k-1]= m1[anc[u][k-1]][k-1] \end{cases}$$

经过一次dfs后,我们即可预处理完成以上内容。

遍历$N$个点,每个点要进行$\log N$次计算,复杂度为 $O(N\log N)$

LCA同时求值

对于点u和v,我们在有预处理的情况下想求其两点之间路径的最长边和次长边,可进行以下操作

用倍增法求LCA的方式将两个点向上跳,跳的同时记录m1和m2的值。

向上跳的做法与倍增法求LCA相同,如果没有了解,请先学习倍增法求LCA,此处不做过多介绍。

具体代码如下:

 /*参数ma为要新建的边的权值,因为要求严格次小生成树,
 所以在原树中删除的边必须严格小于新建的边的权值,
 即lca函数的返回值(也就是在原树中删除的边的权值)需严格小于参数ma。*/
long long findBig(long long u,long long i,long long ma){
    if(m1[u][i]!=ma){
        return m1[u][i];
    }
    return m2[u][i];
}
long long lca(long long u,long long v,long long ma){
    if(dep[u]<dep[v]){
        swap(u,v);
    }
    long long res = -INF;
    for(int i = 20;i>=0;--i){
        if(dep[ancestor[u][i]]<dep[v]){
            continue;
        }
        res = max(res,findBig(u,i,ma));
        u = ancestor[u][i];
    }
    if(u==v){
        return res;
    }
    for(int i = 20;i>=0;--i){
        if(ancestor[u][i]!=ancestor[v][i]){
            res = max(res,findBig(u,i,ma));
            res = max(res,findBig(v,i,ma));
            u = ancestor[u][i];
            v = ancestor[v][i];
        }
    }
    res = max(res,findBig(u,0,ma));
    res = max(res,findBig(v,0,ma));
    return res;
}

步骤

  1. Kruskal算法求出最小生成树
  2. dfs预处理
  3. 枚举每一条不在最小生成树上的边,求出最小生成树上的边权之和加上该边之和,减去该边所连两点在原树上的路径间的最长边(次长边),即为新的生成树的边权之和,记录答案。
  4. 在步骤3中,所记录答案的最小值即为次小生成树的边权之和。(可直接在步骤3中进行)

复杂度

  1. $O(M\log M+M)$
  2. $O(N\log N )$
  3. $O(M\log N)$
  4. $O(N)$

总复杂度为:$O(MlogN)$

具体代码

//
// Created by SeverusNg on 2020/7/24.
//

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>

const long long MAXN = 100005;
const long long MAXM = 300005;
const  long long INF = 0x3f3f3f3ff3f3f3f;
using namespace std;

struct edge{
    long long u,v,w;
    int next;
    bool operator<(const edge &e)const{
        return w<e.w;
    }
} ed[MAXM*2],pool[MAXM];
int head[MAXN];
int topE;
void addE(int u,int v,int w){
    ed[++topE].u = u;
    ed[topE].v = v;
    ed[topE].w = w;
    ed[topE].next = head[u];
    head[u] = topE;
    return;
}
int fa[MAXN];

int find(int u){
    if(fa[u]==u){
        return fa[u];
    }
    fa[u] = find(fa[u]);
    return fa[u];
}
void merge(int u,int v){
    u = find(u);
    v = find(v);
    fa[u] = v;
    return;
}

bool use[MAXM];

long long N,M;
long long mst;
void kruskal(){
    sort(pool+1,pool+M+1);
    for(int i = 1;i<=M;++i){
        long long u = pool[i].u;
        long long v = pool[i].v;
        long long w = pool[i].w;
        if(find(u)!=find(v)){
            mst += w;
            merge(u,v);
            addE(u,v,w);
            addE(v,u,w);
            use[i]=true;
        }
    }
    return;
}
int dep[MAXN];

int ancestor[MAXN][25];
long long m1[MAXN][25];
long long m2[MAXN][25];
long long findBig(long long u,long long i,long long ma){
    if(m1[u][i]!=ma){
        return m1[u][i];
    }
    return m2[u][i];
}
void dfs(long long u,long long father,long long w){
    ancestor[u][0] = father;
    dep[u] = dep[father]+1;
    m1[u][0]=w;
    m2[u][0]=-INF;
    for(int i = 1;i<=20;++i){
        ancestor[u][i]=ancestor[ancestor[u][i-1]][i-1];
        m1[u][i]=max(m1[u][i-1],m1[ancestor[u][i-1]][i-1]);
        m2[u][i]=max(m2[u][i-1],m2[ancestor[u][i-1]][i-1]);
        if(m1[u][i-1]!=m1[ancestor[u][i-1]][i-1]){
            m2[u][i]=max(m2[u][i],min(m1[u][i-1],m1[ancestor[u][i-1]][i-1]));
        }
    }
    for(int i = head[u];i!=0;i=ed[i].next){
        long long u,v,w;
        u = ed[i].u;
        v = ed[i].v;
        w = ed[i].w;
        if(v==father){
            continue;
        }
        dfs(v,u,w);
    }
    return;
}

long long lca(long long u,long long v,long long ma){
    if(dep[u]<dep[v]){
        swap(u,v);
    }
    long long res = -INF;
    for(int i = 20;i>=0;--i){
        if(dep[ancestor[u][i]]<dep[v]){
            continue;
        }
        res = max(res,findBig(u,i,ma));
        u = ancestor[u][i];
    }
    if(u==v){
        return res;
    }
    for(int i = 20;i>=0;--i){
        if(ancestor[u][i]!=ancestor[v][i]){
            res = max(res,findBig(u,i,ma));
            res = max(res,findBig(v,i,ma));
            u = ancestor[u][i];
            v = ancestor[v][i];
        }
    }
    res = max(res,findBig(u,0,ma));
    res = max(res,findBig(v,0,ma));
    return res;
}
int main(){
    scanf("%lld%lld",&N,&M);
    for(int i = 1;i<=N;++i){
        fa[i] = i;
    }
    for(int i = 1;i<=M;++i){
        scanf("%lld%lld%lld",&pool[i].u,&pool[i].v,&pool[i].w);
    }
    kruskal();
    dfs(1,0,-INF);
    long long ans = INF;
    for(int i = 1;i<=M;++i){
        if(use[i]){
            continue;
        }
        long long u = pool[i].u;
        long long v = pool[i].v;
        long long w = pool[i].w;
        ans = min(ans,mst+w-lca(u,v,w));
    }

    printf("%lld\n",ans);
    return 0;
}

嘤嘤嘤,完结撒花~

啊,从建博客,各种调试,到写完这一篇文章,再加上摸鱼,抬头一看,天已经亮了,搞了一个通宵~

不过看到成果,内心还是十分高兴的~

如有错误,希望巨佬您能够在评论区指正,感激不尽~

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
二叉树中两个节点的最近公共祖先节点
算法之LCA与RMQ问题 | 董的博客
夜深人静写算法(六)
Kruskal算法证明及实现
数据结构_树_图_总结
前一日5穿60日
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服