1. 程式人生 > >【圖論】最大流之EK演算法與Dinic演算法及最小費用最大流

【圖論】最大流之EK演算法與Dinic演算法及最小費用最大流

最大流:

給出一張網路圖,並指定源點和終點,每條邊都有它的容量,起點有著無限的流量,求從源點到經過的所有路徑的最終到達匯點的最大流量和。對於同一個節點,流入的流量之和和流出的流量之和相同,即假如結點1有12流量流入結點2,結點2分別有8流量流入結點3,4流量流入結點4,這種情況是可以的。 

EK演算法:

而EK演算法反覆尋找源點s到匯點t之間的增廣路徑,若有,找出增廣路徑上每一段[容量-流量]的最小值delta,若無,則結束。 並且更新殘留網路的值(涉及到反向邊)。所有的正向邊減去delta,所有的反向邊加上delta.由於反向邊存在容量,所以下次尋找增廣路徑可以走該反向邊,這種設計使得我們可以抵消之前的操作,找到更為適合的使總流量增加的邊,使程式有了一個後悔和改正的機會。找到delta後,則使最大流值加上delta,更新為當前的最大流值。

int maxData = 0x7fffffff;
int capacity[arraysize][arraysize]; //記錄殘留網路的容量
int flow[arraysize];                //標記從源點到當前節點實際還剩多少流量可用
int pre[arraysize];                 //標記在這條路徑上當前節點的前驅,同時標記該節點是否在佇列中
int n,m;
queue<int> myqueue;
int BFS(int src,int des)
{
    int i,j;
    while(!myqueue.empty())       //佇列清空
        myqueue.pop();
    for(i=1;i<m+1;++i)pre[i]=-1;
    pre[src]=0;
    flow[src]= maxData;
    myqueue.push(src);
    while(!myqueue.empty())
    {
        int index = myqueue.front();
        myqueue.pop();
        if(index == des)break;//找到了增廣路徑,break掉 
        for(i=1;i<m+1;++i)
        {
            if(i!=src && capacity[index][i]>0 && pre[i]==-1)
            {
                 pre[i] = index; //記錄前驅
                 flow[i] = min(capacity[index][i],flow[index]);   //關鍵:迭代的找到增量
                 myqueue.push(i);
            }
        }
    }
    if(pre[des]==-1)      //殘留圖中不再存在增廣路徑
        return -1;
    else
        return flow[des];
}
int maxFlow(int src,int des)
{
    int increasement= 0;
    int sumflow = 0;
    while((increasement=BFS(src,des))!=-1)
    {
         int k = des;          //利用前驅尋找路徑
         while(k!=src)
         {
              int last = pre[k];
              capacity[last][k] -= increasement; //改變正向邊的容量
              capacity[k][last] += increasement; //改變反向邊的容量
              k = last;
         }
         sumflow += increasement;
    }
    return sumflow;
}
int main()
{
    int i,j;
    int start,end,ci;
    while(cin>>n>>m)
    {
        memset(capacity,0,sizeof(capacity));
        memset(flow,0,sizeof(flow));
        for(i=0;i<n;++i)
        {
            cin>>start>>end>>ci;
            if(start == end)               //考慮起點終點相同的情況
               continue;
            capacity[start][end] +=ci;     //此處注意可能出現多條同一起點終點的情況
        }
        cout<<maxFlow(1,m)<<endl;
    }
    return 0;
}


Dinic演算法:

最核心的內容就是多路增廣。利用對整張圖的分層,即源點為第一層,與源點相連的並且有容量的點為第二層,與第二層相連並且有容量的點為第三層……如果不能到達終點,說明找不到增廣路徑了,此時也就達到了最大流。一次BFS可以增廣好幾次。效率比起EK演算法大大提高。 

Dinic演算法最核心的內容就是多路增廣。利用對整張圖的分層,一次BFS可以增廣好幾次。效率比起EK演算法大大提高。 
*/ 
int tab[250][250];//鄰接矩陣 
int dis[250];//距源點距離,分層圖 
int q[2000],h,r;//BFS佇列 ,首,尾 
int N,M,ANS;//N:點數;M,邊數 
int BFS()
{
     int i,cur;
     memset(dis,-1,sizeof(dis));//以-1填充 
     dis[1]=0;
     head=0;tail=1;
     que[0]=1;//源點入隊 
     while (head<tail)
     {
           cur=que[head++];
           for (i=1;i<=N;i++)
               if (dis[i]<0 && tab[cur][i]>0)
               {
                  dis[i]=dis[cur]+1; 
                  que[tail++]=i;
               }
     }
     if (dis[N]>0)
        return 1;
     else
        return 0;//匯點的DIS小於零,表明BFS不到匯點 
}
//Find代表一次增廣,函式返回本次增廣的流量,返回0表示無法增廣 
int find(int x,int low)//Low是源點到現在最窄的(剩餘流量最小)的邊的剩餘流量
{
    int i,a=0;
    if (x==N)return low;//是匯點 
    for (i=1;i<=N;i++)
    if (tab[x][i] >0 //聯通 
     && dis[i]==dis[x]+1 //是分層圖的下一層 
     &&(a=find(i,min(low,tab[x][i]))))//能到匯點(a <> 0) 
    {
       tab[x][i]-=a;
       tab[i][x]+=a;
       return a;
    }
    return 0;
    
}
int main()
{
    //freopen("ditch.in" ,"r",stdin );
    //freopen("ditch.out","w",stdout);
    int i,j,f,t,flow,tans;
    while (scanf("%d%d",&M,&N)!=EOF){
    memset(tab,0,sizeof(tab));
    for (i=1;i<=M;i++)
    {
        scanf("%d%d%d",&f,&t,&flow);
        tab[f][t]+=flow;
    }
    //
    ANS=0;
    while (BFS())//要不停地建立分層圖,如果BFS不到匯點才結束 
    {
          while(tans=find(1,0x7fffffff))ANS+=tans;//一次BFS要不停地找增廣路,直到找不到為止 
    }
    printf("%d\n",ANS);
    }
    system("pause");
}


