次小生成树

本文详细介绍了次小生成树的概念,通过Kruskal算法求解最小生成树,并利用LCA优化求解任意两点间距离。文章讨论了如何预处理fa[][]、d1[][]、d2[][]数组,并在BFS过程中构建最小生成树图。接着,通过枚举非树边,结合LCA计算次小生成树的权值,最终给出完整的C++代码实现。
摘要由CSDN通过智能技术生成

次小生成树


题目描述

image-20210728124557605


核心思路

这题其实与秘密的牛奶运输是完全一样的,都是求解严格次小生成树。只不过在那一题中,我们是用 O ( n 2 ) O(n^2) O(n2)的时间复杂度暴力求出了树中任意两点间的距离。但是我们知道LCA可以快速地求解出树中任意两点间的距离。因此,在这一题中,我们将使用LCA来进行优化。

如下图分析:

image-20210728125115083

这一题对求解MST中任意两点之间的距离进行了优化处理,即上图中的暴力枚举,将时间复杂度从 O ( n 2 ) O(n^2) O(n2)优化到了 O ( n × l o g ( n ) ) O(n\times log(n)) O(n×log(n)),具体优化过程如下:

  • 我们使用 d 1 [ i , j ] d1[i,j] d1[i,j]表示从节点 i i i往上跳 2 j 2^j 2j步路径过程中的边权的最大值,即这个路径上最大边的权值; d 2 [ i , j ] d2[i,j] d2[i,j]表示从节点 i i i往上跳 2 j 2^j 2j步路径过程中的边权的严格次大值,即这个路径上严格次大边的权值;在向上跳的过程中,记录每次跳跃时的最大值和次大值,那么最终整个路径中的最大值和次大值一定在这些记录的值中,从中求出即可。
  • 那么该如何求解d1[i][j]d2[i][j]呢?这里其实和求解fa[][]是类似的,可以在BFS的过程中递推求解,如下图:
  • image-20210728125743885

算法设计:

  • 使用Kruskal算法求出最小生成树,算出权值总和为 s u m sum sum
  • 将这棵最小生成树用图构建出来
  • 在BFS过程中预处理fa[][]和求解出d1[][]d2[][]
  • 依次枚举每条非树边 w w w,将其加入树中,一定会形成环,去掉环中树边的最大值或者次大值得到另一棵树,这些树中权值最小的就是次小生成树。

注意点:

  1. 设树边为 w [ i ] w[i] w[i],非树边为 w w w,那么次小生成树为 s u m + w − w [ i ] sum+w-w[i] sum+ww[i],我们设增量 Δ = w − w [ i ] \Delta=w-w[i] Δ=ww[i],我们在LCA函数中求出了这个 w w w(最大边或次大边),但是函数返回值的其实就是这个增量 Δ \Delta Δ,而不是 w w w,这点要注意哦
  2. 对于数组 f a , d 1 , d 2 fa,d1,d2 fa,d1,d2,由于题目给定最多1e5个点,由于 2 16 < 1 e 5 < 2 17 2^{16}<1e5<2^{17} 216<1e5<217,如果第二维取16的话,则不能存储完1e5个点;如果第二维取17的话,虽然说多了,但是可以完全存储1(⊙﹏⊙)个点,因此第二维的大小应该是17,其二进制位是从0到16。
  3. 还有一点,题目中给定这个无向图的最大边数 M M M是3e5,这是原图的最大边数。但是当我们对已经得到的最小生成树构建图时,由于树中的边是无向边,有 n n n个顶点,那么有 n − 1 n-1 n1条边,那么构建这个最小生成树的图时需要 2 ( n − 1 ) 2(n-1) 2(n1)条边,不妨开大点就是 2 × n 2\times n 2×n条边,当 n n n取最大为 N N N时,则最多为 2 × N 2\times N 2×N条边。

问题:为什么跳到了最近公共祖先下一层时,最后只加了最大边 d i s t a n c e [ c n t + + ] = d 1 [ a ] [ 0 ] distance[cnt++]=d1[a][0] distance[cnt++]=d1[a][0] d i s t a n c e [ c n t + + ] = d 1 [ b ] [ 0 ] distance[cnt++]=d1[b][0] distance[cnt++]=d1[b][0],而没有添加次小边 d i s t a n c e [ c n t + + ] = d 2 [ a ] [ 0 ] distance[cnt++]=d2[a][0] distance[cnt++]=d2[a][0] d i s t a n c e [ c n t + + ] = d 2 [ b ] [ 0 ] distance[cnt++]=d2[b][0] distance[cnt++]=d2[b][0]呢?

