最小生成树学习笔记

又是晴朗的一天!

最小生成树讲解

例题

我认为讲得比较好的博客

你以为我是这么懒得博主只复制粘贴别人博客的地址吗?

不是吗? 不是的!

当然,里面讲了两种算法的优势与劣势。

UPDATA: 对于生成树而言,你可以随意的插入一条边,并删除一条和他相关的边,如果你发现没有任何一条边加入可以使权值和变小,那么他就是最小生成树,至于证明啊,可以参照Kruskal的证明。

Prim

这种算法我不是很会,比较建议进大佬博客阅读可以有更加舒适的体验。

简单来说就是一开始有个点now,有个dis数组,里面的无限大的那种。

步骤:

  1. dis[now]=0
  2. 寻找与now相连的边k与点y(k号边连接x与y号点),将k的边权拿去更新dis[y](取最小值)。
  3. 更新完后,找到没有加入生成树的点且dis值是最小的,代替now,返回第一步,直到n个点都加入了最小生成树

用大佬的话讲:dis数组为已用点到未用点的最短距离(当然,我个人认为这句话不是很完整,但我语文不好。。。)

具体大佬博客都有。

当然,至于感性的理解你可以理解为这个最小生成树是动态的,对于一个未加入进来的点,我们肯定希望一个与它最近(边权最小)的点与它连接。

所以就有了第2步和第3步,然后就有人问了,我们现在把(x)加入到最小生成树T,那万一后面新加入的点(y)(x)距离更近那我们不是亏了,可以先更新(x)再更新(y)啊?但是我们要明白边的关系是双向的,既然我们先更新了(x),证明(T)(y)的距离比到(x)要大,那么我们为什么不能用(x)去更新(y)呢?

UPDATA:

至于证明,你不难证明这种取边的方法,你没法用剩余的边代替原来的边的方案。

//摘自参考文献
#include<bits/stdc++.h>
using namespace std;
#define re register
#define il inline
il int read()
{
    re int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}//快读,不理解的同学用cin代替即可
#define inf 123456789
#define maxn 5005
#define maxm 200005
struct edge
{
    int v,w,next;
}e[maxm<<1];
//注意是无向图,开两倍数组
int head[maxn],dis[maxn],cnt,n,m,tot,now=1,ans;
//已经加入最小生成树的的点到没有加入的点的最短距离,比如说1和2号节点已经加入了最小生成树,那么dis[3]就等于min(1->3,2->3)
bool vis[maxn];
//链式前向星加边
il void add(int u,int v,int w)
{
    e[++cnt].v=v;
    e[cnt].w=w;
    e[cnt].next=head[u];
    head[u]=cnt;
}
//读入数据
il void init()
{
    n=read(),m=read();
    for(re int i=1,u,v,w;i<=m;++i)
    {
        u=read(),v=read(),w=read();
        add(u,v,w),add(v,u,w);
    }
}
il int prim()
{
    //先把dis数组附为极大值
    for(re int i=2;i<=n;++i)
    {
        dis[i]=inf;
    }
    //这里要注意重边,所以要用到min
    for(re int i=head[1];i;i=e[i].next)
    {
        dis[e[i].v]=min(dis[e[i].v],e[i].w);
    }
    while(++tot<n)//最小生成树边数等于点数-1
    {
        re int minn=inf;//把minn置为极大值
        vis[now]=1;//标记点已经走过
        //枚举每一个没有使用的点
        //找出最小值作为新边
        //注意这里不是枚举now点的所有连边,而是1~n
        for(re int i=1;i<=n;++i)
        {
            if(!vis[i]&&minn>dis[i])
            {
                minn=dis[i];
                now=i;
            }
        }
        ans+=minn;
        //枚举now的所有连边,更新dis数组
        for(re int i=head[now];i;i=e[i].next)
        {
            re int v=e[i].v;
            if(dis[v]>e[i].w&&!vis[v])
            {
                dis[v]=e[i].w;
            }
        }
    }
    return ans;
}
int main()
{
    init();
    printf("%d",prim());
    return 0;
}

Kruskal

这是我学的算法,比Prim好理解多了!(小骄傲)

步骤:

  1. 排序所有的边,按边权从大到小排序。
  2. 从小到大遍历边,判断边的两端是否在同一集合内(用并查集判断),是就将两个点的并查集合并,并且cnt++。
  3. 重复二步骤直到遍历完或者cnt==n-1。

