次小生成树
题目描述
核心思路
这题其实与秘密的牛奶运输是完全一样的,都是求解严格次小生成树。只不过在那一题中,我们是用 O ( n 2 ) O(n^2) O(n2)的时间复杂度暴力求出了树中任意两点间的距离。但是我们知道LCA可以快速地求解出树中任意两点间的距离。因此,在这一题中,我们将使用LCA来进行优化。
如下图分析:
这一题对求解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的过程中递推求解,如下图:
算法设计:
- 使用Kruskal算法求出最小生成树,算出权值总和为 s u m sum sum
- 将这棵最小生成树用图构建出来
- 在BFS过程中预处理
fa[][]
和求解出d1[][]
和d2[][]
- 依次枚举每条非树边 w w w,将其加入树中,一定会形成环,去掉环中树边的最大值或者次大值得到另一棵树,这些树中权值最小的就是次小生成树。
注意点:
- 设树边为 w [ i ] w[i] w[i],非树边为 w w w,那么次小生成树为 s u m + w − w [ i ] sum+w-w[i] sum+w−w[i],我们设增量 Δ = w − w [ i ] \Delta=w-w[i] Δ=w−w[i],我们在LCA函数中求出了这个 w w w(最大边或次大边),但是函数返回值的其实就是这个增量 Δ \Delta Δ,而不是 w w w,这点要注意哦
- 对于数组 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。
- 还有一点,题目中给定这个无向图的最大边数 M M M是3e5,这是原图的最大边数。但是当我们对已经得到的最小生成树构建图时,由于树中的边是无向边,有 n n n个顶点,那么有 n − 1 n-1 n−1条边,那么构建这个最小生成树的图时需要 2 ( n − 1 ) 2(n-1) 2(n−1)条边,不妨开大点就是 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;
}