次小生成树(倍增+lca)

题目描述

给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树。
设最小生成树的边权之和为sum,严格次小生成树就是指边权之和大于sum的生成树中最小的一个。

输入格式

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

输出格式

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

数据范围

N≤105,M≤3∗105

输入样例
5 6
1 2 1
1 3 2
2 4 3
3 5 4
3 4 3
4 5 6
输出样例
11

题目分析

这道题是秘密的牛奶运算的加强版,如果之前没做过次小生成树类似的题目的话建议先看看这道题。本题是对于这道题的做法进行了一个倍增的优化。

解题步骤:

  1. 我们首先要求出最小生成树,并记录最小生成树所包含的边。
  2. 将这个最小生成树单独建成图。
  3. 这道题的的数据范围比较大,暴力预处理图上任意两点间路径上的最大值和次大值会直接TLE,因此我们需要对这个过程进行一个优化。我们可以发现,在一棵树上,任意两点a和b之间的路径即为:a到a和b最大公共祖先p的路径+b到p的路径。因此我们可以用求最小公共祖先的算法(倍增法)来进行一个优化:
    depth[i] //i节点的深度
    fa[i][j] //表示i节点向上跳2^j步后到达的节点
    d1[i][j] //表示i节点向上跳2^j步的路径上的最大边权
    d2[i][j] //表示i节点向上跳2^j步的路径上的次大边权
  4. 对于如何预处理出这三个数组,我们可以参考倍增法求lca的过程。
    fa[i][j]=fa[fa[i][j-1]][j-1]; //fa[i][j]的值即为i节点向上跳2^j-1步得到的节点再向上跳2^j-1步后得到的节点。
    d1[i][j]和d2[i][j]的值一定会在 {d1[i][j-1]、d2[i][j-1]、d1[fa[i][j-1]][j-1]、d2[fa[i][j-1]][j-1]} 这四个之数中。因为,i向上跳2j步的路径即为:i向上跳2j-1步的路径+fa[i][j-1]向上跳2j-1步的路径。因此我们可以直接枚举这四个值来获得d1[i][j]和d2[i][j]。
  5. 枚举所有的非树边(u,v,w),并将这条非树边加入到树中。给最小生成树加上一条边之后,这个图必定会形成一个环,然后我们就可以在这个环上找出除该非树边之外的最大边dmax1和次大边dmax2。
    我们可以求出并记录u和v分别到其最近公共祖先p的路径上产生的所有最大值和次大值。然后枚举所有这些最大值和次大值,并找出它们的最大值和次大值即为最大边dmax1和次大边dmax2。
  6. 当w>dmax1时,我们为了得到次小生成树,我们要让生成树的总边权增加量最小,即加入非树边(u,v,w),删除最大边dmax1。
    当w==dmax1时,用最大边dmax1替换该非树边,得到的还是最小生成树,因此,我们需要用次大边来替换该非树边。
