剑指offer——网络的流程——一生必看的C++经典算法

网络流

0.序言

听起来这个名字如此高级,其实不然,大家要抱着一种开放包容的心态去学它。

一.基本概念

先来见一下网络流的基本图形:
在这里插入图片描述
可以看到,网络流的图形有一个源点s和一个汇点t,每一条边都有一个容量和流量,流量永远小于或等于容量。

二.预备知识

网络流必须要用数组模拟指针的方法去存图,不能用vector哟。
推荐博起:数据结构之邻接表

三.最大流

1.概念

在这里插入图片描述
最大流就是说要求从源点到汇点最大的流量,这个图显而易见,最大流就是15,图中已经标出来了。
那么怎么计算这个最大流呢?

2. EK算法

1.思想

很容易想到一个有些暴力的算法:从源点开始,暴力的找出所有到汇点的路径,再把每条路径上最小的边的容量加起来就行了。
每条能贡献流量的路就叫做增广路。
但是这种方法的正确性有待考究,比如这个图:
在这里插入图片描述
假如我们先走s→1→4→t,图就变成了这样:
在这里插入图片描述
然后走s→2→4→t,发现4→t的时候路被堵了,但是上一条路径明明可以改成s→1→3→t,这样这一条路就可以顺利走通了,并且找到了最大流,就是20,如下图:
在这里插入图片描述
现在最难得问题就是怎么来实现这个算法呢?
现在,引入一种算法:建反边
那么,原图就变成了这个样子(初始化所有反边容量为0):
在这里插入图片描述
每走过一条边,就让你走的方向的边的容量减去流量,让反向的边的容量加上流量就行了。最后你找最大流的时候,就直接在图中从s到t走,找出所有增广路(你建的所有的边都可以走),像最初我们的想法那样做就行了。
总结起来,分三步:
1.建反向边
2.找所有增广路
3.统计最大流

这就叫做EK算法。
那为什么要建反边呢?个人认为是为了让路径上遇到容量不允许的边能够改变原来走那条边的路径,让那条路径去找其它可能的边去走,如果找不到,就让这条路径终止,那条路径不变。额。。。大家可以去找找其它的证明康康。

2.实现

先说找增广路:
因为我们要存储这条路径,所以要用pre[x]去存经过点x的路径的前一个点,和点x与前一个点相连的边的编号(要用数组模拟指针去存图哟)。
上代码:

struct node {
    int x, edge;
    node (){};
    node (int X, int Edge){
        edge = Edge;
        x = X;
    }
}pre[N];
bool find (){
    queue <int> Q;
    Q.push (s);
    memset (vis, 0, sizeof vis);
    memset (pre, -1, sizeof pre);
    vis[s] = 1;
    while (! Q.empty ()){
        int f = Q.front ();
        Q.pop ();
        for (int i = first[f]; i != -1; i = next[i]){
            if (! vis[v[i]] && w[i]){
                pre[v[i]].x = f;
                pre[v[i]].edge = i;
                if (v[i] == t)
                    return 1;
                vis[v[i]] = 1;
                Q.push (v[i]);
            }
        }
    }
    return 0;
}

再来说说统计最大流:
很简单,先沿着找到的增广路找出最小容量的边的容量作为流量,再把路径上的边的容量减去流量,反边加上流量就行了。

void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x){
            minn = min (minn, w[pre[i].edge]);
        }
        for (int i = t; i != s; i = pre[i].x){
        	//printf ("%d ", i);
        	w[pre[i].edge] -= minn;
        	w[pre[i].edge ^ 1] += minn;
		} 
		//printf ("%d\n", s); 
		ans += minn;
    }
}

3.Code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define M 200005
#define N 10005
#define INF 0x3f3f3f3f

struct node {
    int x, edge;
    node (){};
    node (int X, int Edge){
        edge = Edge;
        x = X;
    }
}pre[N];
int n, m, s, t, u[M], v[M], w[M], first[N], next[M], k, ans;
bool vis[N];

