最小樹形圖
最小樹形圖
定義對於帶權有向圖\(G=(V,E)\)對於根\(root\)最小樹形圖為以\(root\)為根的外向樹最小邊權和
有根樹的樹形圖:朱劉演算法
題目給定了\(root\)
樸素版朱劉演算法
核心:
推論1:對於有向圖上的一個點,對於它的所有入邊加減一個權值,答案的樹形圖形態不變
因為所有非根點必然有一條入邊,所以可以對於每個點,取入邊邊權最小值減去,把減去的部分加入答案
經過這樣的操作使得每個點都有一條為0的入邊
\[\ \]
推論2:對於有向圖上邊權為0的有向邊生成的環,可以把環上的點收縮
因為無論最後得到的樹形圖如何連邊,一定可以通過斷掉環上的一條邊來生成一個可行的樹形圖
演算法流程
1.為每個點的入邊更改邊權
2.收縮0環
2.1 存在環 : 回到1
2.2 不存在環:結束演算法
此時存在兩種情況
1.圖不連通,無解
2.圖聯通,每個點一定存在一條為0的入邊,取出一個合法邊集,
然後依次展開每個被收縮的0環,即可得到一個最小樹形圖方案
複雜度分析:
每次收縮環需要依次遍歷,每次至少縮小一個點,因此複雜度上限為\(O(nm)\)
\[\ \]
Tips:
1.注意不要更改根的入邊
2.0邊構成的的圖不連通
樸素的實現:tarjan強連通縮點
判斷無解可以在每次調整邊權時看是否有非根節點不存在入邊,此時無解
const int N=10010,INF=1e9; int n,m,rt,ans; int U[N],V[N],W[N]; vector <int> G[N],E[N]; int id[N],T[N]; void Reweight(){ rep(i,1,n) if(i!=rt && id[i]==i) { int w=INF; for(int j:E[i]) cmin(w,W[j]); if(w==INF) puts("-1"),exit(0); for(int j:E[i]) W[j]-=w; ans+=w; } } int t[N],low[N],ins[N],stk[N],top,dfn,flag; void Union(int u){ low[u]=t[u]=++dfn; ins[stk[++top]=u]=1; for(int i:G[u]) if(W[i]==0) { int v=V[i]; if(!t[v]) Union(v),cmin(low[u],low[v]); else if(ins[v]) cmin(low[u],t[v]); } if(low[u]==t[u]) { int v; do { ins[v=stk[top--]]=0; id[v]=u; if(u!=v) flag=1; } while(u!=v); } } void ReBuild(){ rep(i,1,n) G[i].clear(),E[i].clear(); rt=id[rt]; rep(i,1,m) { U[i]=id[U[i]],V[i]=id[V[i]]; if(U[i]==V[i]) continue; G[U[i]].pb(i),E[V[i]].pb(i); } } int main(){ n=rd(),m=rd(),rt=rd(); rep(i,1,n) id[i]=i; rep(i,1,m) { U[i]=rd(),V[i]=rd(),W[i]=rd(); G[U[i]].pb(i),E[V[i]].pb(i); } while(1) { Reweight(); rep(i,1,n) t[i]=0; dfn=flag=0; rep(i,1,n) if(!t[i] && id[i]==i) Union(i); if(!flag) break; ReBuild(); } Reweight(); printf("%d\n",ans); }
更好的實現:只記錄一條0邊,每次迴圈收縮環(居然細節挺多的。。)
const int N=10010,INF=1e9; int n,m,rt,ans; int U[N],V[N],W[N]; int id[N],inw[N],pre[N]; void Reweight(){ rep(i,1,n) inw[i]=INF,pre[i]=0; rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) if(inw[V[i]]>W[i]) inw[V[i]]=W[i],pre[V[i]]=U[i]; rep(i,1,n) if(i!=rt && id[i]==i) { if(inw[i]==INF) puts("-1"),exit(0); ans+=inw[i]; } rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) W[i]-=inw[V[i]]; } int vis[N]; int Union(){ int fl=0; rep(i,1,n) vis[i]=0; rep(i,1,n) if(id[i]==i && !vis[i]) { int u=i; while(u && !vis[u]) vis[u]=i,u=pre[u]; if(vis[u]==i) { int v=pre[u]; fl=1; while(v!=u) id[v]=u,v=pre[v]; } } return fl; } int main(){ n=rd(),m=rd(),rt=rd(); rep(i,1,n) id[i]=i; rep(i,1,m) U[i]=rd(),V[i]=rd(),W[i]=rd(); while(1) { Reweight(); if(!Union()) break; rep(i,1,m) U[i]=id[U[i]],V[i]=id[V[i]]; rt=id[rt]; } printf("%d\n",ans); }
用可並堆優化朱劉演算法
筆者沒有寫過這個東西的程式碼
涉及到的操作:
1.用可並堆維護合併點集入邊的最小權值,並且支援全域性減操作,單點刪除操作(刪除塊內邊)
2.用並查集維護判斷是否出現了環,由於不能直接合並,需要用按秩合併來實現
比較常見的實現是左偏樹,因為便於全域性修改的標記下傳操作,程式碼也比較好寫
用可並堆維護朱劉演算法的操作,單次合併操作為\(O(\log m)\),因此複雜度為\(O(n(\log m+\log n)+n\log n)\)
無根樹的樹形圖
建立超級原點\(S\)向\(V\)中的點連邊權極大的邊,以限制每次只選一條這樣的邊
單次得到答案後減去這個極大值即可,注意如果答案有兩個這樣的極大值,是無解的