朱劉演算法學習筆記
提出問題
首先給出樹形圖的定義:(可以近似理解為有向圖上的生成樹)(定義取自訓練指南)
- 有向圖中定義
- 無環
- 根節點可以到達任意一個節點
- 根節點入度為 0 ,其他節點入度為 1
然後是最小樹形圖:
- 邊權和最小的樹形圖。
分析問題
演算法簡介
這個演算法名叫 朱-劉演算法,根據網上說法是 朱永津-劉振巨集 發明的,
1965年,提出最小樹形圖演算法,運用圖的收縮與擴張的運算,繪出了在一個有向圖中求最小樹形圖的一個多項式演算法,在擬陣交計算上為首創,被稱為“朱-劉演算法”。
流程
Warning: 以下說明的是 樹有根 的情況。
首先放一張 luogu題解 的圖:(第二張看上去並不友好,於是拿了第一張)
- 首先,每個點的出度可能會有多個,不好考慮,所以按照入度為 \(1\) (非根)這個性質來思考。
- 容易想到,每次對除根外每個點找出權值最小的入邊並累計入答案中。
- 判斷選出的邊是否存在環,如果沒有就說明找到了最小樹形圖,退出。
- 將所有環縮點,構造一個新圖,對於原圖的每條邊:如果這條邊在環內,刪去;否則,如果該邊的終點(指向節點)在環內,將權值修改為(這條邊原先的權值-終點在環上的入邊權值)
- 重複這個步驟,直到滿足無環為止。
正確性證明
- 其實朱劉演算法本質是一個反悔貪心
- 對於每個環,顯然一定存在一個最優解,只去掉一條邊(如果選了兩條,把其中一條選回去,答案不會變差)
- 如果你選了新的權值(就是作差過的),相當於去掉環上對應的入邊,然後改選了當前這一條。程式裡不需要判終點是否在環內,直接把不在環上的點當做一個環處理即可。因為這樣修改邊權,減去的權值就是原來的邊權,就和“選這一條邊”的意義是一樣的了。
- 每次縮點點數至少會減一,複雜度 \(O(VE)\)
拓展——不定根
這個其實和 多源最短路 之類的解決方法是類似的,考慮對每個點都連到一個虛根 \(rt\) ,\(n\) 條邊均由 \(rt\) 指向其他點,並且把邊權設定為 (原來的所有邊權和 \(sum\) +1) 。然後就可以跑有根的朱劉了。
如果最後跑出來,權值和 \(>2\times sum\) 說明用了兩條新的邊,但是原圖的樹形圖裡面顯然不可能存在兩個根節點,所以原圖是無法形成最小樹形圖的。
否則就可以根據唯一的一條新加邊指向的點確定樹形圖的根節點,因為它除了 \(rt\) 以外,沒有被原圖中任何其他節點指向。
解決問題
程式碼來源:
如果你需要通過程式碼更好地理解演算法,那麼這裡提供:
程式碼變數名稱約定
n,m,rt:題目給出的點數,邊數,根節點
min_pre[],fa[]:每次執行中找到的最小入邊的權值,入邊的起點
cnt_cyc,incyc_id[]:環的編號計數,每個點在哪個環裡面
f[]:類似並查集中的最高祖先,找一個點沿著入邊往上跳的最終節點
程式碼實現:
//Author: RingweEH
const int N=110,M=1e4+10,inf=0x3f3f3f3f;
struct edge
{
int u,v; ll val;
}e[M];
int n,m,rt,cnt_cyc,fa[N],incyc_id[N],f[N],min_pre[N];
ll ans=0;
int ZhuLiu()
{
while ( 1 )
{
cnt_cyc=0;
for ( int i=1; i<=n; i++ )
incyc_id[i]=f[i]=0,min_pre[i]=inf;
//---------------------init----------------------
for ( int i=1; i<=m; i++ )
if ( e[i].u!=e[i].v && e[i].val<min_pre[e[i].v] )
fa[e[i].v]=e[i].u,min_pre[e[i].v]=e[i].val;
//--------------找每個點的最小入邊---------------
int now=min_pre[rt]=0;
for ( int i=1; i<=n; i++ )
{
if ( min_pre[i]==inf ) return 0; //孤立點特判
ans+=min_pre[i]; //不管如何先把邊權加進去就好了
for ( now=i; now!=rt && f[now]!=i && !incyc_id[now]; now=fa[now] )
f[now]=i; //從i不斷往選定的入邊跳,途中不能往其他已經判定的環裡面跳
if ( now!=rt && !incyc_id[now] )
//看上面迴圈的判斷條件,只滿足了 f[now]==i ,也就是形成了環
{
incyc_id[now]=++cnt_cyc;
for ( int v=fa[now]; v!=now; v=fa[v] )
incyc_id[v]=cnt_cyc;
}
}
if ( !cnt_cyc ) return 1;
//-----------------------找環----------------------
for ( int i=1; i<=n; i++ ) //給不在環中的點也賦一個標號,方便判斷
if ( !incyc_id[i] ) incyc_id[i]=++cnt_cyc;
for ( int i=1; i<=m; i++ )
{
int las=min_pre[e[i].v]; //e[i].v的最小入邊權
e[i].u=incyc_id[e[i].u]; e[i].v=incyc_id[e[i].v]; //縮成同一個點,也就是環編號
if ( e[i].u!=e[i].v ) e[i].val-=las; //如果不在同一個環裡面就修改邊權
}
n=cnt_cyc; rt=incyc_id[rt]; //縮點完成後的點數就是環的個數,並更新根節點編號。
}
}
int main()
{
n=read(); m=read(); rt=read();
for ( int i=1; i<=m; i++ )
e[i]=(edge){read(),read(),read()};
if ( ZhuLiu() ) printf( "%lld",ans );
else printf( "-1\n" );
return 0;
}
習題
題意:你需要花費不超過 \(cost\) 元來搭建一個比賽網路。網路中有 \(n\) 臺機器,編號 \(0\sim n-1\) ,0 為服務機,其他均為客戶機。一共有 \(m\) 條可以使用的網線,資料只能從 \(u_i\to v_i\) 單向傳遞,頻寬 \(b_i\) Kbps,費用 \(c_i\) 元。每臺客戶機應當恰好從一臺機器接受資料,伺服器不接受資料。最大化最小頻寬。
思路:如果要最大化最小頻寬,很容易想到二分最小頻寬並去掉所有小於頻寬的邊。而讓所有客戶機都能收到,其實就是服務機要能到達每個客戶機,要是對性質熟悉的話就很容易想到樹形圖。那麼對於二分的判定,只需要求出從 0 出發的最小樹形圖,判斷權值和是否超過給定 \(cost\) 即可。
//Author: RingweEH
int ZhuLiu()
{
ans=0;
while ( 1 )
{
cnt_cyc=0;
for ( int i=1; i<=n; i++ )
incyc_id[i]=f[i]=fa[i]=0,min_pre[i]=inf;
for ( int i=1; i<=newm; i++ )
if ( e[i].u!=e[i].v && e[i].val<min_pre[e[i].v] )
fa[e[i].v]=e[i].u,min_pre[e[i].v]=e[i].val;
int now=min_pre[rt]=0;
for ( int i=1; i<=n; i++ )
{
if ( min_pre[i]==inf ) return -1;
ans+=min_pre[i];
for ( now=i; now!=rt && f[now]!=i && !incyc_id[now]; now=fa[now] )
f[now]=i;
if ( now!=rt && !incyc_id[now] )
{
incyc_id[now]=++cnt_cyc;
for ( int v=fa[now]; v!=now; v=fa[v] )
incyc_id[v]=cnt_cyc;
}
}
if ( !cnt_cyc ) break;
for ( int i=1; i<=n; i++ )
if ( !incyc_id[i] ) incyc_id[i]=++cnt_cyc;
for ( int i=1; i<=newm; i++ )
{
int las=min_pre[e[i].v];
e[i].u=incyc_id[e[i].u]; e[i].v=incyc_id[e[i].v];
if ( e[i].u!=e[i].v ) e[i].val-=las;
}
n=cnt_cyc; rt=incyc_id[rt];
}
return ans;
}
bool check( int x )
{
rt=1; n=savn; newm=0;
for ( int i=1; i<=m; i++ )
if ( save[i].wide>=x ) e[++newm]=save[i];
int answer=ZhuLiu();
return answer!=-1 && answer<=cost;
}
int main()
{
int T=read(); n=-1;
for ( int cas=1;cas<=T; cas++)
{
n=savn=read(); m=read(); cost=read(); int mxwid=0;
for ( int i=1; i<=m; i++ )
{
e[i].u=read()+1,e[i].v=read()+1; e[i].wide=read(); e[i].val=read();
mxwid=max( mxwid,e[i].wide ); save[i]=e[i];
}
int l=0,r=mxwid,res=-1;
while ( l<=r )
{
int mid=(l+r)>>1;
if ( check(mid) ) l=mid+1,res=mid;
else r=mid-1;
}
if ( res==-1 ) { printf( "streaming not possible.\n" ); continue; }
printf( "%d kbps\n",res );
}
}