感觉好久不写博客人都变懒了。。。

在这里插入图片描述

update:但是正确性怎么保证呢?

首先,我们只要证明最小生成树包含最小边,然后不断推就行了。

首先,对于一棵不包含最小边的生成树。

那么肯定能够把最小边插入进去,就会构成一个环,然后在环中随便去掉一条边,就可以了,然后次小边什么的都可以无限的推理下去,然后就有了眼前的这个算法。

代码君:

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
int  fa[5100],n,m;
struct  node
{
	int  x,y,c;
}a[210000];
int  findfa(int  x)//并查集的路径压缩
{
	if(fa[x]!=x)fa[x]=findfa(fa[x]);
	return  fa[x];
}
bool  cmp(node  x,node  y){return  x.c<y.c;}
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=n;i++)fa[i]=i;
	for(int  i=1;i<=m;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].c);
	sort(a+1,a+m+1,cmp);//排序
	int  ans=0,tt=0;
	for(int  i=1;i<=m;i++)
	{
		int  x=a[i].x,y=a[i].y;
		int  tx=findfa(x),ty=findfa(y);
		if(tx!=ty)
		{
			ans+=a[i].c;tt++;
			if(tt==n-1)break;
			fa[tx]=ty;
		}
	}
	printf("%d
",ans);
	return  0;
}

于是,最小生成树模版的学习就这么让我这个博主过掉了。。。

例题

练习1

练习1

这道题目是最小生成树?

额额额。

好吧,总之就是dis数组,dis[i]储存有多少个点在最短路的情况下能到达第i个点,然后全部dis乘起来,貌似就A了。

#include<cstdio>
#include<cstring>
#define  N  1100
#define  M  1100000
using  namespace  std;
struct  node
{
	int  y,c,next;
}a[M];int  len,last[N];
inline  void  ins(int  x,int  y,int  c)
{
	len++;
	a[len].y=y;a[len].c=c;a[len].next=last[x];last[x]=len;
}
int  dis[N],n,m;
long  long  rem[N],ans;
int  head,tail,list[N];
bool  v[N];
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=m;i++)
	{
		int  x,y,c;scanf("%d%d%d",&x,&y,&c);
		ins(x,y,c);ins(y,x,c);
	}
	for(int  i=1;i<=n;i++)dis[i]=999999999;
	dis[1]=0;rem[1]=1;head=1;tail=2;list[head]=1;
	while(head!=tail)//伟大的SPFA呀!
	{
		int  x=list[head++];v[x]=false;if(head==n+1)head=1;
		for(int  k=last[x];k;k=a[k].next)
		{
			int  y=a[k].y;
			if(dis[x]+a[k].c<dis[y])
			{
				dis[y]=dis[x]+a[k].c;
				rem[y]=1;
				if(!v[y])
				{
					v[y]=true;
					list[tail++]=y;if(tail==n+1)tail=1;
				}
			}
			else  if(dis[x]+a[k].c==dis[y])rem[y]++;
		}
	}
	ans=1;
	for(int  i=1;i<=n;i++)ans=(ans*rem[i])%2147483647;//统计答案
	printf("%lld
",ans);
	return  0;
}

练习2

练习2

先建一个完全图

然后用某K算法跑最小生成树,只不过找了((n-k))条边就要退出,同时记录最大值。

因为剩下的((k-1))条边可以用(k)个卫星解决。

#include<cstdio>
#include<cmath>
#include<algorithm>
#define  N  510
#define  M  260000
using  namespace  std;
struct  node
{
	int  x,y;
	double  c;
}tr[M];int  len;
inline  void  ins(int  x,int  y,double  c)
{
	len++;
	tr[len].x=x;tr[len].y=y;tr[len].c=c;
}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}
int  fa[N];
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	else  return  fa[x]=findfa(fa[x]);
}
int  n,m;
struct  dian
{
	double  x,y;
}a[N];
inline  double  calc(double  x1,double  y1,double  x2,double  y2){return  sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));}//计算直线距离
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=n;i++)
	{
		int  x,y;scanf("%d%d",&x,&y);
		a[i].x=x*1.0;a[i].y=y*1.0;fa[i]=i;
		for(int  j=1;j<i;j++)ins(i,j,calc(a[i].x,a[i].y,a[j].x,a[j].y));
	}
	sort(tr+1,tr+len+1,cmp);
	int  cnt=0;
	double  ans=0.0;
	for(int  i=1;i<=len;i++)
	{
		int  x=tr[i].x,y=tr[i].y;
		int  tx=findfa(x),ty=findfa(y);
		if(tx!=ty)
		{
			fa[tx]=ty;
			cnt++;
			if(cnt==(n-m))
			{
				ans=tr[i].c;
				break;
			}
		}
	}
	printf("%.2lf",ans);
	return  0;
}

