1. 程式人生 > >網路流最大流的sap()演算法

網路流最大流的sap()演算法

         現在想將一些物資從S運抵T,必須經過一些中轉站。連線中轉站的是公路,每條公路都有最大運載量。

         每條弧代表一條公路,弧上的數表示該公路的最大運載量。最多能將多少貨物從S運抵T?

        
         這是一個典型的網路流模型。為了解答此題,我們先了解網編流的有關定義和概論。

         若有向圖G=(V,E)滿足下列條件:       

1.      有且僅有一個頂點S,它的入度為零,即d-(S)=0,這個頂點S便稱為源點,或稱為發點。

2.      有且僅有一個頂點T,它的出度為零,即d+(T)=0,這個頂點T便稱為匯點,或稱為收點。

3.      每一條弧都有非負數,叫做這條邊的容量。邊(vi,vj)的容量用cij表示。

則稱之為網路流圖,記為G=(V,E,C)。

可行流

對於網路流圖G,每一條弧(i,j)都給定一個非負數fij,這一組數滿足下列三條件時稱為這網路的可行流,用f表示它。

1.      每一條弧(i,j)都有fij<Cij

2.      流量平衡

除了源點S和匯點T之外的所有點vi,恆有:∑j(fij)=k(fjk),該等式說明中間點vi的流量守恆,輸入與輸出量相等。

3.      對於源點S和匯點T有,∑i(fSi)= j(fjT)=V(f)

可增廣路

         給定一個可行流f={fij}。若fij=Cij,稱<vi,vj>為飽和弧;否則稱<vi,vj>為非飽和弧。若fij=0,稱<vi,vj>為零流弧;否則稱<vi,vj>為非零流弧。

         先定義一條道路P,起點是S,終點是T。把P上所有與P方向一致的弧定義為正向弧,正向弧的全體記為P+;把P上所有與P方向相悖的弧定義為反向孤,反向弧的全體記為P-。

         譬如在圖中,P={S,V1,V2,V4,T},那麼P+={<S,V1>,<V1,V2>,<V2,V3>,<V4,T>},P-={<V4,V3>}

         給定一個可行流f,P是從S到T的一條道路,如果滿足:fij是非飽和流,並且<i,j>∈ P+,fij是非零流,並且<i,j>∈P-,那麼就稱P是f的一條可增廣路。之所以稱作“可增廣”,是因為可改進路上弧的流量通過一定的規則修改,可以令整個流量放大。

剩餘圖

剩餘圖G’=(V,E’)

