1. 程式人生 > >網路流 最大流—最小割 之SAP演算法 詳解

網路流 最大流—最小割 之SAP演算法 詳解

首先引入幾個新名詞:

1、距離標號:

所謂距離標號 ,就是某個點到匯點的最少的弧的數量(即邊權值為1時某個點到匯點的最短路徑長度)。

設點i的標號為level[i],那麼如果將滿足level[i]=level[j]+1的弧(i,j)叫做允許弧 ,且增廣時只走允許弧

2、斷層(本演算法的Gap優化思想):

gap[i]陣列表示距離標號為i的點有多少個,如果到某一點沒有符合距離標號的允許弧,那麼需要修改距離標號來找到增廣路; 如果重標號使得gap陣列中原標號數目變為0,則演算法結束。

SAP演算法框架:

1、初始化;

2、不斷沿著可行弧找增廣路。可行弧的定義為{( i , j ) , level[i]==level[j]+1};

3、當前節點遍歷完以後,為了保證下次再來的時候有路可走,重新標號當前距離,level[i]=min(level[j]+1)

該演算法最重要的就是gap常數優化了。

題目大意:

      就是由於下大雨的時候約翰的農場就會被雨水給淹沒,無奈下約翰不得不修建水溝,而且是網路水溝,並且聰明的約翰還控制了水的流速,本題就是讓你求出最大流速,無疑要運用到求最大流了。題中N為水溝數,M為水溝的頂點,接下來Si,Ei,Ci分別是水溝的起點,終點以及其容量。求源點1到終點M的最大流速。注意重邊

題大意:給出邊數N,點數M,每條邊都是單向的,問從1點到M的最大流是多少。

EK演算法模板

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>

using namespace std;

int u,v,dist,a[205][205],flow[205],pre[205],q[205],n,m;


int bfs(int s,int des)
{
    memset(q,0,sizeof(q));
    memset(pre,0,sizeof(pre));
    int h=0,t=0;
    q[h++]=s; flow[s]=0x7fffffff;
    pre[s]=s; flow[des]=0;
    while (h!=t)
    {
        int u=q[t];
        if (u==des) break;
        for (int v=1;v<=m;v++) if (a[u][v]>0 && !pre[v])
        {
            q[h++]=v; pre[v]=u;
            flow[v]=min(flow[u],a[u][v]);
        }
        t++;
    }
    return flow[des];
}

int maxflow(int s,int des)
{
    int t,ans=0;
    while(t=bfs(s,des),t!=0)
    {
        for (int i=des;i!=s;i=pre[i])
        {
            a[pre[i]][i]-=t;
            a[i][pre[i]]+=t;
        }
        ans+=t;
    }
    return ans;
}

int main()
{
    while (scanf("%d%d",&n,&m)!=EOF)
    {
        memset(a,0,sizeof(a));
        for (int i=1;i<=n;i++)
        {
            scanf("%d%d%d",&u,&v,&dist);
            a[u][v]+=dist;
        }
        int ans=maxflow(1,m);
        printf("%d\n",ans);
    }
    return 0;
}


SAP演算法分析

EK的,最經典,也是最白痴的演算法……(不過有些時候正經很好用呢~)

昨天研究了一下神奇的SAP演算法,今天又拿他做了一些網路流的題目,發現確實優化效果極其明顯!

簡單總結一下:

