网络流-最大流

网络流

定义

网络流的定义是:一张有向图,由点集V和边集E组成,其中每条边都有流量(flow),容量(cap)。用f(u,v)和c(u,v)表示u->v的流量和容量。一个图是网络流需要满足以下条件:
1.有源点和汇点。
2.u->v不存在时,c(u,v)=0
3.f(u,v)<=c(u,v)。
4.除了源点和汇点,流入一个节点的流量=流出一个节点的流量。

和网络流联系紧密的是残量网络,记录r(u,v)=c(u,v)-f(u,v),表示u->v的残量,也就是这条边还能流进多少(可以想象成水管)。

后向弧

我们给每条u->v都反向建边,但这不是真正存在的边(所以容量=0),我们称之为后向弧。对于一条u->v的边,他的后向弧的流量是他流量的相反数。后向弧起到的作用是“撤销”,因为往后向弧加流量就等同于往真正的边减流量。

最大流

定义

从源点s到汇点t最大的流量。

增广路

从s到t的一条路径(该路径中可以存在后向弧),其中走过的边的残量r需均>0。

解决

最大流可以通过不停寻找增广路求得,当不存在增广路时,可以证明(并不会证明,要用最小割最大流定理证明,但太蒟蒻了,暂时还不会)此时s到t的流量即为最大流。

Edmond-Karp

思想

由于最大流可以通过不停寻找增广路求得,于是非常暴力的Edmond-Karp(简称EK)诞生了:直接bfs寻找增广路。

实现

EK算法概括如下:
当有最短增广路时:
1.bfs寻找增广路。
2.更改这条增广路流量(+最小的残量)。
否则,最大流求得。

可以证明(太蒟蒻了,不会证明),最多bfs总共n*m(n为节点个数,m为边数)次,又因为每次bfs的复杂度为O(m),所以总复杂度为O(n*m^2),但实际效果远小于此。

模板

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=505,maxm=40005,MAXINT=((1<<30)-1)*2+1;

struct Edge
{
    int cap,flow,to,nxt;
};
struct fa 
{
    int who,e;
};
int n,m,E,st,gl,lnk[maxn],que[maxn],dis[maxn];
fa father[maxn];
Edge e[maxm];
bool vis[maxn];
void Add(int x,int y,int z)
{
    e[E].to=y;e[E].cap=z;e[E].nxt=lnk[x];lnk[x]=E;E++; //加了一条边
    e[E].to=x;e[E].cap=0;e[E].nxt=lnk[y];lnk[y]=E;E++; //后向弧
    //这么建边,则j是真正的边,j^1是对应的后向弧
}
int Bfs() //bfs寻找增广路
{
    int Head=0,Tail=0;memset(vis,0,sizeof(vis));
    que[++Tail]=st;vis[st]=true;dis[Tail]=MAXINT;
    while (Head!=Tail)
    {
        int x=que[++Head];
        for (int j=lnk[x];j!=-1;j=e[j].nxt)
            if (!vis[e[j].to]&&e[j].cap-e[j].flow>0) //残量必须>0
            {
                que[++Tail]=e[j].to;vis[e[j].to]=true;
                father[e[j].to].who=x;father[e[j].to].e=j;
                dis[Tail]=min(dis[Head],e[j].cap-e[j].flow);
                if (e[j].to==gl) return dis[Tail]; //走到终点
            }
    }
    return 0;
}
void change(int MIN) //修正
{
    int now=gl;
    while (father[now].who) //ÑØ×Åfather×ß
    {
        int j=father[now].e;
        e[j].flow+=MIN;e[j^1].flow-=MIN; //j的流量多了MIN,同时j的相反边流量减少MIN
        now=father[now].who;
    }
}
int MAXflow() //求最大流
{
    int MAX=0;
    while (true)
    {
        int f=Bfs();
        if (f==0) break;
        change(f);MAX+=f;
    }
    return MAX;
}
int main()
{
    freopen("EK.in","r",stdin);
    freopen("EK.out","w",stdout);
    scanf("%d%d",&n,&m);
    memset(lnk,255,sizeof(lnk));
    for (int i=1;i<=m;i++)
    {
        int x,y,z;scanf("%d%d%d",&x,&y,&z);
        Add(x,y,z);
    }
    scanf("%d%d",&st,&gl);
    printf("%d\n",MAXflow());
    return 0;
}

