Loading

网络流入门

网络流是什么?

网络流是 OI 图论中的内容,比较有用的是网络最大流费用流

网络流就像一个复杂的水网,它有一个源点 \(S\),可以供应无穷无尽的水,有一个汇点 \(T\),要让水从源点流到汇点,我们就连上了许多水管,也就是,中间有一些中转站连接不同的水管,也就是,每条水管都有一个流量上限,叫做容量,如果超过了容量就会炸裂,所以我们不能灌入比该水管流量上限更多的水,空闲量就是该边还可以通过的水的容量。

比如下图,图中的边就相当于水管,边上的数字(权值)就是每条边的流量上限。

image

这是将水输送到 \(T\) 一种合法的方案:

其中紫色数字是水流大小 \(d\),黑色数字是容量 \(f\),其中,一定有 \(d \leq f\)

image

那这个图的的流量就是 \(3\),我们既可以看源点也可以看汇点得出(\(1 + 2 = 3\))。

找阻塞流

如果我们不能从源点输送更多的水到汇点,那么这个水流的状态就叫阻塞流

上图就不是一个阻塞流,因为实际上可以从源点往容量为 \(3\) 的水管再输送 \(1\) 的水。

我们还要知道:

空闲量 = 容量 - 流量

即空闲量表示还可容纳多少水。

首先,我们建出以空闲量为权值的图 \(G\),最开始时没有水流通过,所以该图等于原图。

image

然后我们随便从图 \(G\) 中找出从 \(S\)\(T\) 的简单路径,我们现在就要让这条路径尽它的最大能力输送水,

image

取到这条路径上最小的权值 \(w\),再将所有的权值减去 \(w\),表示有 \(w\) 的水流过,空闲量减少了 \(w\)

image

然后再将所没用的边去掉,即容量为 \(0\) 的边。

image

不断重复上述步骤,

image

image

image

image

image

image

image

当我们已经找不到路径了,我们就可以停止了。

我们把刚刚减去 \(w\) 全部累加起来,就得到了阻塞流。

image

网络最大流

什么是网络最大流呢?

就是在不超过容量的情况下,从源点输送更多的水到汇点。

比如上图的网络最大流就是 \(4\),不可能再输送更多的水从 \(S\)\(T\)

image

那么我们怎么求网络最大流呢?

有很多算法可以办到,比如 Ford–Fulkerson,EK,dinic 算法。

可以这么理解 EK 是 Ford–Fulkerson 的优化,dinic 是 EK 的优化。

Ford–Fulkerson 算法

首先,我们发现再刚刚找阻塞流的时候就是一个劲地往下找,

运气不好就找不到网络最大流,比如下面这个例子(注意不是上面那个图),

找到的阻塞流为 \(3\),最大流其实是 \(4\)

image
image
image

其实,我们可以建立一条反向边。

让水倒着流回去,比如:

image

那么我们在找阻塞流的时候顺便加上一条容量相同的反向边就实现了“可以后悔”的做法,

即找错了边也不要紧,再回来不就完了。

算法过程就变成了:

  1. 随便找一条从 \(S\)\(T\) 的路径,与找阻塞流一样;
  2. 然后取该边上最小的权值 \(w\),让所有的边都减去 \(w\),与找阻塞流一样;
  3. 建立一条权值为 \(w\) 的反边,可以让水流原路流回去。

比如下图,图中虚线是反边,红色边是找到的路径(正反边皆可),紫色数字是反边权值,黑色数字是原边权值:

image

然后不断重复以上步骤,直到找不到路径为止。

image
image
image
image
image

最后我们把刚刚得到的所有 \(w\) 加起来就是最大流。

image

Ford–Fulkerson 算法的时间复杂度为 \(O(fm)\)\(f\) 为最大流大小,\(m\) 为边的数量(可以想一想为什么是 \(O(fm)\)

EK 算法

剩下的部分,终于不用画图了,纯属优化

EK 算法唯一优化的地方是找路径的时候找最短路径,而不是随便找,BFS 就可以实现。

当然,我们是把图当作无权图来对待。

image

发明者证明了该算法时间复杂度为 \(O(m^2n)\)

C++代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 210, M = 10010, inf = 0x3f3f3f3f3f3f3f3f;

struct Edge {
    int to;
    int next;
    LL c;
} e[M];

int head[N], idx = 1;

void add(int a, int b, int c) {
    idx++;
    e[idx].next = head[a];
    e[idx].c = c;
    e[idx].to = b;
    head[a] = idx;
}

int n, m, S, T;

bool vis[N];
int q[N];
int hh, tt;
int pre[N];
LL flow[N];

bool bfs() {
    memset(vis, 0, sizeof vis);
    vis[S] = 1;
    hh = 0, tt = -1;
    q[++tt] = S;
    flow[S] = inf;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = head[t]; i; i = e[i].next) {
            if (e[i].c) {
                int to = e[i].to;
                if (vis[to]) continue;
				flow[to] = min(flow[t], e[i].c);
                vis[to] = 1;
                pre[to] = i;
                q[++tt] = to;
                if (to == T) return 1;
            }
        }
    }
    return 0;
}

