网络流入门

网络流入门

24602.png

给出一个源点\(s\), 现在有\(inf\)单位的水,中间经过一大堆水管和一大堆点到达一个汇点\(t\),每个水管都有流量限制,求最多有多少水能流到\(t\)。能流到t的水量就是最大流。

像上面这张图,它的最大流就是\(22 + 10 + 45 = 77\)。(这图是盗的)

那么如何用程序的方法来实现最大流呢?

Edmond-Karp算法(EK)

最大流

这是一种时间复杂度比较不优秀的算法。具体等会再说。

现在我们定义增广路:可以从\(s\)流到\(t\)的一条可行路径。

增广:给一条增广路增流。

于是我们想:可不可以用一些玄妙的算法,每次找到一条增广路进行增广

这就是EK算法的核心。我们每次从源点进行bfs找向汇点,找到一条增广路之后记录这条增广路上流量限制最小的边,然后把这整条增广路的边权全部扣去这个值

bool /*SPFA*/dijstra(){
    memset(pre, 0, sizeof(pre));
    memset(vis, 0, sizeof(vis));
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = dt[u]; i; i = e[i].next){
            int v = e[i].to;
            if(e[i].val != 0 && !vis[v]){
                pre[v].fa = u;
                pre[v].num = i;
                if(v == t)return 1;
                vis[v] = 1;
                q.push(v);
            }
        }
    }
    return 0;
}

如上面的代码。我们用\(pre\)数组来记录增广路。当这条边的边权\(>0\)的时候就意味着可以进行增广。

别忘记用一个\(vis\)数组来记录这条边有没有被重复遍历。

void EK(){
    while(dijstra()){
        mi = 0x3f3f3f3f;
        for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
        for(int i = t; i != s; i = pre[i].fa){
            e[pre[i].num].val -= mi;
            e[pre[i].num ^ 1].val += mi;
        }ans += mi;
    }
}

最后递归遍历一下,进行增广。

这里有些奇奇怪怪的东西:为什么要\(e_{pre_{i}.num \ xor \ 1}.val += mi\)

这里隆重介绍一个小技巧:反向边

大体来说,就是给了一条流反悔的机会

(具体我也不是很清楚,反正每次照打就是了)

正向边扣流量的同时反向边要加上同样的流量

反向边的编号是正向边的编号\(xor \ 1\)

struct dartou{
    int to, val, next;
}e[N];

struct dat{
    int fa, num;
}pre[N];

bool vis[N];

int dt[N], s, t;

void add(int x, int y, int value){
    cnt++;
    e[cnt].to = y;
    e[cnt].val = value;
    e[cnt].next = dt[x];
    dt[x] = cnt;
}

bool /*SPFA*/dijstra(){
    memset(pre, 0, sizeof(pre));
    memset(vis, 0, sizeof(vis));
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = dt[u]; i; i = e[i].next){
            int v = e[i].to;
            if(e[i].val != 0 && !vis[v]){
                pre[v].fa = u;
                pre[v].num = i;
                if(v == t)return 1;
                vis[v] = 1;
                q.push(v);
            }
        }
    }
    return 0;
}

void EK(){
    while(dijstra()){
        mi = 0x3f3f3f3f;
        for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
        for(int i = t; i != s; i = pre[i].fa){
            connect[pre[i].fa] = i;
            e[pre[i].num].val -= mi;
            e[pre[i].num ^ 1].val += mi;
        }ans += mi;
    }
}

int main(){
    //主体加入处
}

此处建议在add函数处加入以下代码:

    cnt++;
    e[cnt].to = x;
    e[cnt].next = dt[y];
    dt[y] = cnt;

这样子就可以正反向边一起建。

EK这就讲完了。时间复杂度\(O(nm^2)\)。这里n是点数,m是边数。

例题1 【Luogu P3376】 网络最大流

题目传送门

EK板子。

这部分就结束了。

最小费用最大流

每条边除了有流量限制以外还有一个\(w\),指流过每一个单位流量所需的费用。

现在问在达到流量最大的前提下,最小费用是多少。

一样找增广路。

只不过这次找的是所需费用最小的增广路。

然后呢?

把bfs改成spfa就行了!虽然它已经死了

