最近公共祖先之:【严格次小生成树 o(nlogn)写法】【利用倍增2^k步拆成两个2^(k-1)】【维护树上路径中边权的最大值和次大值】【a和b跳到最近公共祖先过程中的弧(每跳一次形成一个弧)】

本文详细介绍了如何使用树上倍增方法计算树上两点间路径的最大边权和次大边权,并提供了最小生成树与最近公共祖先结合的次小生成树算法实现。通过 bfs 进行预处理,构建倍增数组 s1 和 s2,用于快速获取路径上的最大值和次大值,进而求得路径的最优点权和次优点权。同时,文章给出了 LCA 的应用以及在处理次小生成树问题中的关键思路。
摘要由CSDN通过智能技术生成

【每一个弧相当于一个链,在图中画了,链上面有最大值和次大值(bfs倍增初始化好的),把这些所有的最大值和次大值存起来,找出整体的最大值和次大值】

与下链接同类型:

​​​​​​​最小生成树&LCA之:【除了dis[]以外,两个点之间新的维护】【求两点之间经过的最小边权】【树上倍增DP的思考方式】【树上倍增原理的剖析】【倍增法对到祖宗节点的某个东西的维护】_bei2002315的博客-CSDN博客

//新版代码
/*
要求严格次小生成树,
当然这也是模板题 .
涉及到严格次小生成树的问题,都要与最近公共祖先结合起来。

s1[i][j]:存i 跳2^j步路径上边权的最大值
s2[i][j]:存i 跳2^j步路径上边权的次大值 
*/

/*
本题的基本模型是:
求树上两个点之间路径的最大边权和次大边权(利用LCA的那个) 
*/

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=3e5+10;
const int inf=0x3f3f3f3f;
typedef long long LL;
int e[M],ne[M],h[N],w[M],idx;
int p[N];
int depth[N];
int fa[N][20];
int s1[N][20];
int s2[N][20];
int cnt;
int n,m;

struct Node
{
	int x,y;
	int w;
	bool is_leaf;
	bool operator<(const Node &W)const
	{
		return w<W.w;
	}
}edges[M];

void add(int a,int b,int c)
{
	e[idx]=b;
	ne[idx]=h[a];
	w[idx]=c;
	h[a]=idx++;
}


void bfs(int root)
{
	memset(depth,0x3f,sizeof depth);
	depth[0]=0;
	depth[root]=1;
	
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		auto t=q.front();
		q.pop();
		
		for(int i=h[t];i!=-1;i=ne[i])
		{
			int j=e[i];
			if(depth[j]>depth[t]+1)
			{
				depth[j]=depth[t]+1;
				q.push(j);
				fa[j][0]=t;
				s1[j][0]=w[i];
				s2[j][0]=-inf;
				
				for(int k=1;k<=19;k++)
				{
					fa[j][k]=fa[fa[j][k-1]][k-1];
				  //s1[j][k]=max(s1[j][k-1],s1[fa[j][k-1]][k-1]);
				  //s2[j][k]=max(s2[j][k-1],s2[fa[j][k-1]][k-1]); 
				  //由于要更新最大值和次大值,所以拆开来写
				  
				  s1[j][k]=s2[j][k]=-inf;
				  //先存下来所有的元素 
				  int distance[5]={s1[j][k-1],s2[j][k-1],s1[fa[j][k-1]][k-1],s2[fa[j][k-1]][k-1]};
	              for(int u=0;u<4;u++)
				  {
				  	int d=distance[u];
				  	if(d>s1[j][k])
				  	{
				  		s2[j][k]=s1[j][k];
				  		s1[j][k]=d;
					}
					else if(d!=s1[j][k]&&d>s2[j][k])
					{
						s2[j][k]=d;
					}
				  }			
				
				}
			}
		}
	}
}


int find(int x)
{
	if(x!=p[x]) p[x]=find(p[x]);
	return p[x];
}