练习3

练习3

其实很简单,建一个新的点编号n+1,与其他点连边,如果说n+1的点与其他点的连边被选中,代表建一个发电站,就这样。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define  N  310
#define  M  91000
using  namespace  std;
struct  node
{
	int  x,y,c;
}a[M];int  len;
inline  void  ins(int  x,int  y,int  c){len++;a[len].x=x;a[len].y=y;a[len].c=c;}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}
int  fa[N];
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	return  fa[x]=findfa(fa[x]);
}
int  n;
int  main()
{
	scanf("%d",&n);
	for(int  i=1;i<=n;i++)//建立n+1的点
	{
		int  x;scanf("%d",&x);
		ins(i,n+1,x);fa[i]=i;
	}
	for(int  i=1;i<=n;i++)
	{
		for(int  j=1;j<=n;j++)
		{
			int  x;scanf("%d",&x);
			if(i<j)ins(i,j,x);
		}
	}
	sort(a+1,a+len+1,cmp);
	int  cnt=0,ans=0;
	for(int  i=1;i<=len;i++)
	{
		int  x=a[i].x,y=a[i].y;
		int  tx=findfa(x),ty=findfa(y);
		if(tx!=ty)
		{
			fa[tx]=ty;
			cnt++;ans+=a[i].c;if(cnt==n)break;//有n+1个点
		}
	}
	printf("%d
",ans);
	return  0;
}

练习4

练习4
首先,明白一件事情,在最小生成树里面,两个集合之间的边肯定要是最小的,那么就可以肯定一种方法,就是对树再跑一遍最小生成树,每个集合的点用带权并查集维护,同时呢,我们用乘法原理算出答案。

你以为这样就完了?

边权要从小到大排序,为什么,因为后边在两个集合内加的边的权值不能小于两个集合中的任意一条边。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  110000
using  namespace  std;
typedef  long  long  ll;
struct  node
{
	int  x,y;
	ll  c;
}tr[N];int  len;
inline  void  ins(int  x,int  y,ll  c){len++;tr[len].x=x;tr[len].y=y;tr[len].c=c;}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}//排序
int  fa[N],num[N];
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	else  return  fa[x]=findfa(fa[x]);
}
int  n;
int  main()
{
	scanf("%d",&n);
	for(int  i=1;i<n;i++)
	{
		int  x,y;ll  c;scanf("%d%d%lld",&x,&y,&c);
		ins(x,y,c);
	}
	for(int  i=1;i<=n;i++)fa[i]=i,num[i]=1;
	sort(tr+1,tr+len+1,cmp);//排序
	ll  ans=0;
	for(int  i=1;i<=len;i++)
	{
		int  tx=findfa(tr[i].x),ty=findfa(tr[i].y);
		ans+=(tr[i].c+1)*(num[tx]*num[ty]);ans--;//乘法原理计算,同时注意,ans要--,因为有一条原来的边。
		fa[tx]=ty;num[ty]+=num[tx];//带权并查集
	}
	printf("%lld
",ans);
	return  0;
}

练习5

练习5

严格次小生成树!数据这么小。

那么就暴力

首先要知道,删掉树边就行了,你不删树边跑的还是最小生成树呀!

而且我们得明白:

肯定存在一种严格次小生成树与最小生成树仅一边之差,为何?

我们知道集合中有许多边连接子集合,那么次小生成树不就是讲一条边换成另一条比较大的边吗,很容易想得到,因此我们就只删掉树边就行了。

于是我们只需要枚举外面的一条边进来就行了。

其实正解差不多出来了,但是码量太大了,打暴力无非是最佳选择

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define  N  510
#define  M  11000
using  namespace  std;
typedef  long  long  ll;
struct  node
{
	int  x,y,next;
	ll  c;
	bool  bk;
}a[M];int  len,last[N];
inline  void  ins(int  x,int  y,ll  c){len++;a[len].x=x;a[len].y=y;a[len].c=c;a[len].next=last[x];last[x]=len;}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}
int  fa[N],n,m;
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	return  fa[x]=findfa(fa[x]);
}
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=m;i++)
	{
		int  x,y;ll  c;scanf("%d%d%lld",&x,&y,&c);
		ins(x,y,c);
	}
	for(int  i=1;i<=n;i++)fa[i]=i;
	sort(a+1,a+m+1,cmp);
	int  cnt=0;ll  ans=0;
	for(int  i=1;i<=m;i++)
	{
		int  x=a[i].x,y=a[i].y;
		int  tx=findfa(x),ty=findfa(y);
		if(tx!=ty)
		{
			fa[tx]=ty;
			a[i].bk=true;
			ans+=a[i].c;cnt++;
			if(cnt==n-1)break;
		}
	}
	ll  zans=999999999999999;
	for(int  k=1;k<=m;k++)
	{
		if(a[k].bk==true)//是树边
		{
			for(int  i=1;i<=n;i++)fa[i]=i;
			for(int  i=1;i<=m;i++)//构建删掉一条边的最小生成树
			{
				if(i!=k  &&  a[i].bk)
				{
					int  tx=findfa(a[i].x),ty=findfa(a[i].y);
					fa[tx]=ty;
				}
			}
			for(int  i=1;i<=m;i++)//枚举任意一条边
			{
				if(i==k)continue;
				int  x=a[i].x,y=a[i].y;
				int  tx=findfa(x),ty=findfa(y);
				if(tx!=ty)
				{
					if(a[i].c>a[k].c  &&  ans-a[k].c+a[i].c<zans)zans=ans-a[k].c+a[i].c;
				}
			}
		} 
	}
	printf("%lld
",zans);//输出
	return  0;
}