LL ans;

void update() {
    int x = T;
    while (x != S) {
        int p = pre[x];
        e[p].c -= flow[T];
        e[p ^ 1].c += flow[T];
        x = e[p ^ 1].to;
    }
    ans += flow[T];
}

int main() {
    scanf("%d%d%d%d", &n, &m, &S, &T);
    for (int i = 1; i <= m; i++) {
        int a, b;
        LL c;
        scanf("%d%d%lld", &a, &b, &c);
        add(a, b, c);
        add(b, a, 0);
    }
    while (bfs()) update();
    printf("%lld\n", ans);
    return 0;
}

dinic 算法

我们在找最短路径的时候顺便将图分层,从 \(S\) 最少几步可以到达,一个点的层次就是几,但是我们假设到 \(S\) 点本身就需要 \(1\) 步,即 \(S\) 的层次为 \(1\)

比如:

image

然后我们在找路径的时候,加入我们已经到了 \(u\),下一步我们一定要到下一层的 \(v\),而不是相同层次的点:

image

其他内容与 EK 算法相同,算法发明者证明了其时间复杂度为 \(O(n^2m)\)

C++代码:

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 210, M = 10010, INF = 1e16;

struct edge {
    int to, next, w;
} e[M];

int head[N], idx = 1;

void add(int a, int b, int c) {
    idx++, e[idx].to = b, e[idx].next = head[a], e[idx].w = c, head[a] = idx;
    idx++, e[idx].to = a, e[idx].next = head[b], e[idx].w = 0, head[b] = idx;
}

int n, m, S, T;
int q[N];
int hh, tt;
int d[N];

bool bfs() {
    memset(d, 0, sizeof(d));
    hh = tt = 0;
    q[0] = S;
    d[S] = 1;
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (!d[to] && e[i].w) {
                d[to] = d[t] + 1;
                q[++tt] = to;
                if (to == T) return true;
            }
        }
    }
    return false;
}

int dinic(int u, int limit) {
    if (u == T) return limit;
    int rest = limit;
    for (int i = head[u]; i && rest; i = e[i].next) {
        int to = e[i].to;
        if (d[to] == d[u] + 1 && e[i].w) {
            int k = dinic(to, min(rest, e[i].w));
            if (!k) d[to] = 0;
            rest -= k;
            e[i].w -= k;
            e[i ^ 1].w += k;
        }
    }
    return limit - rest;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> S >> T;
    for (int i = 1; i <= m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    int maxflow = 0, flow = 0;

    while (bfs()) {
        while (flow = dinic(S, INF)) {
            maxflow += flow;
        }
    }
    cout << maxflow << '\n';
    return 0;
}

费用流

思路

有时,输送水是需要付钱的!

费用流就是在每条管道指定要解决在保证最大流的前提下,付的钱最少。

每条边会有两个权值,\(w, cost\)\(w\) 是边的容量,\(cost\) 是流过单位量的水需要付的钱。

那么怎么解决这个问题呢?

实际上,非常简单,既然要保证最大流,那么还是需要找路径,又要付的钱最少,

那么就相当于每一条边带上了一个权值,我们不能再把它当作无权图来对待了。

您可能已经想到,可以跑最短路,这样既找到路径,又保证费用最小化。

是的,费用流非常简单,只要把网络最大流的 BFS 改成 SPFA 即可,

至于为什么不用 Dijkstra,很显然可能有负边权,但一般遇不到,对这一点有疑惑的可以看看官方抽象的定义。


那我们怎么建反边呢?

容量与最大流一样,就是正边流过了 \(w\),反边就加上 \(w\)

但是价格不太一样,假如正边的价格是 \(cost\),那么反边就是 \(-cost\),因为我们要把钱拿回来。

此时,我们因为用了 SPFA,那么说明费用流的算法就不是基于分层图的,

因为如果像最大流一样 BFS,那么 BFS 到哪一层,哪一层的点一定是最先被访问到的,一定是最短的,

但是加上了权值就不一样了,经过同一层次的点也有可能最短,所以费用流的算法就不是基于分层图的。

我们用 dinic 反而是小题大作,还要分个层,所以我们费用流多用 EK 算法。

C++代码

#include <bits/stdc++.h>

using namespace std;

const int N = 5010, M = 100010, INF = 0x3f3f3f3f;

struct edge {
    int to, next, w, cost; 
} e[M];

int head[N], idx = 1;

void add(int u, int v, int w, int cost) {
    idx++;
    e[idx].to = v;
    e[idx].next = head[u];
    e[idx].w = w;
    e[idx].cost = cost;
    head[u] = idx;
    
    idx++;
    e[idx].to = u;
    e[idx].next = head[v];
    e[idx].w = 0;
    e[idx].cost = -cost;
    head[v] = idx;
}

int n, m, S, T;
int dis[N], pre[N], flow[N];
bool st[N];

bool spfa() {
    queue<int> q;
    q.push(S);
    memset(dis, 0x3f, sizeof(dis));
    memset(flow, 0, sizeof(flow));
    dis[S] = 0;
    flow[S] = INF;
    st[S] = true;
    
    while (q.size()) {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (e[i].w && dis[to] > dis[t] + e[i].cost) {
                dis[to] = dis[t] + e[i].cost;
                pre[to] = i;
                flow[to] = min(flow[t], e[i].w);
                if (!st[to]) {
                    q.push(to);
                    st[to] = true;
                }
            }
        }
    }
    return flow[T] > 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> m >> S >> T;
    for (int i = 1; i <= m; i++) {
        int u, v, w, cost;
        cin >> u >> v >> w >> cost;
        add(u, v, w, cost);
    }
    
    int maxflow = 0, mincost = 0;
    while (spfa()) {
        maxflow += flow[T];
        mincost += flow[T] * dis[T];
        
        int x = T;
        while (x != S) {
            e[pre[x]].w -= flow[T];
            e[pre[x] ^ 1].w += flow[T];
            x = e[pre[x] ^ 1].to;
        }
    }
    cout << maxflow << ' ' << mincost << '\n';
    return 0;
}