int lca(int a,int b,int w)
{
	static int distance[N*2]; //每条路径上面的 最大值 和 次大值 都存在distance中
	 /*
    其次为什么最多有N*2个路径要保存呢?
    其实这里不算路径,顶多是一个点跳到另外一个点的一道弧,
    根据所画的图,每一个弧都可以看成一个链,有最大值和次大值
    最多有N个点,所以最多N条弧,每个弧保存最大值和次大值,
    就是 N*2
    */
    cnt=0;
    
    if(depth[a]<depth[b])
    {
    	swap(a,b);
	}
	
	for(int k=19;k>=0;k--)
	{
		if(depth[fa[a][k]]>=depth[b])
		{
			distance[cnt++]=s1[a][k];//每跨一大步,相当于图中的弧度,把每个弧度的最大次大值都存起来
			distance[cnt++]=s2[a][k];//每一根弧,都相当于图中的一个链,所以都要存起来
			a=fa[a][k];
		}
	}
	
	if(a!=b) //继续跳 
	{
		for(int k=19;k>=0;k--)
		{
			if(fa[a][k]!=fa[b][k])
			{
				//① 
			    distance[cnt++]=s1[a][k];
			    distance[cnt++]=s2[a][k];
			    //②
				distance[cnt++]=s1[b][k];
				distance[cnt++]=s2[b][k];
				
				
				a=fa[a][k];
				b=fa[b][k];
			}
		}
		
		distance[cnt++]=s1[a][0];
		distance[cnt++]=s1[b][0];
	}
	
	//下面从所有存的最大值和次大值里面选出最终的最大值和次大值 
	int maxv1=-inf,maxv2=-inf;
	for(int i=0;i<cnt;i++)
	{
		int d=distance[i];
		if(d>maxv1)
		{
			maxv2=maxv1;
			maxv1=d;
		
		}
		else if(d!=maxv1&&d>maxv2)
		{
			maxv2=d;
		}
	}
	
	
	if(w>maxv1) return w-maxv1;
	if(w>maxv2) return w-maxv2;
	
	return inf;
} 

LL kruskal()
{
	sort(edges+1,edges+1+m);
	//memset(h,-1,sizeof h);
	LL sum=0;
	for(int i=1;i<=m;i++)
    {
    	int a=edges[i].x;
    	int b=edges[i].y;
    	int w=edges[i].w;
        int pa=find(a);
        int pb=find(b);
        
        if(pa!=pb)
        {
        	p[pa]=pb;
        	edges[i].is_leaf=true;
        	sum+=w;
		}
	}
	
	return sum;
}

void build()
{
	memset(h,-1,sizeof h);
	for(int i=1;i<=m;i++)
     {
         if(edges[i].is_leaf)
         {
             int a=edges[i].x,b=edges[i].y,w=edges[i].w;
             add(a,b,w);
             add(b,a,w);
         }
     }
}

int main()
{
	//int n,m;
	cin>>n>>m;
	
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		edges[i]={x,y,z};
	}
	for(int i=1;i<=n;i++)
	{
		p[i]=i;
	} 
	
	

	LL sum=kruskal();
	build();
    
	bfs(1);
	
	LL ans=1e18;
	//要知道最小生成树的sum和次小生成树的sum只有一条边权不同 
	for(int i=1;i<=m;i++)  //枚举一下所有边  
	{
		if(!edges[i].is_leaf)
		{
		int a=edges[i].x;
		int b=edges[i].y;
		int w=edges[i].w;
		
		ans=min(ans,sum+lca(a,b,w));
		//lca返回的是:相当于新边-旧边的差值
	   }
		
	}
	
	printf("%lld\n",ans);
	
	return 0;
}

 

 次小生成树的原理:

本题的题解:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10,M=3e5+10;
const int inf=0x3f3f3f3f;

int n,m;
struct Edge
{
    int a,b,w;
    bool used;  //标明当前边有没有被用过
    bool operator<(const Edge &W)const
    {
        return w<W.w;
    }
}edges[M];
int e[M],ne[M],h[N],w[M],idx;
int p[N];
int fa[N][18],d1[N][18],d2[N][18];
int q[N];
int depth[N];
int cnt;

void add(int a,int b,int c)  //做最小生成树算法的时候,顺便建立最小生成树
{
    e[idx]=b;
    ne[idx]=h[a];
    w[idx]=c;
    h[a]=idx++;
}

int find(int x)
{
    if(x!=p[x]) p[x]=find(p[x]);
    return p[x];
}


LL kruskal()
{
    for(int i=1;i<=n;i++) p[i]=i;
    sort(edges,edges+m);
    
    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].used=true;
        }
    }
    
    return res;
}

