【图论】最大流之EK算法与Dinic算法及最小费用最大流

时间:2021-09-19 21:31:48

最大流:

给出一张网络图,并指定源点和终点,每条边都有它的容量,起点有着无限的流量,求从源点到经过的所有路径的最终到达汇点的最大流量和。对于同一个节点,流入的流量之和和流出的流量之和相同,即假如结点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;
}