1. 程式人生 > 實用技巧 >朱劉演算法學習筆記

朱劉演算法學習筆記

提出問題

首先給出樹形圖的定義:(可以近似理解為有向圖上的生成樹)(定義取自訓練指南)

  • 有向圖中定義
  • 無環
  • 根節點可以到達任意一個節點
  • 根節點入度為 0 ,其他節點入度為 1

然後是最小樹形圖:

  • 邊權和最小的樹形圖。

分析問題

演算法簡介

這個演算法名叫 朱-劉演算法,根據網上說法是 朱永津-劉振巨集 發明的,

1965年,提出最小樹形圖演算法,運用圖的收縮與擴張的運算,繪出了在一個有向圖中求最小樹形圖的一個多項式演算法,在擬陣交計算上為首創,被稱為“朱-劉演算法”。

流程

Warning: 以下說明的是 樹有根 的情況。

首先放一張 luogu題解 的圖:(第二張看上去並不友好,於是拿了第一張)

  • 首先,每個點的出度可能會有多個,不好考慮,所以按照入度為 \(1\) (非根)這個性質來思考。
  • 容易想到,每次對除根外每個點找出權值最小的入邊並累計入答案中。
  • 判斷選出的邊是否存在環,如果沒有就說明找到了最小樹形圖,退出。
  • 將所有環縮點,構造一個新圖,對於原圖的每條邊:如果這條邊在環內,刪去;否則,如果該邊的終點(指向節點)在環內,將權值修改為(這條邊原先的權值-終點在環上的入邊權值)
  • 重複這個步驟,直到滿足無環為止。

正確性證明

  • 其實朱劉演算法本質是一個反悔貪心
  • 對於每個環,顯然一定存在一個最優解,只去掉一條邊(如果選了兩條,把其中一條選回去,答案不會變差)
  • 如果你選了新的權值(就是作差過的),相當於去掉環上對應的入邊,然後改選了當前這一條。程式裡不需要判終點是否在環內,直接把不在環上的點當做一個環處理即可。因為這樣修改邊權,減去的權值就是原來的邊權,就和“選這一條邊”的意義是一樣的了。
  • 每次縮點點數至少會減一,複雜度 \(O(VE)\)

拓展——不定根

這個其實和 多源最短路 之類的解決方法是類似的,考慮對每個點都連到一個虛根 \(rt\)\(n\) 條邊均由 \(rt\) 指向其他點,並且把邊權設定為 (原來的所有邊權和 \(sum\) +1) 。然後就可以跑有根的朱劉了。

如果最後跑出來,權值和 \(>2\times sum\) 說明用了兩條新的邊,但是原圖的樹形圖裡面顯然不可能存在兩個根節點,所以原圖是無法形成最小樹形圖的。

否則就可以根據唯一的一條新加邊指向的點確定樹形圖的根節點,因為它除了 \(rt\) 以外,沒有被原圖中任何其他節點指向。

解決問題

程式碼來源:

P4716 【模板】最小樹形圖

如果你需要通過程式碼更好地理解演算法,那麼這裡提供:

程式碼變數名稱約定
 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;
}

習題

UVA11865 Stream My Contest

題意:你需要花費不超過 \(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 );
    }
}

參考

墨染空大佬的部落格