bool /*SPFA*/dijstra(){
    memset(pre, 0, sizeof(pre));
    memset(vis, 0, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    dis[s] = 0;
    while(!q.empty()){
        register int u = q.front();
        vis[u] = 0;
        q.pop();
        for(register int i = dt[u]; i; i = e[i].next){
            register int v = e[i].to, w = e[i].w;
            if(e[i].val > 0 && dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                pre[v].fa = u;
                pre[v].num = i;
                if(!vis[v]){
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    return dis[t] != 0x3f3f3f3f;
}
void EK(){
    while(dijstra()){
        mi = 0x3f3f3f3f;
        for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
        for(int i = t; i != s; i = pre[i].fa){
            e[pre[i].num].val -= mi;
            e[pre[i].num ^ 1].val += mi;
        }
        maxflow += mi;
        cost += mi * dis[t];
    }
}

需要注意的是:反向边的费用是正向边的费用的相反数

很好理解。如果这一段反悔不流了,就要把花的钱退掉。

例题2【Luogu P3381】【模板】最小费用最大流

跑板子。

下面介绍一种更为高效的算法:

Dinic算法

时间复杂度\(O(n^2m)\)。对于稠密图要比EK快很多。

二分图匹配的复杂度则是\(O(sqrt(n)m)\)。(全是抄的)

总之,Dinic很快,比EK快得多。

所以尽量不要打EK。会被卡上天。那还讲它干嘛

基本步骤

1、bfs标号。

2、dfs增广。

3、反复运行。

最大流

如下图:

61211.png

(用sketchbook画图真难受)

拿这张标完号的图作为例子。

我们利用标号找增广路。一次可以找到三条:

\(s -> 1 -> t\)

\(s -> 2 -> t\)

\(s -> 3 -> t\)

相对于EK每一次只能找到一条增广路来说,dinic效率实在要高很多。

而且我们用dinic还能排除一些不必要的鬼畜路径,比如:
\[ s -> 2 -> 3 -> t \]
这条路径明显较长。

所以,dinic找的是最短增广路\(\forall u,v \text{}且u -> v\),有\(dep[v] = dep[u] + 1\)时,\(u -> v\)这条路径在一条最短增广路上。

dfs每一次沿着最短增广路进行增广。

再举一个极端例子。

27822.png

(这图又是偷的。画图很麻烦啊……)

有了标号,就不会兜一大圈了。

bool bfs(){
    memset(dep, 0x3f, sizeof(dep));
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= N; i++)cur[i] = head[i];
    queue<int> q;
    vis[s] = 1;
    dep[s] = 0;
    q.push(s);
    while(!q.empty()){
        register int u = q.front();
        q.pop();
        for(int i = head[u]; i; i = e[i].next){
            int v = e[i].to;
            if(dep[v] > dep[u] + 1 && e[i].f){
                dep[v] = dep[u] + 1;
                if(!vis[v])
                    q.push(v);
                vis[v] = 1;
            }
        }
    }
    return dep[t] != 0x3f3f3f3f;
}

int dfs(int now, int low){
    int flow = 0;
    if(now == t)return low;
    for(int i = cur[now]; i; i = e[i].next){
        cur[now] = i;
        int v = e[i].to;
        if(e[i].f && dep[v] == dep[now] + 1){
            if(flow = dfs(v, min(low, e[i].f))){
                e[i].f -= flow;
                e[i ^ 1].f += flow;
                return flow;
            }
        }
    }
    return 0;
}
最小费用最大流

同样地,dinic也可以运用到最小费用最大流上。

先跑一遍spfa。虽然它已经死了

然后在dfs中累加费用。

举一反三一下,\(\forall u,v\) \(u -> v\),若\(dis[v] = dis[u] + e[i].w\),则\(u -> v\)这条路径在最短路上。

原样增广就完事了。

bool /*SPFA*/dijstra(){
    memset(vis, 0, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    for(int i = 0; i <= N; i++)cur[i] = dt[i];
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    dis[s] = 0;
    while(!q.empty()){
        int u = q.front();
        vis[u] = 0;
        q.pop();
        for(int i = dt[u]; i; i = e[i].next){
            int v = e[i].to, w = e[i].w;
            if(e[i].val > 0 && dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                //cout << v << " " << pre[v].fa << endl;
                if(!vis[v]){
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    return dis[t] < 0x3f3f3f3f;
}

int dfs(int now, int low){
    if(now == t) return low;
    int flow = 0;
    vis[now] = 1;
    for(int i = cur[now]; i; i = e[i].next){
        cur[now] = i;
        int v = e[i].to;
        if(!vis[v] && e[i].val && dis[v] == dis[now] + e[i].w){
            if(flow = dfs(v, min(low, e[i].val))){
                e[i].val -= flow;
                e[i ^ 1].val += flow;
                cost += e[i].w * flow;
                return flow;
            }
        }
    }
    return 0;
}

int dinic(){
    int minflow, maxflow = 0;
    while(dijstra()){
        while(minflow = dfs(s, inf)){
            memset(vis, 0, sizeof(vis));
            maxflow += minflow;
        }
    }
    return maxflow;
}

记得给图打上vis标记。如果费用为0,就会两个点来回跳,被卡死。感性理解下?

每次照打就是了。没有例题。可以再把板子写一遍。

引用

1.洛谷日报#14 用最通俗的语言让你学会网络流

2.[洛谷日报#56 EK不够快?再学个Dinic吧](

转载于:https://www.cnblogs.com/ironwheel/p/11189161.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值