1. 程式人生 > >次小生成樹—學習筆記

次小生成樹—學習筆記

次小生成樹

分為非嚴格次小生成樹嚴格次小生成樹

對於前者,若最小生成樹不唯一
則次小生成樹與最小生成樹權值相同

對於後者,則要求次小生成樹權值嚴格大於最小生成樹

接下來的求解方法都將分別討論

這裡是次小生成樹的版題

演算法一:

這一演算法較簡潔易懂,且程式碼量小
但演算法時間複雜度較高,一般不建議用,瞭解思路即可
效率較高的演算法參見演算法二

首先,不難證明次小生成樹的連邊與最小生成樹一定不相同
因此,我們可以列舉每一條在最小生成樹上的邊
在剩下的邊的集合中再求最小生成樹
也就是再對n-1個缺一條邊的圖求最小生成樹

對於嚴格次小生成樹
找出n-1棵樹中找到權值>原最小生成樹且最小的

對非嚴格次小生成樹
找出n-1棵樹中找到權值>=原最小生成樹且最小的

這種思路可以說是很暴力了
程式碼就不貼了,主要講下面的高效演算法

演算法二:

這一演算法程式碼量較大,維護方法不唯一
思路不難,主要在於維護方法的選擇,特別是對於嚴格次小生成樹
演算法時間複雜度是很高效的(也要看維護方法)

對於一棵已經求出的最小生成樹
列舉每一條不在最小生成樹上的邊
並把這條邊加入最小生成樹

設這條邊連線的兩個節點為u和v
這時樹上u到v的路徑會出現迴路
所以我們需要刪掉u到v路徑上的一條邊使其重新變成一顆樹

這時生成樹權值的增量就是新加入的邊權-刪掉的邊權
為了使增量最小,我們要刪去的自然是u到v路徑中權值最大的邊

特別的,若要求的是嚴格次小生成樹
如果發現u到v路徑中權值最大的邊等於加入的邊
就要刪掉u到v路徑上的次大邊權

對於樹上路徑權值的維護選擇有很多
例如樹剖、LCT、倍增等等
這裡用倍增示例

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<queue>
#include<cstring>
using namespace std;
typedef long long lt;

lt read()
{
    lt f=
1,x=0; char ss=getchar(); while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();} while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();} return f*x; } const int maxn=100010; lt n,m,ans; struct node{int u,v;lt dis;}edge[maxn*10]; struct node2{lt v,dis,nxt;}E[maxn*10]; int head[maxn],tot; int fa[maxn]; bool judge[maxn];//判斷是否是最小生成樹的邊 int gra[maxn][18],dep[maxn]; lt fir[maxn][18],sec[maxn][18]; //fir表示區間內最大值,sec表示次大值,和lca一樣的倍增思想 bool cmp(node a,node b){return a.dis<b.dis;} void add(int u,int v,lt dis) { E[++tot].nxt=head[u]; E[tot].v=v; E[tot].dis=dis; head[u]=tot; } int find(int x) { if(x==fa[x]) return x; else return fa[x]=find(fa[x]); } lt kruskal() { int num=0; lt len=0; for(int i=1;i<=n;i++)fa[i]=i; for(int i=1;i<=m;i++) { int u=edge[i].u,v=edge[i].v;lt dis=edge[i].dis; int fu=find(u),fv=find(v); if(fu!=fv) { fa[fu]=fv; judge[i]=true; num++; len+=dis; add(u,v,dis); add(v,u,dis); if(num==n-1) break; } } return len; } void dfs(int u,int pa) { for(int i=head[u];i;i=E[i].nxt) { int v=E[i].v; if(v==pa) continue; dep[v]=dep[u]+1; gra[v][0]=u; fir[v][0]=E[i].dis; sec[v][0]=-1e9; dfs(v,u); } } void work() { for(int i=1;(1<<i)<=n;i++) for(int u=1;u<=n;u++) { gra[u][i]=gra[ gra[u][i-1] ][i-1];//處理祖先 fir[u][i]=max(fir[u][i-1],fir[ gra[u][i-1] ][i-1]); sec[u][i]=max(sec[u][i-1],sec[ gra[u][i-1] ][i-1]); //處理最大和次大值 if(fir[u][i-1]>fir[ gra[u][i-1] ][i-1]) sec[u][i]=max(fir[ gra[u][i-1] ][i-1],sec[u][i]); else if(fir[u][i-1]<fir[ gra[u][i-1] ][i-1]) sec[u][i]=max(fir[u][i-1],sec[u][i]); //注意對次大值的判斷 } } int lca(int u,int v) { if(dep[u]<dep[v]) swap(u,v); int d=dep[u]-dep[v]; for(int i=0;(1<<i)<=d;i++) if((1<<i)&d) u=gra[u][i]; if(u==v) return u; for(int i=(int)log(n);i>=0;i--) { if(gra[u][i]!=gra[v][i]) u=gra[u][i],v=gra[v][i]; } return gra[u][0]; } lt qmax(int u,int v,lt dis) { lt tp=-1e9; for(int i=1;(1<<i)<=n;i++)//倍增思想 { //只要深度比LCA大,就倍增查詢更新 if(dep[ gra[u][i] ]>=dep[v]) { if(dis==fir[u][i]) tp=max(tp,sec[u][i]); else tp=max(tp,fir[u][i]);//發現一樣的邊權要查詢次大值 } } return tp; } int main() { n=read();m=read(); for(int i=1;i<=m;i++) edge[i].u=read(),edge[i].v=read(),edge[i].dis=read(); sort(edge+1,edge+1+m,cmp); ans=kruskal();//先求最小生成樹 dfs(1,-1);//無根樹轉有根樹 work();//預處理gra,fir和sec lt add=1e9; for(int i=1;i<=m;i++) { if(judge[i])continue;//若是最小生成樹的邊就跳過 int u=edge[i].u,v=edge[i].v;lt dis=edge[i].dis; int LCA=lca(u,v); lt maxu=qmax(u,LCA,dis); lt maxv=qmax(v,LCA,dis);//分別查詢u、v到他們LCA路徑上的最大值 add=min(add,dis-max(maxu,maxv));//更新最小增量 } cout<<ans+add; return 0; }