流量網路=(V,E)中,對於任意一條邊(a,b),若flow(a,b)<capacity(a,b) or flow(b,a)>0,則(a,b)∈E’。(可以沿著aàb方向增廣。

剩餘圖的權值代表能沿邊增廣的大小

剩餘圖中,每條邊都可以沿其方向增廣

剩餘圖中,從源點到匯點的每一條路徑都對應著一條增廣路

割切

G={V,E,C}是已知的網路流圖,設U是V的一個子集,W=V\U,滿足S∈U,T∈W。即U、W把V分成兩個不相交的集合,且源點和匯點分屬不同的集合。

對於弧尾在U,弧頭在W的弧所構成的集合稱之為割切,用(U,W)表示。把割切(U,W)中所有弧的容量之和叫做此割切的容量,記為C(U,W),即:

割切示例

        

         上例中,令U={S,V1},則W={V2,V3,V4,T},那麼,C(U,W)=<S,V2>+<V1,V2>+<V1,V3>+<V1,V4>=8+4+4+1=17

流量演算法的基本理論

定理1:對於已知的網路流圖,設任意一可行流為f,任意一割切為(U,W),必有:V(f)<C(U,W).

定理2:可行流f是最大流的充分必要條件是:f中不存在可改進路。

定理3:整流定理。

         如果網路中所有的弧的容量是整數,則存在整數值的最大值。

定理4:最大流最小割定理

         最大流等於最小割,即maxV(f)=minC(U,W)。

 SAP()

求最大流有一種經典的演算法,就是每次找增廣路時用BFS找,保證找到的增廣路是弧數最少的,也就是所謂的Edmonds-Karp演算法。可以證明的是在使用最短路增廣時增廣過程不會超過V*E次,每次BFS的時間都是;O(E),所以Edmonds-Karp的時間複雜度就是O(V*E^2)。

         如果能讓每次尋找增廣路的時間複雜度降低下來,那麼就能夠提高演算法效率了,使用距離標號的最短增廣路演算法就是這樣的。所謂距離標號,就是某個點到匯點的最小的弧的數量(另外一種距離標號是從源點到該點的最小的弧的數量,本質上沒有什麼區別)。設點i的標號為D[i],那麼如果將滿足D[i]=D[j]+1的弧稱為允許弧,且增廣時只走允許弧,那麼就可以達到“怎麼走老師最短路”的效果。每個點的初始標號可以在一開始用一次從匯點沿所有的反向邊BFS求出,實踐中可以初始設全部點的距離標號為0,問題就是如何在增廣過程中維護這個距離標號。

         維護距離標號的方法是這樣的:當找增廣路過程中發現某點出發沒有允許弧時,將這個點的距離標號設為由它出發的所有弧的終點的距離標號的最小值加一。維護這個距離標號的方法的正確性我就不證了。由於距離標號的存在,由於“怎麼走都是最短路”,所以就可以採用DFS找增廣路,用一個棧儲存當前路徑的弧即可。當某個點的距離標號被改變時,棧中指向它的那條弧肯定還是允許弧了,所以就讓它出棧,並繼續用棧頂的弧的端點增廣。為了使每次找增廣路的時間均攤成O(V),還有一個重要的優化是對於每個點儲存“當前弧”:初始是當前弧是鄰接表的第一條弧;在鄰接表中查詢時從當前弧開始查詢,找到了一條允許弧,就把這條弧設為當前弧;改變距離標號時,把當前弧重新設為鄰接表的第一條弧,還有一種在常數上有所優化的寫法是改變距離標號的時把當前弧設為那條提供了最小標號的弧。當前弧的寫法之所以正確就在於任何時間,我們都能保證在鄰接表中當前弧的前面肯定不存在允許弧。

         還有一種常數優化是在每次找到路徑並增廣完畢之後還要將路徑中的所有的頂點退棧,而是隻將瓶頸邊以及之後的邊退棧,這是借鑑了Dinic演算法的思想。注意任何時間待增廣的“當前點”都應該是棧頂的點的終點。這的確只是一個常數優化,由於當前邊結構的存在,我們肯定可以在O(n)的時候內復原路徑中瓶頸邊之前的所有邊。

優化:

1.      鄰接表優化:如果頂點多,往往n^2存不下,這時候就要存邊:存每條邊的出發點,終點點和價值,然後排序一下,再記錄每個出發點的位置。以後要呼叫從出發點出發的邊時候,只需要從記錄的位置開始找就可以(其實可以用連結串列)。優化是時間快空間節省,缺點是程式設計複雜度將變大,所以在題目允許的情況下,建議使用鄰接矩陣。

2.      GAP優化:如果一次重標號時,出現斷層,則可以證明ST無可行流,此時則可以直接退出演算法

3.      當前弧優化:為了使每次打增廣路的時間變成均攤O(v),還有一個重要的優化是對於每個點儲存“當前弧”:初始時當前弧是鄰接表的第一條弧;在鄰接表中查詢時從當前弧開始查詢,找到了一條弧,就把這條弧設為當前弧;改變距離標號時,把當前弧設為鄰接表的第一條弧。

最近發現自己最大流的版有問題,更新了下。使用的時候,除了新增邊(之前要初始化),還要設定MaxFlow的起點,終點和點的數目。

const int MaxN=2005;
const int MaxE=400005;
#define INF 0x3f3f3f3f
struct Edge
{
    int u,v,flow,cap,pre;
    Edge(){}
    Edge(int u,int v,int cap,int pre) :
        u(u),v(v),cap(cap),pre(pre) {flow=0;}
}edge[MaxE];

int head[MaxN],nEdge;

void addEdge(int u,int v,int cap)
{
    edge[nEdge]=Edge(u,v,cap,head[u]);
    head[u]=nEdge++;
    edge[nEdge]=Edge(v,u,0,head[v]);
    head[v]=nEdge++;
}
void edgeInit() //新增邊之前要先初始化。
{
    nEdge=0;
    memset(head,-1,sizeof(head));
}
struct MaxFlow
{
    int st,ed,n,mx_flow;//表示起點,終點,有多少個點,所求的最大流是多少。
    int dis[MaxN],gap[MaxN],cur[MaxN],aug[MaxN],path[MaxN];
    //dis表示每個點的距離標記,gap表示距離為i的點有多少個,cur用於當前孤優化,
    //aug記錄找到的增廣路流量,path記錄找到的增廣路的路徑。
	MaxFlow(){}
	MaxFlow(int st,int ed,int n) :
		st(st),ed(ed),n(n) { init(); }
    void init()
    {
        for(int i=0;i<MaxN;i++)
        {
            aug[i]=gap[i]=dis[i]=0;
            cur[i]=head[i];
        }
        aug[st]=INF;    gap[0]=n;
        mx_flow=0;
    }
    int augment(int &point)//修改找到的增廣路上的邊的容量,當前點修改為起點。
    {
        for(int i=ed;i!=st;i=edge[path[i]].u)
        {
            int pair=path[i]^1;
            edge[ path[i] ].flow+=aug[ed];
            edge[ pair ].flow-=aug[ed];
        }
        point=st;
        return aug[ed];
    }
    int solve()
    {
        int u=st;
        while(dis[st]<n)
        {
            if(u==ed) mx_flow+=augment(u);

            bool flag=1;
            for(int i=head[u];i!=-1;i=edge[i].pre)
            {
                int v=edge[i].v;
                if(edge[i].cap-edge[i].flow>0&&dis[u]==dis[v]+1)
                {
                    path[v]=i;  cur[u]=i;
                    aug[v]=min(aug[u],edge[i].cap-edge[i].flow);
                    u=v;
                    flag=0; break;
                }
            }
            if(flag)
            {
                if(--gap[dis[u]]==0) return mx_flow;
                dis[u]=MaxN;
                for(int i=head[u];i!=-1;i=edge[i].pre)
                {
                    int v=edge[i].v;
                    if(edge[i].cap-edge[i].flow>0) dis[u]=min(dis[u],dis[v]+1);
                }
                gap[dis[u]]++;
                cur[u]=head[u];
                if(u!=st) u=edge[path[u]].u;
            }
        }
        return mx_flow;
    }
}sap;