次小生成树
一行小字。这篇博客是我的gg之前写的博客,一直想写博客,但是直到gg,都没写几篇博客。我再把这篇博客翻出来的时候,不知已经过了半年还是一年了。博客园的博客系统做的不错,就是数学公式和一些图片会乱,图片的话手动上传一下还ok。数学公式不影响阅读,大家自行理解就ok嘤嘤~
前置知识
-
最小生成树Kurskal算法
-
最近公共祖先LCA!
引入问题
定义
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的次少的边。
模板题
解决方法
想法
把所有生成树按照权值之和从小到小排序,求排在第二位的生成树。注意,如果最小生成树不唯一,次小生成树的权值和最小生成树相同。
解法一
次小生成树不会和最小生成树相同,因此可以枚举最小生成树中不在次小生成树中出现的边。 注意最小生成树只有n-1条边,所以只需枚举n-1次。每次在剩下的边里,求一次最小生成树。
步骤
-
Kruskal算法求出最小生成树
-
枚举最小生成树的每一条边,对这条边做标记,再进行一次Kruskal算法,Kruskal算法中,跳过被标记的边,求最小生成树,记录答案。
-
去步骤2中所记录答案的最小值即为次小生成树的边权之和。(可直接在步骤2中进行)
复杂度
-
\(O(M\log M+M)\)
-
\(O(NM)\)
-
\(O(N)\)
总复杂度\(O(NM)\)
有没有更快些的解决方法呢?
补充知识
可行交换与临集
- T为图G的一棵生成树,对于非树边a和树边b,插入边a并且删除边b的操作记为(+a,-b)
- 如果T+a-b仍然是一棵生成树,称(+a,-b)是一个可行交换
- 由T进行一次可行交换以后得到的新的生成树的集合称为T的临集
显然,可得定理:次小生成树在最小生成树的临集中
更好的解法
- 枚举要加入哪条新边,在最小生成树中加入一条边u-v以后,图上会出现一条回路,如果想保持该图为生成树,需删除一条边,删除的边必须是在最小生成树上u到v的路径上,如果想要删除边后的新的生成树有可能为次小生成树,必须要求删除的边这条路径的最长边。
如图,枚举出一条不在最小生成树上的边E1,如果连接E1,E1两端点为(1,4),如果想连接E1后依然保证新图为生成树,需删除路径(1,4)上一条最小生成树上的边。为了求出次小生成树,我们选择删除原树上的尽可能大的边,即删除边(3,4),连接边E1。
如图为新生成的树。
总结一下,新的算法是这样的
- 求出最小生成树之后,枚举每一个不在树上的边,对于边(u,v),连上此边,删去原来树上路径(u,v)上最长的边。
- 题目中要求的是严格次小生成树,因此,当原来树上路径(u,v)上最长的边的权值等于新连边(u,v)时,是不符合要求的,我们需要找次长边。
那么枚举每一条边复杂度是\(O(M)\),对每一条边用dfs求(u,v)上的最长&次长边,复杂度是\(O(N)\),总复杂度是\(O(MN)\)没有遍低呀~
因此,我没需要对算法进行优化,可以通过dfs\(O(N)\)来预处理每个点到其k代父节点的路径上的最长边和次长边,再通过lca倍增的方法在\(O(\log N)\)复杂度下求出u,v两点之间的最长边和次长边。
预处理
对于每一个点u,我们记记录anc[u][k]数组,u是节点编号,k代表其$2^k $代父节点。
记录m1[u][k]数组,其中,u是节点编号,k代表从u节点往上\(2^k\)个点,数组的值是u点到u节点上方$2^k $个点间的最长边,可得状态转移方程
记录m2[u][k]数组,u,k含义同m1,数组的值是u点到u节点上方$2^k $个点间的次长边,可得状态转移方程
经过一次dfs后,我们即可预处理完成以上内容。
遍历\(N\)个点,每个点要进行\(\log N\)次计算,复杂度为 \(O(N\log N)\)
LCA同时求值
对于点u和v,我们在有预处理的情况下想求其两点之间路径的最长边和次长边,可进行以下操作
用倍增法求LCA的方式将两个点向上跳,跳的同时记录m1和m2的值。
向上跳的做法与倍增法求LCA相同,如果没有了解,请先学习倍增法求LCA,此处不做过多介绍。
具体代码如下:
/*参数ma为要新建的边的权值,因为要求严格次小生成树,
所以在原树中删除的边必须严格小于新建的边的权值,
即lca函数的返回值(也就是在原树中删除的边的权值)需严格小于参数ma。*/
long long findBig(long long u,long long i,long long ma){
if(m1[u][i]!=ma){
return m1[u][i];
}
return m2[u][i];
}
long long lca(long long u,long long v,long long ma){
if(dep[u]<dep[v]){
swap(u,v);
}
long long res = -INF;
for(int i = 20;i>=0;--i){
if(dep[ancestor[u][i]]<dep[v]){
continue;
}
res = max(res,findBig(u,i,ma));
u = ancestor[u][i];
}
if(u==v){
return res;
}
for(int i = 20;i>=0;--i){
if(ancestor[u][i]!=ancestor[v][i]){
res = max(res,findBig(u,i,ma));
res = max(res,findBig(v,i,ma));
u = ancestor[u][i];
v = ancestor[v][i];
}
}
res = max(res,findBig(u,0,ma));
res = max(res,findBig(v,0,ma));
return res;
}
步骤
- Kruskal算法求出最小生成树
- dfs预处理
- 枚举每一条不在最小生成树上的边,求出最小生成树上的边权之和加上该边之和,减去该边所连两点在原树上的路径间的最长边(次长边),即为新的生成树的边权之和,记录答案。
- 在步骤3中,所记录答案的最小值即为次小生成树的边权之和。(可直接在步骤3中进行)
复杂度
- \(O(M\log M+M)\)
- \(O(N\log N )\)
- \(O(M\log N)\)
- \(O(N)\)
总复杂度为:\(O(MlogN)\)
具体代码
//
// Created by SeverusNg on 2020/7/24.
//
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
const long long MAXN = 100005;
const long long MAXM = 300005;
const long long INF = 0x3f3f3f3ff3f3f3f;
using namespace std;
struct edge{
long long u,v,w;
int next;
bool operator<(const edge &e)const{
return w<e.w;
}
} ed[MAXM*2],pool[MAXM];
int head[MAXN];
int topE;
void addE(int u,int v,int w){
ed[++topE].u = u;
ed[topE].v = v;
ed[topE].w = w;
ed[topE].next = head[u];
head[u] = topE;
return;
}
int fa[MAXN];
int find(int u){
if(fa[u]==u){
return fa[u];
}
fa[u] = find(fa[u]);
return fa[u];
}
void merge(int u,int v){
u = find(u);
v = find(v);
fa[u] = v;
return;
}
bool use[MAXM];
long long N,M;
long long mst;
void kruskal(){
sort(pool+1,pool+M+1);
for(int i = 1;i<=M;++i){
long long u = pool[i].u;
long long v = pool[i].v;
long long w = pool[i].w;
if(find(u)!=find(v)){
mst += w;
merge(u,v);
addE(u,v,w);
addE(v,u,w);
use[i]=true;
}
}
return;
}
int dep[MAXN];
int ancestor[MAXN][25];
long long m1[MAXN][25];
long long m2[MAXN][25];
long long findBig(long long u,long long i,long long ma){
if(m1[u][i]!=ma){
return m1[u][i];
}
return m2[u][i];
}
void dfs(long long u,long long father,long long w){
ancestor[u][0] = father;
dep[u] = dep[father]+1;
m1[u][0]=w;
m2[u][0]=-INF;
for(int i = 1;i<=20;++i){
ancestor[u][i]=ancestor[ancestor[u][i-1]][i-1];
m1[u][i]=max(m1[u][i-1],m1[ancestor[u][i-1]][i-1]);
m2[u][i]=max(m2[u][i-1],m2[ancestor[u][i-1]][i-1]);
if(m1[u][i-1]!=m1[ancestor[u][i-1]][i-1]){
m2[u][i]=max(m2[u][i],min(m1[u][i-1],m1[ancestor[u][i-1]][i-1]));
}
}
for(int i = head[u];i!=0;i=ed[i].next){
long long u,v,w;
u = ed[i].u;
v = ed[i].v;
w = ed[i].w;
if(v==father){
continue;
}
dfs(v,u,w);
}
return;
}
long long lca(long long u,long long v,long long ma){
if(dep[u]<dep[v]){
swap(u,v);
}
long long res = -INF;
for(int i = 20;i>=0;--i){
if(dep[ancestor[u][i]]<dep[v]){
continue;
}
res = max(res,findBig(u,i,ma));
u = ancestor[u][i];
}
if(u==v){
return res;
}
for(int i = 20;i>=0;--i){
if(ancestor[u][i]!=ancestor[v][i]){
res = max(res,findBig(u,i,ma));
res = max(res,findBig(v,i,ma));
u = ancestor[u][i];
v = ancestor[v][i];
}
}
res = max(res,findBig(u,0,ma));
res = max(res,findBig(v,0,ma));
return res;
}
int main(){
scanf("%lld%lld",&N,&M);
for(int i = 1;i<=N;++i){
fa[i] = i;
}
for(int i = 1;i<=M;++i){
scanf("%lld%lld%lld",&pool[i].u,&pool[i].v,&pool[i].w);
}
kruskal();
dfs(1,0,-INF);
long long ans = INF;
for(int i = 1;i<=M;++i){
if(use[i]){
continue;
}
long long u = pool[i].u;
long long v = pool[i].v;
long long w = pool[i].w;
ans = min(ans,mst+w-lca(u,v,w));
}
printf("%lld\n",ans);
return 0;
}
嘤嘤嘤,完结撒花~
啊,从建博客,各种调试,到写完这一篇文章,再加上摸鱼,抬头一看,天已经亮了,搞了一个通宵~
不过看到成果,内心还是十分高兴的~
如有错误,希望巨佬您能够在评论区指正,感激不尽~