void build()
{
     memset(h,-1,sizeof h);
     for(int i=0;i<m;i++)
     {
         if(edges[i].used)
         {
             int a=edges[i].a,b=edges[i].b,w=edges[i].w;
             add(a,b,w);
             add(b,a,w);
         }
     }
}

int lca(int a,int b,int w)
{
    
    static int distance[N*2];  //每条路径上面的 最大值 和 次大值 都存在distance中、
    /*
    其次为什么最多有N*2个路径要保存呢?
    其实这里不算路径,顶多是一个点跳到另外一个点的一道弧,
    根据所画的图,每一个弧都可以看成一个链,有最大值和次大值
    最多有N个点,所以最多N条弧,每个弧保存最大值和次大值,
    就是 N*2
    */
    
    cnt=0;
    
    
    if(depth[a]<depth[b])
    {
        swap(a,b);
    }
    
    for(int k=16;k>=0;k--)
    {
        if(depth[fa[a][k]]>=depth[b])  //当跳k步深度还不够时
        {
            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])
           {
               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];
           }
        }
        //到目前为止只是跳到了最近公共祖先的下一层,所以还要再跳一层
        distance[cnt++]=d1[a][0];
        distance[cnt++]=d1[b][0];
    }
    
    //distance存的是集合,我们要在里面找出最大值和次大值
    //记住了,最终返回的是一个差值,所以还要找出旧边中
    int td1=-inf,td2=-inf;
    for(int i=0;i<cnt;i++)
    {
        int d=distance[i];
        if(d>td1) td2=td1,td1=d;
        else if(d!=td1&&d>td2) td2=d;
    }
    
    if(w>td1) return w-td1; //判断如果可以用最大边
    if(w>td2) return w-td2;  //如果不可以用最大边,就用次大边
    
    
    return inf; //否则返回+无穷大
}

void bfs(int root)
{
    memset(depth,0x3f,sizeof depth); //和dis一样要初始化为0x3f
    depth[0]=0;
    depth[root]=1;
    
    int hh=0,tt=-1;
    q[++tt]=1;
    while(hh<=tt)
    {
        int t=q[hh++];
        
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            
            if(depth[j]>depth[t]+1)
            {
                depth[j]=depth[t]+1;
                q[++tt]=j;
                
                //下面就是预处理一下:三个倍增数组
                //首先先把步长为0的都初始化了
                fa[j][0]=t;
                d1[j][0]=w[i];
                d2[j][0]=-inf; //不存在次大边,所以设置为-无穷
                for(int k=1;k<=16;k++)
                {
                    int anc=fa[j][k-1]; //先存一下到达的点
                    fa[j][k]=fa[anc][k-1];
                    /*
                    难点在最大值和次大值的更新上
                    之前说了,最大值和次大值,就是在
                    四个值里面选,所以我们就在四个值
                    里面选就好了。
                    */
                   
                    int distance[5]={d1[j][k-1],d2[j][k-1],d1[anc][k-1],d2[anc][k-1]}; //存一下四个值
                    //然后就是在上面这一整个集合中求最大值和次大值
                    
                    d1[j][k]=d2[j][k]=-inf; //先初始化为负无穷,因为要求最大和次大
                    
                    for(int u=0;u<4;u++)
                    {
                        int d=distance[u];
                        if(d>d1[j][k])
                        {
                            d2[j][k]=d1[j][k];
                            d1[j][k]=d;
                        }
                        else if(d!=d1[j][k]&&d>d2[j][k])
                        {
                            d2[j][k]=d;
                        }
                    }
                    
                }
            }
        }
    }
}


int main()
{
    scanf("%d%d",&n,&m);
   // memset(h,-1,sizeof h);
    
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        edges[i]={a,b,c};
    }
    
    LL sum=kruskal();
    
    build();  //建那些 used被标记为true的边
    
    bfs(1); //用bfs初始化三个倍增数组f[][18],d1[][18],d2[][18]
    
    
    LL res=1e18;
    for(int i=0;i<m;i++) //枚举一下非树边
    {
        if(!edges[i].used)
        {
            int a=edges[i].a,b=edges[i].b;
            int w=edges[i].w;
            res=min(res,sum+lca(a,b,w));  
        //lca返回的是类似于之前(o(n*n)版本的)的 加上 w(非树边) 并且 减去一个树边的边权
        //相当于新边-旧边的差值
        }
    }
    
    printf("%lld\n",res);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值