代码如下
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <map>
#include <queue>
#include <vector>
#include <set>
#include <algorithm>
#define LL long long
#define ULL unsigned long long
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=1e5+5,M=3e5+5,INF=0x3f3f3f3f;
struct Edge{
    
    
    int u,v,w;
    bool used;
    bool operator<(const Edge &a)
    {
    
     return w<a.w; }
}edge[M];
int h[N],e[M],w[M],ne[M],idx;
int p[N];
int depth[N],fa[N][17],d1[N][17],d2[N][17];
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(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
LL kruskal(int m)					//kruskal算法求最小生成树
{
    
    
    for(int i=1;i<N;i++) p[i]=i;
    LL res=0;
    sort(edge,edge+m);
    for(int i=0;i<m;i++)
    {
    
    
        int a=find(edge[i].u),b=find(edge[i].v),w=edge[i].w;
        if(a!=b)
        {
    
    
            p[b]=a;
            res+=w;
            edge[i].used=true;		//如果这条边是最小生成树上的边,则标记一下
        }
    }
    return res;
}
void build(int m)				//建图
{
    
    
    memset(h,-1,sizeof h);
    for(int i=0;i<m;i++)		//将这棵最小生成树单独建出来
        if(edge[i].used)
        {
    
    
            int u=edge[i].u,v=edge[i].v,w=edge[i].w;
            add(u,v,w); add(v,u,w);
        }
}
int q[N];
void bfs()
{
    
    
    memset(depth,0x3f,sizeof depth);		//初始化
    int head=0,tail=0;
    depth[0]=0; depth[1]=1;				//将0号点设置为哨兵,1号点为根节点
    q[0]=1;								//将根节点放入队列中
    while(tail>=head)
    {
    
    
        int u=q[head++];
        for(int i=h[u];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(depth[v]>depth[u]+1)		//如果depth[v]的深度可以被更新
            {
    
    
                depth[v]=depth[u]+1;	//则更新depth[v]
                q[++tail]=v;			//并将v节点放入队列中
                fa[v][0]=u;				//v向上跳2^0步的节点即为其父节点u
                d1[v][0]=w[i],d2[v][0]=-INF;
                for(int k=1;k<=16;k++)
                {
    
    
                    int anc=fa[v][k-1];			//anc为v节点向上跳2^k-1步后得到的节点
                    fa[v][k]=fa[anc][k-1];
                    int dist[4]={
    
    d1[v][k-1],d2[v][k-1],d1[anc][k-1],d2[anc][k-1]};
                    d1[v][k]=d2[v][k]=-INF;
                    for(int j=0;j<4;j++)		//从这四个数中找最大值和次大值
                    {
    
    
                        if(dist[j]>d1[v][k]) d2[v][k]=d1[v][k],d1[v][k]=dist[j];
                        else if(dist[j]<d1[v][k]&&dist[j]>d2[v][k]) d2[v][k]=dist[j];
                    }
                }
            }
        }
    }
}
int lca(int a,int b,int w)		//求最小生成树加入该非树边之后,得到的次小生成树的值
{
    
    
    int cnt=0;
    static int dist[N*2];		//用dist[]记录a到p+b到p的过程中,产生的所有值(p为其最近公共祖先)
    if(depth[a]<depth[b]) swap(a,b);	//保证a节点的深度大于等于b
    for(int k=16;k>=0;k--)
        if(depth[fa[a][k]]>=depth[b])	//让a节点跳到与b同层的位置
        {
    
    
            dist[cnt++]=d1[a][k];		//记录下跳的过程中产生的所有最大值和次大值
            dist[cnt++]=d2[a][k];
            a=fa[a][k];
        }
    
    if(a!=b)
    {
    
    
        for(int k=16;k>=0;k--)				//a和b同时向上跳到p的下一层的位置
            if(fa[a][k]!=fa[b][k])
            {
    
    
                dist[cnt++]=d1[a][k];		//记录下跳的过程中产生的所有最大值和次大值
                dist[cnt++]=d2[a][k];
                dist[cnt++]=d1[b][k];
                dist[cnt++]=d2[b][k];
                a=fa[a][k]; b=fa[b][k];
            }
            
        dist[cnt++]=d1[a][0];			//因为a和b只是跳到了p的下一层,所以还要记录a和b到p的边权
        dist[cnt++]=d1[b][0];
    }
    int d1=-INF,d2=-INF;
    for(int i=0;i<cnt;i++)			//枚举这个过程产生的所有值,找出最大值和次大值
    {
    
    
        if(dist[i]>d1) d2=d1,d1=dist[i];
        else if(dist[i]<d1&&dist[i]>d2) d2=dist[i];
    }
    if(w>d1) return w-d1;
    if(w>d2) return w-d2;
    return INF;
}
int main()
{
    
    
    int n,m;
    scanf("%d %d",&n,&m);
    for(int i=0;i<m;i++)
    {
    
    
        int u,v,w;
        scanf("%d %d %d",&u,&v,&w);
        edge[i]={
    
    u,v,w};
    }
    LL sum=kruskal(m);				//先求出该图的最小生成树
    build(m);						//建图
    bfs();							//预处理出需要的几个数组
    LL ans=1e18;
    for(int i=0;i<m;i++)
        if(!edge[i].used)			//枚举所有非树边
        {
    
    
            int u=edge[i].u,v=edge[i].v,w=edge[i].w;
            ans=min(ans,sum+lca(u,v,w));		//找出次小生成树
        }
    printf("%lld\n",ans);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/li_wen_zhuo/article/details/109842758