题目描述
给定一张 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
题目分析
这道题是秘密的牛奶运算的加强版,如果之前没做过次小生成树类似的题目的话建议先看看这道题。本题是对于这道题的做法进行了一个倍增的优化。
解题步骤:
- 我们首先要求出最小生成树,并记录最小生成树所包含的边。
- 将这个最小生成树单独建成图。
- 这道题的的数据范围比较大,暴力预处理图上任意两点间路径上的最大值和次大值会直接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步的路径上的次大边权
- 对于如何预处理出这三个数组,我们可以参考倍增法求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]。 - 枚举所有的非树边(u,v,w),并将这条非树边加入到树中。给最小生成树加上一条边之后,这个图必定会形成一个环,然后我们就可以在这个环上找出除该非树边之外的最大边dmax1和次大边dmax2。
我们可以求出并记录u和v分别到其最近公共祖先p的路径上产生的所有最大值和次大值。然后枚举所有这些最大值和次大值,并找出它们的最大值和次大值即为最大边dmax1和次大边dmax2。 - 当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;
}