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




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值