1. 程式人生 > 實用技巧 >最小樹形圖

最小樹形圖

最小樹形圖

定義對於帶權有向圖\(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\)中的點連邊權極大的邊,以限制每次只選一條這樣的邊

單次得到答案後減去這個極大值即可,注意如果答案有兩個這樣的極大值,是無解的