经典例题

SP4063 MPIGS - Sell Pigs / P4638 [SHOI2011] 银行家题解

考虑使用网络流。

建立源点 \(S\) 和汇点 \(T\)

每个人作为一个点,将它们与汇点 \(T\) 连接,权值为需要的猪的数量。

然后对于每个人,如果和之前的某个人开了相同的猪圈,那么就将之前的那个人的点与这个人的点连接。

如果猪圈还没有被开过,就从源点 \(S\) 连接这个点,权值为猪圈猪的初始数量。

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 2510, M = 1000010, INF = 0x3f3f3f3f3f3f3f3f;

struct edge {
    int to, next, w;
} e[M];

int head[N], idx = 1;

void add(int u, int v, int w) {
    idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
    idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}

int n, m, S, T;
int dep[N];

bool bfs() {
    queue<int> q;
    q.push(S);
    memset(dep, 0x3f, sizeof(dep));
    dep[S] = 1;
    while (q.size()) {
        int t = q.front();
        q.pop();

        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (dep[to] > dep[t] + 1 && e[i].w) {
                dep[to] = dep[t] + 1;
                q.push(to);
            }
        }
    }
    if (dep[T] < INF) return true;
    else return false;
}

int dinic(int u, int limit) {
    if (u == T) return limit; 
    int rest = limit;
    for (int i = head[u]; i && rest; i = e[i].next) {
        int to = e[i].to;
        if (dep[to] == dep[u] + 1 && e[i].w) {
            int k = dinic(to, min(rest, e[i].w));
            if (!k) dep[to] = INF;
            rest -= k;
            e[i].w -= k;
            e[i ^ 1].w += k;
        }
    }
    return limit - rest;
}

int last[N], init[N];

signed main() {
    freopen("a.in", "r", stdin);

    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> m >> n;

    for (int i = 1; i <= m; i++) cin >> init[i];
    S = 0, T = n + 1;
    for (int i = 1; i <= n; i++) {
        int cnt;
        cin >> cnt;
        while (cnt--) {
            int x;
            cin >> x;
            if (last[x]) add(last[x], i, INF);
            else add(S, i, init[x]);
            last[x] = i;
        }
        int x;
        cin >> x;
        add(i, T, x);
    }

    int flow = 0, maxflow = 0;
    while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
    cout << maxflow << '\n';
    return 0;
}

P2754 [CTSC1999] 家园 / 星际转移问题题解

开始时,将源点连一条权值为 \(k\) 的边到地球。

然后以时间分层,从上一层的点连接到下一层的点,权值为飞船载人数量,并将代表月球的点连接到汇点。每加一层,在上一层的基础上进行增广,看能不能增加流量,如果流量变为 \(k\),那么证明运送完成。

可以证明枚举时间最多到 \(1500\),图的点数不超过 \(22500\),边数越大越好。

不用担心超时,luogu上最慢才 31 ms

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 22500, M = 10000010, INF = 0x3f3f3f3f3f3f3f3f;

struct edge {
    int to, next, w;
} e[M];