此时x和y距离他们的最近公共祖先只有一步了,也就是只有一条边了,显然只有它自身的权值作为最大距离,次大距离初始化为-INF了

但其实写上了也是可以的


代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=3e5+10,INF=0x3f3f3f3f;
typedef long long LL;
int n,m;    //点数 边数
struct Edge{
    int a,b,w;
    bool flag;  //判断是否为树边(是否为最小生成树中的边)
    bool operator < (const Edge& W)const{
        return w<W.w;
    }
}edges[M];
//这个邻接表是用来给最小生成树建图的
int h[N],e[2*N],ne[2*N],w[2*N],idx;
int p[N];   //并查集的集合数组
int depth[N];   //节点的深度
int fa[N][17],d1[N][17],d2[N][17];  //d1是最大边 d2是次大边
int q[N];   // bfs使用到的队列
void add(int a,int b,int c)
{
    e[idx]=b;
    w[idx]=c;
    ne[idx]=h[a];
    h[a]=idx++;
}
int find(int x)
{
    if(x!=p[x])
        p[x]=find(p[x]);
    return p[x];
}
//Kruskal算法求出这棵最小生成树的权值总和
LL Kruskal()
{
    sort(edges,edges+m);
    for(int i=1;i<=n;i++)
        p[i]=i;
    LL res=0;
    for(int i=0;i<m;i++)
    {
        int a=find(edges[i].a);
        int b=find(edges[i].b);
        int w=edges[i].w;
        if(a!=b)
        {
            p[a]=b;
            res+=w;
            edges[i].flag=true; //标记i这条边是树边
        }
    }   
    return res; 
}
//Kruskal算法只是求出了最小生成树的权值总和,但是并没有把这棵最小生成树构建出来
//这里是把这棵最小生成树构建成图
void build()
{
    memset(h,-1,sizeof h);
    for(int i=0;i<m;i++)
    {
        if(edges[i].flag)//如果是树边
        {
            int a=edges[i].a;
            int b=edges[i].b;
            int w=edges[i].w;
            //树中的边都是无向边
            add(a,b,w);
            add(b,a,w);
        }
    }
}
//bfs预处理出fa[][]、d1[][]、d2[][]
void bfs(int root)//root是根节点  我们这里默认是1号点  其实任意一个点都可以作为根节点
{
    memset(depth,0x3f,sizeof depth);
    int hh=0,tt=0;
    // depth[0]=0是哨兵  depth[root]=1设置根节点的深度为1
    depth[0]=0,depth[root]=1;
    q[0]=root;  //将根节点入队
    //进行广搜
    while(hh<=tt)
    {
        int t=q[hh++];  //取出队头元素
        //遍历t的所有邻接点j
        for(int i=h[t];~i;i=ne[i])
        {
            int j=e[i]; //t的某个邻接点j
            if(depth[j]>depth[t]+1)
            {
                depth[j]=depth[t]+1;    //更新j的深度
                q[++tt]=j;  //将j入队
                fa[j][0]=t; //节点j向上走2^0步就是节点t
                //由于此时节点j到节点t之间只有一条边,那么最大边就是这条边的权值w[i],由于最大边>次大边
                //因此如果只有一条边,那么次大边不存在  我们设置为负无穷
                d1[j][0]=w[i];
                d2[j][0]=-INF;
                //由于已经处理了2^0,那么接下来就是2^1,2^2,...,2^16
                for(int k=1;k<=16;k++)
                {
                    int anc=fa[j][k-1]; //先从j跳2^{k-1}到了anc节点
                    fa[j][k]=fa[anc][k-1];  //然后从anc节点跳2^{k-1}到了fa[j][k]节点
                    //distance数组存储的是从节点j跳2^k到达节点fa[j][k]过程中的最大边和次大边
                    //由于这个过程分为两段:
                    //  (1)从j到anc,这段中的最大边是d1[j][k-1],次大边是d2[j][k-1]
                    //  (2)从anc到fa[j][k],这段中的最大边是d1[anc][k-1],次大边是d2[anc][k-1]
                    //由于我们并不知道这两段中应该选择哪个最大边和次大边,因此我们可以先都存储起来
                    int distance[4]={d1[j][k-1],d2[j][k-1],d1[anc][k-1],d2[anc][k-1]};
                    //刚开始初始化从j节点到fa[j][k]不可达,因此最大边和次大边都为负无穷
                    d1[j][k]=d2[j][k]=-INF;
                    for(int u=0;u<4;u++)//遍历这四个  找到最大边和次大边
                    {
                        int d=distance[u];
                        //如果新边d大于最小生成树中的最大边d1[j][k]  则直接用这个新边去替代这个最大边即可
                        if(d>d1[j][k])
                        {
                            d2[j][k]=d1[j][k];
                            d1[j][k]=d;
                        }
                        //如果新边d小于最小生成树中的最大边d1[j][k],但是大于最小生成树中的次大边d2[j][k]
                        //那么可以用这个新边去替代这个次大边即可
                        else if(d<d1[j][k]&&d>d2[j][k])
                            d2[j][k]=d;
                    }
                }
            }
        }
    }
}
//次小生成树权值为sum+w-w[i],w是新边,w[i]是最大边,这里LCA返回的是w-w[i]这个增量的值而不是返回w[i]
LL LCA(int a,int b,int w)
{
    //缓存数组 记录上跳过程中每段的最大值和次大值
    //由于每个点可能有最大边和次大边,因此数组要开2*N,而不是N
    static int distance[2*N];
    if(depth[a]<depth[b])
        swap(a,b);
    int cnt=0;  // 记录跳跃的次数,每跳一次,会记录两个数据
    for(int k=16;k>=0;k--)
    {
        if(depth[fa[a][k]]>=depth[b])
        {
            //记录节点a的最大边和次大边
            distance[cnt++]=d1[a][k];
            distance[cnt++]=d2[a][k];
            a=fa[a][k];
        }
    }
    if(a!=b)
    {
        for(int k=16;k>=0;k--)
        {
            if(fa[a][k]!=fa[b][k])
            {
                //记录节点a和节点b的最大边和次大边
                distance[cnt++]=d1[a][k];
                distance[cnt++]=d2[a][k];
                distance[cnt++]=d1[b][k];
                distance[cnt++]=d2[b][k];
                a=fa[a][k];
                b=fa[b][k];
            }
        }
        //到这里,说明已经跳到了最大公共祖先的下一层,那么此时只需要再向上跳一步就可以到达了LCA
        distance[cnt++]=d1[a][0];
        distance[cnt++]=d1[b][0];
        //下面的这个写上也是可以的
        // distance[cnt++]=d2[a][0];
        // distance[cnt++]=d2[b][0]
    }
    int dist1=-INF,dist2=INF;
    for(int i=0;i<cnt;i++)
    {
        int d=distance[i];
        if(d>dist1)
        {
            dist2=dist1;
            dist1=d;
        }
        else if(d<dist1&&d>dist2)
            dist2=d;
    }
    if(w>dist1)
        return w-dist1;
    if(w>dist2)
        return w-dist2;
    return INF; 
}
int main()
{
    int root=1; //根节点  我们选择1号节点
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)//读入m条边的信息
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i]={a,b,w};
    }
    LL sum=Kruskal();   //求出这棵最小生成树的权值总和
    //这里是把这棵最小生成树构建成图
    build();
    bfs(root);
    LL res=1e18;
    //枚举将每一条非树边去替代最小生成树中的最大边或次大边
    for(int i=0;i<m;i++)
    {
        if(!edges[i].flag)//非树边
        {
            int a=edges[i].a;
            int b=edges[i].b;
            int w=edges[i].w;
            //有x条非树边,则会有x个res值,这里取最小的那个,那么就是次小生成树的权值总和了
            res=min(res,sum+LCA(a,b,w));
        }
    }
    printf("%lld\n",res);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

卷心菜不卷Iris

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

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

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

打赏作者

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

抵扣说明:

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

余额充值