最小費用最大流: 
在最大流有多組解時,給每條邊在附上一個單位費用的量,問在滿足最大流時的最小費用是多少?

最小費用最大流只是在殘留網路的基礎上多了個費用網路。在最大流的基礎上,將費用看成路徑長度,求最短路即可。 注意一開始反向邊的費用為正向邊的負數。 

例如POJ2516這道題,有N個供給商,M個僱主,K種物品。每個供給商對每種物品的的供給量已知,每個僱主對每種物品的需求量的已知,從不同的供給商輸送不同的貨物到不同的僱主手上需要不同的花費,又已知從供給商Mj送第kind種貨物的單位數量到僱主Ni手上所需的單位花費。問:供給是否滿足需求?若是滿足,最小運費是多少?
程式碼很好理解,可當做模板。

int map[nMax][nMax];//map[i][j]表示對於每種k物品從i運輸到j所花費的錢
int vis[nMax];//表示i是否用過
int cap[nMax][nMax];//表示i到j的最大通貨量
int dis[nMax];//到i的距離
int que[nMax];//佇列
int pre[nMax];//儲存每一條最短增流路
int num,ans;//num最後的匯點,ans最終的答案
int spfa()//spfa求最短路徑,dijstra不允許有負權,所以這裡使用spfa
{
    int i,k;
    int head,tail;
    memset(vis, 0, sizeof(vis));
    for (i = 0; i <= num; ++ i)
    {
    dis[i] = inf;
    }
    dis[0] = 0;
    vis[0] = 1;
    head = tail = 0;
    que[tail++] = 0;
    while (head < tail)
    {
	    k = que[head];
	    vis[k] = 0;
	    for (i = 0; i <= num; ++ i)
	    {
	    	if (cap[k][i] && dis[i] > dis[k] + map[k][i])//如果k到i還有量,表明還可以增流,那麼就求最短路
	    	{
			    dis[i] = dis[k] + map[k][i];
			    pre[i] = k;
			    if (!vis[i])
			    {
				    vis[i] = 1;
				    que[tail ++] = i;
			    }
	   		}
	    }
	    head ++;
    }
    if (dis[num]<inf)return 1;
    return 0;
}
void end()
{
    int i, sum = inf;
    for (i = num; i!= 0; i = pre[i])//找到可以增加的最大的流,是整條最短路上的最小流
    	sum = Min(sum, cap[pre[i]][i]);
    for (i = num; i != 0; i = pre[i])
    {
	    cap[pre[i]][i] -= sum;//正向減去增加的流
	    cap[i][pre[i]] += sum;//逆向加上增加的流
	    ans += map[pre[i]][i] * sum;//計算本次的花費,實際上就是從place pre[i]到第i個人對於當前種類的物品所花費的錢
    }
}

    int main()
{
    int N,M,K,i,j,k;
    int need[nMax][nMax];
    int needk[nMax];
    int have[nMax][nMax];
    int havek[nMax];
    int flag;
    while (scanf("%d %d %d", &N, &M, &K), N)
    {
    memset(needk, 0, sizeof(needk));
    for (i = 1; i <= N; ++ i)
    {
    for (j = 1; j <= K; ++ j)
    {
    scanf("%d", &need[i][j]);
    needk[j] += need[i][j];//求出每種貨物最大的需求量
    }
    }
    memset(havek, 0, sizeof(havek));
    for (i = 1; i <= M; ++ i)
    {
    for (j = 1; j <= K; ++ j)
    {
    scanf("%d", &have[i][j]);
    havek[j] += have[i][j];//計算出所有地方能提供出每種貨物的最大量
    }
    }
    flag = 1;
    for (i = 1; i <= K; ++ i)
    {
    if (needk[i] > havek[i])//如果有物品供給不足,那麼肯定不能完成運送
    {
    flag = 0;
    break;
    }
    }
    ans = 0;
    num = N + M + 1;
    for (k = 1; k <= K; ++ k)//計算每種貨物的最小花費,然後求和
    {
	    memset(cap, 0, sizeof(cap));
	    memset(map, 0, sizeof(map));
	    for (i = 1; i <= N; ++ i)
	    {
	    for (j = 1; j <= M; ++ j)
	    {
		    scanf("%d", &map[j][M + i]);//將N個人對映到M+1-M+N區間上,這樣方便建圖,map j到M+i就是從地方j運送到人i的花費
		    map[M + i][j] = -map[j][M + i];
		    cap[j][M + i] = have[j][k];//j到i的量是第k種貨物的在place j的最大的量
		    cap[M + i][j] = 0;
	    }
	    }
	    if (!flag)
	    {
	   		continue;
	    }
	    for (i = 1; i <= M; ++ i)
	    {
		    cap[0][i] = have[i][k];//源點到place i其實也設為第k中貨物在place i的量
		    map[0][i] = map[i][0] = 0;//從原點到i花費為0
	    }
	    for (i = 1; i <= N; ++ i)
	    {
		    cap[M + i][num] = need[i][k];//從人i到匯點的量設為第i個人對第k種貨物的需求量。
		    map[M + i][num] = map[num][M + i] = 0;
	    }
	    while (spfa())//求第k種貨物的最小花費
	    {
	    	end();
	    }
    }
    if (flag)
    {
    printf("%d\n", ans);
    }
    else
    printf("-1\n");
    }
    return 0;
}