首先我麼先回顧一下EK(這個不會的可以看namiheike寫的EK的詳解,地址:http://www.oibh.org/bbs/thread-29333-1-1.html)。

EK的思想就是每一次都用一個BFS來找到一條增廣路,所以說我們就會發現他的複雜度是:O(V*E^2)。所以說我們找到的不一定就是最優的。

 EK詳細講解及優化分析

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

如果能讓每次尋找增廣路時的時間複雜度降下來,那麼就能提高演算法效率了,使用距離標號的最短增廣路演算法就是這樣的。所謂距離標號,就是某個點到匯點的最少的弧的數量(另外一種距離標號是從源點到該點的最少的弧的數量,本質上沒什麼區別)。

設點i的標號為D[i],那麼如果將滿足D[i]=D[j]+1的弧(i,j)叫做允許弧,且增廣時只走允許弧,那麼就可以達到“怎麼走都是最短路”的效果。每個點的初始標號可以在一開始用一次從匯點沿所有反向邊的BFS求出,實踐中可以初始設全部點的距離標號為0,問題就是如何在增廣過程中維護這個距離標號。

維護距離標號的方法是這樣的:當找增廣路過程中發現某點出發沒有允許弧時,將這個點的距離標號設為由它出發的所有弧的終點的距離標號的最小值加一。

這種維護距離標號的方法的正確性我就不證了。由於距離標號的存在,由於“怎麼走都是最短路”,所以就可以採用DFS找增廣路,用一個棧儲存當前路徑的弧即可。當某個點的距離標號被改變時,棧中指向它的那條弧肯定已經不是允許弧了,所以就讓它出棧,並繼續用棧頂的弧的端點增廣。

為了使每次找增廣路的時間變成均攤O(V),還有一個重要的優化是對於每個點儲存“當前弧”:初始時當前弧是鄰接表的第一條弧;在鄰接表中查詢時從當前弧開始查詢,找到了一條允許弧,就把這條弧設為當前弧;改變距離標號時,把當前弧重新設為鄰接表的第一條弧,還有一種在常數上有所優化的寫法是改變距離標號時把當前弧設為那條提供了最小標號的弧。當前弧的寫法之所以正確就在於任何時候我們都能保證在鄰接表中當前弧的前面肯定不存在允許弧。

優化2:

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

優化小結:

1.鄰接表優化:

如果頂點多的話,往往N^2存不下,這時候就要存邊:

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

2.GAP優化:

如果一次重標號時,出現距離斷層,則可以證明ST無可行流,此時則可以直接退出演算法。

3.當前弧優化:

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

學過之後又看了演算法速度的比較,發現如果寫好的話SAP的速度不會輸給HLPP。

SAP實現程式碼如下:


#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 222
#define inf 100000000+1000
int map[MAXN][MAXN];//存圖
int pre[MAXN];//記錄當前點的前驅
int level[MAXN];//記錄距離標號
int gap[MAXN];//gap常數優化
int NV,NE;

//入口引數vs源點,vt匯點
int SAP(int vs,int vt)
{
	memset(pre,-1,sizeof(pre));
	memset(level,0,sizeof(level));
	memset(gap,0,sizeof(gap));
	gap[0]=vt;
	int v,u=pre[vs]=vs,maxflow=0,aug=inf;
	
	while(level[vs]<vt)
	{
		//尋找可行弧
		for(v=1;v<=vt;v++)
		{
		    if(map[u][v]>0&&level[u]==level[v]+1){
		         break;
		    }
		}
		if(v<=vt)
		{
		   pre[v]=u;
	       u=v;
		   if(v==vt)
		   {
		   	     int neck=0;
		         aug=inf;
		         //尋找當前找到的一條路徑上的最大流 , (瓶頸邊)
		         for(int i=v;i!=vs;i=pre[i])
				 {
		             if(aug>map[pre[i]][i])
					 {
					 	aug=map[pre[i]][i];
						neck=i;
					 }
		         }
		         maxflow+=aug;
		         //更新殘留網路
		         for(int i=v;i!=vs;i=pre[i]){
		             map[pre[i]][i]-=aug;
		             map[i][pre[i]]+=aug;
		        }
			    u=vs;		  //從源點開始繼續搜
		//		u=neck; 	  // Dnic 多路增廣優化,下次增廣時,從瓶頸邊(後面)開始
		    }
		}
		else
		{
		    //找不到可行弧
		    int minlevel=vt;
		    //尋找與當前點相連線的點中最小的距離標號
		    for(v=1;v<=vt;v++){
		         if(map[u][v]>0&&minlevel>level[v]){
		             minlevel=level[v];
		         }
		    }
		    gap[level[u]]--;//(更新gap陣列)當前標號的數目減1;
		    if(gap[level[u]]==0)break;//出現斷層
		    level[u]=minlevel+1;
		    gap[level[u]]++;
		    u=pre[u];
		}
	}
	return maxflow;
}

int main()
{
	int n,m,u,v,cap;
	while(~scanf("%d%d",&m,&n))
	{
		memset(map,0,sizeof(map));
		for(int i=1;i<=m;i++)
		{
		    scanf("%d%d%d",&u,&v,&cap);
		    map[u][v]+=cap;
		}
			printf("%d\n",SAP(1,n));
	}
	return 0;
}