【圖論】最大流之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;
}