吐槽:嚴格次小生成樹我調了將近10個小時……(雖然有3個小時的電競時間html
若是您還不知道最小生成樹的定義是什麼,請空降這裏ios
介於咱們以前已經討論過了最小生成樹的定義和\(Prim\)算法了,此次咱們直奔主題——\(Kruskal\)算法算法
仍是老樣子,咱們拋開正確性不談,只談算法工做方式和代碼實現數組
\(Kruskal\)的思路很是暴躁:把每一個點當作是一個單獨的連通塊,而後把邊按照邊權排序,從小到大一條一條加入最小生成樹,這樣直到有\(n-1\)條邊被加入了最小生成樹數據結構
注意每次加入邊的時候,至少有原來不連通的兩連通塊被聯通,這樣纔可以保證不會造成環框架
爲何不知足上面的條件就會造成環?ui
顯然易見,若是兩個點在加入這條邊以前,已經能夠相互到達了(成爲一個連通塊),那麼這條邊就是多餘的,在鏈接以後會造成重邊或者環
每個連通塊都是一個無根樹,在樹上任意兩點之間加一條邊,必定會造成一個環,因此爲了避免造成環,咱們不加這條邊spa
讓咱們舉一個生動形象的例子(由於電腦畫圖實在不方便,這裏改爲手畫了)
好比像上面這張帶權無向圖:debug
咱們模擬\(Kruskal\),每一個點全都分開,從小到大加入邊
重複這個操做一直從小到大的加邊,直到完成最小生成樹
3d
而後這樣咱們就獲得了這個圖的最小生成樹(不惟一),它的大小爲\(7\)
在算法中,有一個很重要的步驟,就是維護每一條邊連通的兩個點是否是處於同一個連通塊以內,若是處於同一連通塊以內,那麼這條邊是不能被加入最小生成樹的
因此咱們須要一種數據結構來維護各個連通塊的狀況,而並查集就很好的知足了咱們的要求
PS:不知道什麼是並查集?請戳這裏(請從其餘園友那裏搜叭)
維護一個有\(n\)個集合(每一個集合表明一個點)的並查集,初始時每一個集合中只有一個元素(點\(i\))
\(一、\)對於每次加邊以前,查詢兩個端點\(a,b\)是否在同一集合裏,若是不在同一集合裏,執行\(2\),若是在同一集合,那麼說明\(a,b\)已經連通了,跳過這條邊
\(二、\)把這條邊計入最小生成樹,而後合併點\(a,b\)所在的集合
\(Code\)
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> #include<set> #include<map> #include<utility> #include<iostream> #include<list> #include<ctime> #include<cmath> #include<cstdlib> #include<iomanip> typedef long long int ll; inline int read(){ int fh=1,x=0; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); } while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); } return fh*x; } inline int _abs(const int x){ return x>=0?x:-x; } inline int _max(const int x,const int y){ return x>=y?x:y; } inline int _min(const int x,const int y){ return x<=y?x:y; } inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; } inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); } const int maxn=5005; const int inf=1e9; int n,m; struct Edge{ int a,b,w; Edge(){} }edge[200005];//創建一個結構體,存邊的兩個端點和權值 bool operator < (const Edge p,const Edge q){ return p.w<q.w; } //重載運算符,按照權值從小到大排序 int fa[maxn]; int find(const int x){ if(fa[x]==x) return x; return fa[x]=find(fa[x]); }//並查集的查詢操做,查詢x的集合表明 inline int Kruskal(){ int ans=0;//最小生成樹大小 std::sort(edge,edge+m);//排個序 for(int i=1;i<=n;i++)//並查集初始化,每一個點都是獨立的連通塊 fa[i]=i; for(int i=0;i<m;i++){//把全部邊掃一遍 int x=edge[i].a,y=edge[i].b; if(find(x)==find(y)) continue;//若是屬於一個集合,就跳出 fa[find(x)]=y;//合併兩個集合 ans+=edge[i].w;//統計最小生成樹大小 } return ans; } int main(){ n=read(),m=read(); for(int i=0,x,y,z;i<m;i++){ x=read(),y=read(),z=read(); if(x==y) continue; edge[i].a=x; edge[i].b=y; edge[i].w=z;//建圖 } printf("%d\n",Kruskal());//跑最小生成樹 return 0; }
傳送門:【模板】嚴格次小生成樹
字面意思來說,次小生成樹就是除了最小生成樹以外最小的那棵生成樹
那麼爲何把「嚴格」加了個括號呢?由於它並不嚴格(廢話)
若是最小生成樹選擇的邊集是\(E_M\),嚴格次小生成樹選擇的邊集是\(E_S\),那麼須要知足:\((value(e)\)表示邊\(e\)的權值\()\)
\(\sum_{e \in E_M}value(e)<\sum_{e \in E_S}value(e)\)
顯然這個公式它比較噁心,簡單來講,就是次小生成樹的大小必須嚴格小於最小生成樹的大小
若是咱們只是在最小生成樹上更換了邊,可是總的生成樹大小同樣,那麼求出的就是不嚴格的次小生成樹(由於一張圖的最小生成樹能夠有多個,此時求出了另外一個最小生成樹)
看到「嚴格次小生成樹」,你的第一反應應該是「這個東西會和最小生成樹有關!」
所謂「次小」就是除了最小以外的最小的,那麼咱們先求出最小的,起碼不會對咱們的求解起阻礙做用吧
假設你已經順手使用\(Prim\)或者\(Kruskal\)算法求出了這張圖的最小生成樹
如今,咱們能夠把這個圖上的全部邊簡單的分紅兩類了——第一類是在最小生成樹上的邊(簡稱樹邊),第二類是不在最小生成樹上的邊(簡稱非樹邊)
仍是一開始的那個圖,最小生成樹上的邊使用藍筆標出,咱們已知這個最小生成樹的大小\(size=7\)
能夠貪心的轉化問題——求第二小,也就是求一個新的生成樹大小\(size'\)在比\(size\)小的前提下最大
由於嚴格次小生成樹和最小生成樹必定不是同一棵樹,那麼咱們能夠考慮用一些非樹邊,替換掉同等數量的樹邊
想到替換時,也許你會遇到這些問題,(如下用三元組\((a,b,c)\)表示連通\(a、b\)兩點的邊,權值爲\(c\))
1. 替換時,怎麼保證求出的是\(2\)小而不是\(3\)小、\(4\)小、\(n\)小呢?
根據上面的貪心思路:替換後使得\(size'-size\)最小,那麼\(size'\)就是嚴格次小生成樹
2. 一條非樹邊能夠替換掉哪些樹邊?
顯然不能夠胡亂替換,由於須要保證求出的次小生成樹還有個樹的模樣(不能出現環、必須連通)
想到利用樹的性質:當咱們要向一棵樹中加入一條邊時,會造成且僅造成一個環,而且這個環包含\(a\rightarrow LCA(a,b)\)和\(b\rightarrow LCA(a,b)\)路徑中的全部邊
咱們先把一條非樹邊\((a,b,c)\)加進去,在\(a\rightarrow LCA(a,b)\)和\(b\rightarrow LCA(a,b)\)路徑中的邊中刪除一條便可保證樹的性質
上面的解釋理解不能?請看下面的圖,幫助理解:
如圖,黑色的是樹邊,如今要把一條綠色的非樹邊\((8,6,n)\)加進去,那麼圖中標紅的樹邊就能夠被替換
書面證實
對於一棵樹\(T\)其中任意兩個節點\(a,b\)都存在且僅存在一條簡單路徑:\(a\rightarrow LCA(a,b)\rightarrow b\)
如今鏈接\((a,b)\),那麼上式能夠寫成這樣:\(a\rightarrow LCA(a,b)\rightarrow b\rightarrow a\),顯然構成一個環
PS:另外,由於鏈接\((a,b)\)以前路徑只有一條,因此鏈接後,造成的環也只有一個
證畢
3. 用幾條非樹邊替換幾條樹邊呢?
在解決這個問題以前,先給出另外一個結論:
對於任意一條非樹邊\((a,b,c)\),在把它加入最小生成樹後造成的環中,每一條樹邊的權值\(w\leq c\)
解決這個問題,須要用到這個結論,因此咱們先來證實一下它
證實
設加入的非樹邊是\((a,b,c)\),產生環包含樹邊的集合是\(T\),\(w_e\)表示\(e\)的邊權,最小生成樹的大小爲\(size\)
反證法:
設\(p:\exists e\in T\)且有\(w_e>c\)
假設\(p\)爲真,那麼此時斷掉樹邊\(e\),加入非樹邊\((a,b,c)\)會使得\(size\)減少
可是\(size\)是咱們所求出的最小生成樹的大小,它不可能更小了,因此\(p\)爲假
那麼\(!p:\forall e\in T w_e\leq c\)必定爲真
證畢
有了上面的結論還不夠,要想繼續解決這個問題,咱們還得大膽猜測,當心求證
作出假設:替換\(1\)條邊最優
證實
還記得咱們的貪心思路嗎:替換後使得\(size'-size\)最小,那麼\(size'\)就是嚴格次小生成樹
咱們假設用一條非樹邊\(e_1\)去替換一條樹邊,運用上面的結論,咱們知道全部與\(e_1\)構成環的邊的權值\(w\geq w_{e_1}\)
若是咱們作此次替換,\(size'\)的大小就會比\(size\)大\(k=w-w_{e_1}\)
考慮特殊狀況,\(k=0\)時,表明着咱們把兩條權值相等的樹邊和非樹邊作了替換
可是這麼作對\(size'\)沒有任何影響,只對次小生成樹的形態有影響,但它長得好很差看咱們並不關心,咱們只關心\(size'\)而已
由於\(k=0\)時,對\(size'\)沒有影響,排除這種狀況,如今\(k\)的範圍\((k>0)\)
(注意咱們之後都再也不考慮\(k=0\)的狀況了!!!)
顯然咱們選擇\(n\)條邊,就會產生多個\(k\)值,此時\(size'\)必定大於只選\(1\)條邊的\(size'\)
根據貪心思路,要求\(size'\)最小,顯然選\(1\)條邊作替換最優
證畢
問題解決,選擇\(1\)條邊作替換最優(也就是最小生成樹和次小生成樹中權值不相同的邊有且只有\(1\)條)
4. 用哪條非樹邊替換哪條樹邊?答案怎麼更新?
這兩個咱們逃不掉得枚舉其中一個了,這裏顯然枚舉非樹邊比較方便(設樹邊邊權爲\(w\))
由於咱們要求\(size'\)最大的,因此要求\((c-w>0)\)且\((c-w)\)儘可能小,由於\(c\)必定,只要使得\(w\)儘可能大便可(注意這裏根據上面的結論,\(c\geq w\)因此不用擔憂\(c-w<0\)的狀況)
枚舉每個非樹邊\((a,b,c)\),找到路徑\(a\rightarrow LCA(a,b)\)和\(b\rightarrow LCA(a,b)\)中\(w\)最大的那條邊,作替換便可,答案就是\(size'=min(size',size+c-w)\)
注意一下小細節:由於\(w\)有可能等於\(c\),若是作替換的話,求出的並非嚴格次小生成樹,因此還須要記錄一個次大值\(w'\),若是\(w=c\),那麼答案\(size'=min(size',size+c-w')\)
囉囉嗦嗦這麼多,按照上面的思路,就能夠求嚴格次小生成樹了
明確了思路,有沒有感到幹勁十足呢?(必須得幹勁十足啊,筆者但是\(1\)小時寫完,\(7\)小時\(debug\)的男人)
這裏梳理一下咱們要用的算法和數據結構吧:
1. 求最小生成樹:並查集,\(Kruskal\),無腦存邊
2. 最近公共祖先\(LCA\):樹上倍增,\(dfs\)預處理,鄰接表
3. 區間最大/次大\(RMQ\):樹上\(st\)表
4. 一句提醒:不開\(long\) \(long\)見祖宗
因此獲得公式:基礎算法+數據結構+簡單證實=毒瘤題(這題其實出的挺好的,既沒超綱,又有必定難度)
大致框架有了,剩下的都是代碼實現的細節了,就都在代碼註釋裏講解吧
#include<cstdio> #include<cstring> #include<queue> #include<stack> #include<algorithm> #include<set> #include<map> #include<utility> #include<iostream> #include<list> #include<ctime> #include<cmath> #include<cstdlib> #include<iomanip> typedef long long int ll; inline int read(){ int fh=1,x=0; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); } while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); } return fh*x; } inline int _abs(const int x){ return x>=0?x:-x; } inline ll _max(const ll x,const ll y){ return x>=y?x:y; } inline ll _min(const ll x,const ll y){ return x<=y?x:y; } inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; } inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); } const int maxn=100010; const ll inf=1e18;//賦個極大值 int n,m; ll MST,ans=inf;//ans是次小生成樹,MST是最小生成樹 struct Node{ int to;//到達點 ll cost;//邊權 }; std::vector<Node>v[maxn];//鄰接表存最小生成樹 int dep[maxn];//dep[i]表示第i個點的深度 bool vis[maxn]; ll f[maxn][25];//f[i][j]存節點i的2^j級祖先是誰 ll g1[maxn][25];//g1[i][j]存節點i到他的2^j級祖先路徑上最大值 ll g2[maxn][25];//g2[i][j]存節點i到他的2^j級祖先路徑上次大值 void dfs(const int x){//由於是樹上st和樹上倍增,因此能夠一塊兒預處理 vis[x]=true;//x號點訪問過了 for(int i=0;i<v[x].size();i++){//掃全部出邊 int y=v[x][i].to; if(vis[y]) continue;//兒子不能被訪問過 dep[y]=dep[x]+1;//兒子的深度是父親+1 f[y][0]=x;//兒子y的2^0級祖先是父親x g1[y][0]=v[x][i].cost;//y到他的2^0級祖先的最大邊長 g2[y][0]=-inf;//y到他的2^0級祖先的次大邊長(沒有次大邊長,故爲-inf) dfs(y);//遞歸預處理 } } inline void prework(){//暴力預處理 for(int i=1;i<=20;i++)//枚舉2^1-2^20 for(int j=1;j<=n;j++){//枚舉每一個點 f[j][i]=f[f[j][i-1]][i-1];//正常的倍增更新 g1[j][i]=_max(g1[j][i-1],g1[f[j][i-1]][i-1]); g2[j][i]=_max(g2[j][i-1],g2[f[j][i-1]][i-1]); //如下是求次大的精華了 if(g1[j][i-1]>g1[f[j][i-1]][i-1]) g2[j][i]=_max(g2[j][i],g1[f[j][i-1]][i-1]); //j的2^i次大值,是j的2^(i-1)和j^2(i-1)的2^(i-1)最大值中的較小的那一個 //特別的,若是這兩個相等,那麼沒有次大值,不更新g2數組 else if(g1[j][i-1]<g1[f[j][i-1]][i-1]) g2[j][i]=_max(g2[j][i],g1[j][i-1]); } } inline void LCA(int x,int y,const ll w){ //非樹邊鏈接x,y權值爲w //求LCA時候直接更新答案 ll zui=-inf,ci=-inf;//zui表示最大值,ci表示次大值 if(dep[x]>dep[y]) std::swap(x,y);//保證y比x深 for(int i=20;i>=0;i--)//倍增向上處理y if(dep[f[y][i]]>=dep[x]){ zui=_max(zui,g1[y][i]);//更新路徑最大值 ci=_max(ci,g2[y][i]);//更新路徑次大值 y=f[y][i]; } if(x==y){ if(zui!=w) ans=_min(ans,MST-zui+w);//若是最大值和w不等,用最大值更新 else if(ci!=w&&ci>0) ans=_min(ans,MST-ci+w);//有毒瘤狀況,沒有次大值,此時也不能用次大值更新 return; } for(int i=20;i>=0;i--) if(f[x][i]!=f[y][i]){ zui=_max(zui,_max(g1[x][i],g1[y][i])); ci=_max(ci,_max(g2[x][i],g2[y][i])); x=f[x][i]; y=f[y][i]; }//依舊是普通的更新最大、次大值 zui=_max(zui,_max(g1[x][0],g1[y][0]));//更新最後一步的最大值 //注意下面這兩句又凝結了人類智慧精華 //由於次大值有可能出如今最後一步上,因此在更新答案前還要更新一下ci //若是最後兩邊的某一邊是最大值,ci就只能對另外一邊取max if(g1[x][0]!=zui) ci=_max(ci,g1[x][0]); if(g2[y][0]!=zui) ci=_max(ci,g2[y][0]); if(zui!=w) ans=_min(ans,MST-zui+w); else if(ci!=w&&ci>0) ans=_min(ans,MST-ci+w);//依舊特判毒瘤狀況 } struct Edge{ int from,to;//鏈接from和to兩個點 ll cost;//邊權 bool is_tree;//記錄是否是樹邊 }edge[maxn*3]; bool operator < (const Edge x,const Edge y){ return x.cost<y.cost; } //重載運算符,按照邊權從大到小排序 int fa[maxn];//並查集數組 inline int find(const int x){ if(fa[x]==x) return x; else return fa[x]=find(fa[x]); }//查詢包含x的集合的表明元素 inline void Kruskal(){ std::sort(edge,edge+m);//先排序 for(int i=1;i<=n;i++)//初始化並查集 fa[i]=i; for(int i=0;i<m;i++){ int x=edge[i].from; int y=edge[i].to; ll z=edge[i].cost; int a=find(x),b=find(y); if(a==b) continue;//若是x和y已經連通,continue掉 fa[find(x)]=y;//合併x,y所在集合 MST+=z;//求最小生成樹 edge[i].is_tree=true;//標記爲樹邊 v[x].push_back((Node){y,z});//鄰接表記錄下樹邊 v[y].push_back((Node){x,z}); } } int main(){ n=read(),m=read(); for(int i=0,x,y;i<m;i++){ ll z; x=read(),y=read();scanf("%lld",&z); if(x==y) continue; edge[i].from=x; edge[i].to=y; edge[i].cost=z; }//讀入整個圖 Kruskal();//初始化最小生成樹 dep[1]=1;//設1號點是根節點,把它變成有根樹 dfs(1);//從1開始預處理 prework();//倍增預處理 for(int i=0;i<m;i++)//枚舉全部邊 if(!edge[i].is_tree)//若是是非樹邊,那麼更新答案 LCA(edge[i].from,edge[i].to,edge[i].cost); printf("%lld\n",ans);//輸出答案,不開long long見祖宗 return 0;//我諤諤終於結束 }
嚴格的都說了,那就再稍微說一兩句不嚴格次小生成樹的求法
剛纔那麼多的證實,就是爲了讓這個不嚴格變成嚴格,如今咱們倒過來看,這個問題就變得小兒科了
總體框架不用變,只是咱們不須要處理次大值了,每次更新的時候直接\(ans=min(ans,ans+c-w);\)便可