int head[N], idx = 1;

void add(int u, int v, int w) {
    idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
    idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}

int S, T;
int dep[N];

bool bfs() {
    queue<int> q;
    q.push(S);
    memset(dep, 0x3f, sizeof(dep));
    dep[S] = 1;
    while (q.size()) {
        int t = q.front();
        q.pop();

        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (dep[to] > dep[t] + 1 && e[i].w) {
                dep[to] = dep[t] + 1;
                q.push(to);
            }
        }
    }
    if (dep[T] < INF) return true;
    else return false;
}

int dinic(int u, int limit) {
    if (u == T) return limit; 
    int rest = limit;
    for (int i = head[u]; i && rest; i = e[i].next) {
        int to = e[i].to;
        if (dep[to] == dep[u] + 1 && e[i].w) {
            int k = dinic(to, min(rest, e[i].w));
            if (!k) dep[to] = INF;
            rest -= k;
            e[i].w -= k;
            e[i ^ 1].w += k;
        }
    }
    return limit - rest;
}

int n, m, k;
int h[N], r[N];
vector<int> a[N];

int turn(int x, int c) {
    return c * (n + 2) + x;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k;
    for (int i = 1; i <= m; i++) {
        cin >> h[i];
        cin >> r[i];
        a[i].resize(r[i]);
        for (auto& x : a[i]) {
            cin >> x;
            if (x == 0) x = n + 1;
            else if (x == -1) x = n + 2;
        }
    }
    S = 0, T = N - 1;
    add(S, turn(n + 1, 0), k);
    int maxflow = 0;
    for (int c = 1; c <= 7; c++) {
        // cout << "first" << n + 2 << ' ' << c << endl;
        add(turn(n + 2, c), T, INF);
        for (int i = 1; i <= n + 1; i++) add(turn(i, c - 1), turn(i, c), INF);
        for (int i = 1; i <= m; i++) {
            int lst = ((c - 1) % r[i] + r[i]) % r[i];
            int cur = c % r[i];
            // cout << a[i][lst] << ' ' << c - 1 << endl;
            // cout << a[i][cur] << ' ' << c << endl;
            add(turn(a[i][lst], c - 1), turn(a[i][cur], c), h[i]);
        }
        int flow = 0;
        while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
        if (maxflow == k) {
            cout << c << '\n';
            return 0;
        }
    }
    cout << 0 << '\n';
    return 0;
}

P1402 酒店之王题解

考虑使用网络流。

分为 \(6\) 层。

第一层为源点。

第二层为所有菜的点。

第三层和第四层都表示人。(限制只能选择一个)。

第五层为房子。

第六层为汇点。

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 410, M = 101000, INF = 0x3f3f3f3f3f3f3f3f;

int n, m, S, T;

struct edge {
    int to, next, w;
} e[M];

int head[N], idx = 1;

void add(int u, int v, int w) {
    // cout << u << ' ' << v << ' ' << w << endl;
    idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
    idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}

int dep[N];

bool bfs() {
    queue<int> q;
    q.push(S);
    memset(dep, 0x3f, sizeof(dep));
    dep[S] = 1;

    while (q.size()) {
        int t = q.front();
        q.pop();

        for (int i = head[t]; i; i = e[i].next) {
            int to = e[i].to;
            if (dep[to] > dep[t] + 1 && e[i].w) {
                dep[to] = dep[t] + 1;
                q.push(to);
            }
        }
    }
    return dep[T] < INF;
}

int dinic(int u, int limit) {
    if (u == T) return limit;
    int rest = limit;
    for (int i = head[u]; i && rest; i = e[i].next) {
        int to = e[i].to;
        if (dep[to] == dep[u] + 1 && e[i].w) {
            int k = dinic(to, min(rest, e[i].w));
            if (!k) dep[to] = INF;
            rest -= k;
            e[i].w -= k;
            e[i ^ 1].w += k;
        }
    }
    return limit - rest;
}

int p, q;

signed main() {
    // freopen("a.in", "r", stdin);

    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    S = 0, T = N - 1;
    cin >> n >> p >> q;
  
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= p; j++) {
            int x;
            cin >> x;
            if (x) add(j, i + 100, 1);
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= q; j++) {
            int x;
            cin >> x;
            if (x) add(i + 200, j + 300, 1);
        }
    }

    for (int i = 1; i <= 100; i++) add(i + 100, i + 200, 1);
 
    for (int i = 1; i <= 100; i++) add(S, i, 1);
    for (int i = 301; i <= 400; i++) add(i, T, 1);

    int maxflow = 0, flow = 0;
    while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
    cout << maxflow << '\n';
    return 0;
}
posted @ 2023-08-04 19:49  SunnyYuan  阅读(186)  评论(3编辑  收藏  举报