练习6

练习6

这道题目,一看一脸蒙蔽,二看已经爬上了阳台,三看.......。以上纯属口胡。

那么这道题目,我们可以二分枚举一个k,让所有白边加上一个k,二分自己想。

为什么正确

首先要在排序把白边优先级设成比黑边高

我们只需要统计白边数量大于等于need的情况,为什么?

我们先不考虑二分,就是for循环,当我i++的时候,白边的数量也会--,其实减掉的是原本白边与黑边权值相同的部分,于是,当我这一次i白边数量大于need,而i++后就小于了,我们可以得知,在i的情况下,把部分白边换成权值相同的黑边,那么也可以凑到need,二分同理。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  51000
#define  M  110000
using  namespace  std;
int  fa[N];
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	return  (fa[x]=findfa(fa[x]));
}
struct  node
{
	int  x,y,c,type;
}a[M];int  len,dep/*白边的系数*/,need;
inline  void  ins(int  x,int  y,int  c,int  type){len++;a[len].x=x;a[len].y=y;a[len].c=c;a[len].type=type;}
inline  bool  cmp(node  x,node  y){return  (x.c+x.type*dep)==(y.c+y.type*dep)?x.type>y.type:(x.c+x.type*dep)<(y.c+y.type*dep);}
int  n,m,ans,zans;
int  check()/*查看目前的最小生成树的白边的数量*/
{
	sort(a+1,a+m+1,cmp);
	for(int  i=1;i<=n;i++)fa[i]=i;
	int  cnt=0,bai=0;ans=0;
	for(int  i=1;i<=m;i++)
	{
		int  x=a[i].x,y=a[i].y;
		int  tx=findfa(x),ty=findfa(y);
		if(tx!=ty)
		{
			fa[tx]=ty;
			cnt++;ans+=a[i].c;
			if(a[i].type)bai++,ans+=dep;
			if(cnt==n-1)return  bai;
		}
	}
}
int  main()
{
	scanf("%d%d%d",&n,&m,&need);
	for(int  i=1;i<=m;i++)
	{
		int  x,y,c,type;scanf("%d%d%d%d",&x,&y,&c,&type);
		ins(x+1,y+1,c,type^1);
	}
	int  l=-100,r=100;
	while(l<=r)
	{
		dep=(l+r)/2;
		int  tt=0;
		if((tt=check())>=need)zans=ans-need*dep/*这里可能比较难理解*/,l=dep+1;//记录答案
		else  r=dep-1;
	}
	printf("%d
",zans);
	return  0;
}

练习7

首先,明白,在最小生成树里面,同样边权的边的数量是固定的。

证明:

第一次,将最小边权的边所有放进一个图里面,成为好几个联通块,并且删掉一些边让这些联通块成为一个树,并且缩点,加入次小边权的边.......。(由于删除的边的数量固定,所以证毕)。

那么,我们就可以暴力统计每种边权能构成多少种情况,然后暴力。。。

而且同样边权的边数量少呀!

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  110
#define  M  1100
using  namespace  std;
typedef  long  long  ll;
struct  node
{
	int  x,y;ll  c;
}a[M];int  len;
inline  void  ins(int  x,int  y,ll  c){len++;a[len].x=x;a[len].y=y;a[len].c=c;}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}
int  fa[N];
int  findfa(int  x)
{
	if(fa[x]==x)return  x;
	return  (fa[x]=findfa(fa[x]));
}
int  lb[M],rb[M],b[M],tt,ne[M],de[M];
int  n,m;
ll  cntt;
void  work(int  now,int  kk,int  type)//利用递归统计情况
{
	if(kk==ne[type])
	{
		cntt++;
		return  ;
	}
	if(now>rb[type])return  ;
	int  p[N];
	memcpy(p,fa,sizeof(p));
	int  tx=findfa(a[now].x),ty=findfa(a[now].y);
	if(tx!=ty)
	{
		fa[tx]=ty;
		work(now+1,kk+1,type);
	}
	memcpy(fa,p,sizeof(p));
	work(now+1,kk,type);
}
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=m;i++)
	{
		int  x,y;ll  c;scanf("%d%d%lld",&x,&y,&c);
		ins(x,y,c);
	}
	sort(a+1,a+m+1,cmp);
	for(int  i=1;i<=m;i++)//统计同样边权的边有多少条
	{
		if(a[i].c!=a[i-1].c)
		{
			tt++;b[tt]=a[i].c;
			de[i]=tt;
			lb[tt]=rb[tt]=i;
		}
		else  rb[tt]++,de[i]=tt;
	}
	int  cnt=0;
	for(int  i=1;i<=n;i++)fa[i]=i;
	for(int  i=1;i<=m;i++)
	{
		int  tx=findfa(a[i].x),ty=findfa(a[i].y);
		if(tx!=ty)
		{
			fa[tx]=ty;
			ne[de[i]]++;cnt++;
			if(cnt==n-1)break;
		}
	}
	if(cnt!=n-1)//不存在最小生成树
	{
		printf("0
");
		return  0;
	}
	//最小生成树
	ll  ans=1;
	for(int  i=1;i<=n;i++)fa[i]=i;
	for(int  i=1;i<=tt;i++)//统计答案
	{
		cntt=0;work(lb[i],0/*添加了多少条边*/,i);
		ans=(ans*cntt)%31011;
		for(int  j=lb[i];j<=rb[i];j++)
		{
			int  tx=findfa(a[j].x),ty=findfa(a[j].y);
			if(tx!=ty)fa[tx]=ty;
		}
	}
	printf("%lld
",ans);
	return  0;
}

练习8

想必一路做过来心情十分复杂吧!

不闹了不闹了。

正规的严格次小生成树,前面我们说了肯定存在一种严格次小生成树与最小生成树仅一边之差。

不过我们枚举的是那条边被踢掉,但是枚举那条边加入不是更优吗?

于是我们枚举每一条边加入最小生成树,但是删掉哪条树边呢?我们可以统计这条边两端的点在树中两个点路径上边权的最大值,其实就是用另一条边代替连接集合的作用,而且这条边一定比最大值要大,不然为什么不在最小生成树时选他呢?

不对!!!

也许可以等于最大值呢?没事,多统计次大值不就行了,嘿嘿嘿。

如何处理树上次大值与最大值?树上倍增LCA。

丑陋的代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  110000
using  namespace  std;
typedef  long  long  ll;
inline  ll  mymax(ll  x,ll  y){return x>y?x:y;}//取最小值
inline  ll  mymin(ll  x,ll  y){return x<y?x:y;}//取最大值
struct  Trnode//树边
{
    int  y,next;
    ll  c;
}tra[N*2];int  tlen,last[N];
inline  void  tins(int  x,int  y,ll  c){tlen++;tra[tlen].y=y;tra[tlen].c=c;tra[tlen].next=last[x];last[x]=tlen;}

struct  LCA
{
    int  d;
    ll  m1/*最大*/,m2/*次大*/;
}st[N][30];/*LCA*/int  Log[N];//处理LCA
inline  void  pd(LCA  &x,LCA  y)//用y的最大值与次大值更新x
{
    if(x.m1>y.m1)x.m2=mymax(x.m2,y.m1);
    else  if(x.m1<y.m1)
    {
        x.m2=mymax(x.m1,y.m2);
        x.m1=y.m1;
    }
    else  x.m2=mymax(x.m2,y.m2);
}
struct  Tree//处理树
{
    int  l,r,dep;
}tr[N];int  son[N],socnt;//儿子的内存池
void  dfs(int  x)//处理倍增的LCA
{
    for(int  i=1;i<=Log[tr[x].dep];i++)
    {
        pd(st[x][i],st[x][i-1]);pd(st[x][i],st[st[x][i-1].d][i-1]);
        st[x][i].d=st[st[x][i-1].d][i-1].d;
    }
    tr[x].l=socnt+1;
    for(int  k=last[x];k;k=tra[k].next)
    {
        int  y=tra[k].y;
        if(y!=st[x][0].d)
        {
            son[++socnt]=y;
            st[y][0].d=x;st[y][0].m1=tra[k].c;
            tr[y].dep=tr[x].dep+1;
        }
    }
    tr[x].r=socnt;
    for(int  i=tr[x].l;i<=tr[x].r;i++)dfs(son[i]);
}
LCA  lca(int  l,int  r)//处理树上两点间的最大值与次大值
{
    LCA  x;x.d=0;x.m1=0;x.m2=0;
    if(tr[l].dep>tr[r].dep){int  tt=l;l=r;r=tt;}
    for(int  i=Log[tr[r].dep-tr[l].dep];i>=0;i--)//到达同一层数
    {
    	if(tr[st[r][i].d].dep>=tr[l].dep)
    	{
    		pd(x,st[r][i]);
        	r=st[r][i].d;
		}
    }
    x.d=l;
    if(l==r)return  x;
    for(int  i=Log[tr[l].dep];i>=0;i--)//不断往上
    {
        if(st[l][i].d!=st[r][i].d)
        {
            pd(x,st[l][i]);pd(x,st[r][i]);
            l=st[l][i].d;r=st[r][i].d;
        }
    }
    x.d=st[l][0].d;
    pd(x,st[l][0]);pd(x,st[r][0]);
    return  x;
}

struct  node//普通的图边
{
    int  x,y;
    ll  c;
    bool  type;
}a[N*3];int  len;
inline  void  ins(int  x,int  y,ll  c){len++;a[len].x=x;a[len].y=y;a[len].c=c;}
inline  bool  cmp(node  x,node  y){return  x.c<y.c;}

int  fa[N];
int  findfa(int  x)
{
    if(fa[x]==x)return  x;
    return  (fa[x]=findfa(fa[x]));
}

int  n,m;
int  main()
{
//	freopen("1.in","r",stdin);
    scanf("%d%d",&n,&m);
    for(int  i=2;i<=n;i++)Log[i]=Log[(i>>1)]+1;
    for(int  i=1;i<=m;i++)
    {
        int  x,y;ll  c;scanf("%d%d%lld",&x,&y,&c);
        ins(x,y,c);
    }
    sort(a+1,a+m+1,cmp);
    int  cnt=0;
    ll  mans=0;
    for(int  i=1;i<=n;i++)fa[i]=i;
    for(int  i=1;i<=m;i++)//处理最小生成树
    {
        int  tx=findfa(a[i].x),ty=findfa(a[i].y);
        if(tx!=ty)
        {
            a[i].type=true;
            fa[tx]=ty;
            tins(a[i].x,a[i].y,a[i].c);tins(a[i].y,a[i].x,a[i].c);
            mans+=a[i].c;
            cnt++;if(cnt==n-1)break;
        }
    }
    tr[1].dep=1;dfs(1);
    ll  minimax=999999999999999;
    for(int  i=1;i<=m;i++)//枚举那一条边
    {
        if(a[i].type==false)
        {
            int  x=a[i].x,y=a[i].y;
            LCA  ans=lca(x,y);
            if(ans.m1!=a[i].c)minimax=mymin(mans+(a[i].c-ans.m1),minimax);
            else  if(ans.m2!=0)minimax=mymin(mans+(a[i].c-ans.m2),minimax);//统计答案
        }
    }
    printf("%lld
",minimax);
    return  0;
}

小结

在博主写完博客的时候,离过年就不远了,过完年,差不多就要上学了QAQ。

原文地址:https://www.cnblogs.com/zhangjianjunab/p/10322734.html