Dinic

作用

EK的时间复杂度不是那么良好,这是由于寻找增广路的想法并不好,而寻找增广路又是求最大流所必须的。Dinic算法改善了寻找增广路的方法,能比较快速地求出最大流。

思想

Dinic分为两个步骤:1.求层次图。2.求阻塞流。
1.求层次图
顾名思义,层次图是一层一层的,第一层距离为0,第二层距离为1,第三层距离为2……举个例子,如下:
这里写图片描述
很明显用bfs就可以求出层次图(其实就是算出距离)。
2.求阻塞流
如果存在层次图(存在层次图等价于能从s到t),就在层次图中寻找增广路(只有i-1层的点才能到达i层),直到没有增广路为止。所有的这些增广路成为阻塞流。因为同时求很多增广路,所以我们可以用dfs实现,但要注意细节优化,详见代码。

初始层次图s到t的距离最小为1,每次求完阻塞流后s到t的距离至少+1,因为阻塞流会让层次图的至少一层断开(否则肯定还能找到增广路)。所以求层次图和阻塞流的次数最多为n-1(距离为n是不可能的,因为总共只有n个节点),而每次求阻塞流的时间复杂度是O(n*m)(每条边都被经过了n次),则Dinic的效率为O(n^2*m),和EK一样,实际效果远小于此。

比较

如下是POJ1459的时间对比。
这里写图片描述
第一个是Dinic,第二个是EK,可见Dinic快了非常多。

模板

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=500,maxm=40000,MAXINT=((1<<30)-1)*2+1;

int n,m,E,lnk[maxn+5],nxt[maxm+5],son[maxm+5];
int cur[maxn+5],que[maxn+5],dis[maxn+5];
bool vis[maxn+5];
struct Edge
{
    int cap,flow;
};
Edge e[maxm+5];

void Add(int x,int y,int z)
{
    son[E]=y;nxt[E]=lnk[x];lnk[x]=E;e[E].cap=z;e[E].flow=0;E++;
    son[E]=x;nxt[E]=lnk[y];lnk[y]=E;e[E].cap=0;e[E].flow=0;E++;
}
bool Bfs(int st,int gl)
{
    memset(vis,0,sizeof(vis));
    int Head=0,Tail=0;que[++Tail]=st;vis[st]=true;dis[st]=1;
    while (Head!=Tail)
    {
        int x=que[++Head];
        for (int j=lnk[x];~j;j=nxt[j])
            if (!vis[son[j]]&&e[j].cap>e[j].flow)
            {
                vis[son[j]]=true;dis[son[j]]=dis[x]+1;
                que[++Tail]=son[j];
            }
    }
    return vis[gl];
}
int Dfs(int x,int gl,int MIN) //MIN是目前的最小残量
{
    if (x==gl||MIN==0) return MIN; //当MIN为0时及时退出,否则会有很多多余搜索
    int flow=0;
    for (int &j=cur[x];~j;j=nxt[j])
    //从上一次继续做,而之前的不用管了因为早就处理完了
        if (dis[x]+1==dis[son[j]]) //只能在层次图中增广
        {
            int now=Dfs(son[j],gl,min(MIN,e[j].cap-e[j].flow));
            if (now)
            {
                e[j].flow+=now;e[j^1].flow-=now;
                flow+=now;MIN-=now;
                if (!MIN) break;
            }
        }
    return flow;
}
int MAXflow(int s,int t)
{
    int flow=0;
    while (Bfs(s,t)) //层次图
    {
        memcpy(cur,lnk,sizeof(cur));
        flow+=Dfs(s,t,MAXINT); //阻塞流
    }
    return flow;
}
int main()
{
    freopen("Dinic.in","r",stdin);
    freopen("Dinic.out","w",stdout);
    scanf("%d%d",&n,&m);
    memset(lnk,255,sizeof(lnk));
    for (int i=1;i<=m;i++)
    {
        int x,y,z;scanf("%d%d%d",&x,&y,&z);
        Add(x,y,z);
    }
    printf("%d\n",MAXflow(1,n));
    return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值