void addedge (int x, int y, int z){
	k ++;
	u[k] = x, v[k] = y, w[k] = z;
	next[k] = first[x];
	first[x] = k;
}
bool find (){
    queue <int> Q;
    Q.push (s);
    memset (vis, 0, sizeof vis);
    memset (pre, -1, sizeof pre);
    vis[s] = 1;
    while (! Q.empty ()){
        int f = Q.front ();
        Q.pop ();
        for (int i = first[f]; i != -1; i = next[i]){
            if (! vis[v[i]] && w[i]){
                pre[v[i]].x = f;
                pre[v[i]].edge = i;
                if (v[i] == t)
                    return 1;
                vis[v[i]] = 1;
                Q.push (v[i]);
            }
        }
    }
    return 0;
}
void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x){
            minn = min (minn, w[pre[i].edge]);
        }
        for (int i = t; i != s; i = pre[i].x){
        	//printf ("%d ", i);
        	w[pre[i].edge] -= minn;
        	w[pre[i].edge ^ 1] += minn;
		} 
		//printf ("%d\n", s); 
		ans += minn;
    }
}
int main (){
    scanf ("%d %d %d %d", &n, &m, &s, &t);
    k = -1;
    for (int i = 1; i <= n; i ++)
        first[i] = -1;
    for (int i = 1; i <= m; i ++){
        int u, v, w;
        scanf ("%d %d %d", &u, &v, &w);
        addedge (u, v, w);
        addedge (v, u, 0);
    }
    EK ();
    printf ("%d\n", ans);
    return 0;
}

4.运用

先给模板:最大流模板
二分图匹配:完美的牛栏
会了最大流算法,就不怕二分图匹配了。
我们建一个源点,向一个集合中的所有点连一条无限流量的边,再建一个汇点,从另一个集合的每一个点向汇点连一条无限流量的边,两个集合之间的边的容量就为1,最后找一波最大流就是答案了。
虽然要慢一些,但是不要怕,好打。

四.最小费用最大流(费用流)

1.概念

每条边加一个经过这条边单位流量需要的费用,在保证最大流的情况下,要使花费的费用最小。

2.思想&实现

既然要费用最小,很容易想到各种求最短路的算法,这里用spfa。
我们直接把原来的bfs改成spfa就完事了。
统计的时候把每条边的费用加起来得到总费用,乘以流量就是这条增广路的费用了。

3.Code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define N 10005
#define M 100005
#define INF 0x3f3f3f3f

struct node {
    int x, edge;
}pre[N];
int n, m, s, t, u[M], v[M], w[M], f[M], head[N], next[M], k = -1, ans1, ans2, dis[N];
bool vis[N];

void addedge (int x, int y, int z, int p){
    k ++;
    u[k] = x, v[k] = y, w[k] = z, f[k] = p;
    next[k] = head[x];
    head[x] = k;
}
bool find (){
    memset (pre, -1, sizeof pre);
    memset (dis, INF, sizeof dis);
    memset (vis, 0, sizeof vis);
    vis[s] = 1;
    queue <int> Q;
    Q.push (s);
    dis[s] = 0;
    while (! Q.empty ()){
        int p = Q.front ();
        Q.pop ();
        vis[p] = 0;
        for (int i = head[p]; i != -1; i = next[i]){
            if (w[i] && dis[v[i]] > dis[p] + f[i]){
                dis[v[i]] = dis[p] + f[i];
                pre[v[i]].x = p;
                pre[v[i]].edge = i;
                if (! vis[v[i]]){
                    vis[v[i]] = 1;
                    Q.push (v[i]);
                }
            }
        }
    }
    return dis[t] != INF;
}
void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x)
            minn = min (minn, w[pre[i].edge]);
        for (int i = t; i != s; i = pre[i].x){
            w[pre[i].edge] -= minn;
            w[pre[i].edge ^ 1] += minn;
        }
        ans1 += minn;
        ans2 += minn * dis[t];
    }
}
int main (){
    scanf ("%d %d %d %d", &n, &m, &s, &t);
    memset (head, -1, sizeof head);
    for (int i = 1; i <= m; i ++){
        int x, y, z, p;
        scanf ("%d %d %d %d", &x, &y, &z, &p);
        addedge (x, y, z, p);
        addedge (y, x, 0, -p);
    }
    EK ();
    printf ("%d %d\n", ans1, ans2);
    return 0;
}

4.运用

模板:最小费用最大流

五.最小割最大流

1.概念

最小割的概念就是把整个图分成两个点集,割掉边让两集合的点不相连的最小费用和。

2.解法

给你个定理你就知道了:一个图的最小割等于最大流
知道了吧。

六.运用

网络流的运用其实挺容易看出来的,下面有几道题可以借鉴:
费用流
切糕
Going Home

谢谢!

发布了61 篇原创文章 · 获赞 32 · 访问量 8331

猜你喜欢

转载自blog.csdn.net/weixin_43908980/article/details/103596278