【Coel.学习笔记】最大流的拆点与判定问题

最大流的问题好多……

最大流判定问题

这类问题通常会把二分、枚举、并查集等知识和最大流相结合。

[USACO2005FEB] Secret Milking Machine

洛谷没有收录(
在一个 \(N\)\(P\) 边的无向图中从 \(1\) 到达 \(N\)\(T\) 次,要求每次走过的道路互不相同,并让走过的最长道路最短化,求出这条道路的长度。

解析:“最大值小化”通常会考虑二分,先思考一下问题是否具有单调性。
很显然,对于一个二分中确定的值 \(x\),所有大于 \(x\) 的值都不存在而小于 \(x\) 的均可存在,因此满足二分性质。

由于走过的边有限制,所以考虑使用网络流模型。由于流网络中均为有向边,所以我们要把无向边建成双向。这时可能存在走两次的问题,把双向边删除即可,这样容量限制和流量守恒也不会受到影响。

那残留网络怎么办呢?建四条边当然没问题,但我们可以利用流量相加的原理,合并成两条边。

令起点为源点,终点为汇点,求最大流,那么如果最大流大于 \(T\) 则合法,反之不合法。
代码如下:

// Problem: 秘密挤奶机
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/2279/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// Author: Coel
// 
// Powered by CP Editor (https://cpeditor.org)

#include <iostream>
#include <cstring>

const int maxn = 1e5 + 10, inf = 1e8;

using namespace std;

int n, m, K, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], val[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];

void add(int u, int v, int w) {
    nxt[cnt] = head[u], to[cnt] = v, val[cnt] = w, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, val[cnt] = w, head[v] = cnt++;
}

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, -1, sizeof(d));
    q[0] = S, d[S] = 0 , cur[S] = head[S];
    while (hh <= tt) {
        int u = q[hh++];
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i]) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                q[++tt] = v;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if (d[v] == d[u] + 1 && c[i]) {
            int t = find(v, min(c[i], limit - flow));
            if (!t) d[v] = -1;
          c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0, flow;
    while (bfs())
        while ((flow = find(S, inf)))
            ans += flow;
    return ans;
}

bool check(int mid) {
    for (int i = 0; i < cnt; i++)
        if (val[i] > mid) c[i] = 0; //删除不合法的边
        else c[i] = 1;
    return dinic() >= K; //判断最大流是否满足要求
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m >> K;
    S = 1, T = n;
    memset(head, -1, sizeof(head));
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        add(u, v, w);
    }
    int l = 1, r = 1e6;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << r;
    return 0;
}

网络流 24 题:星际转移问题

洛谷传送门
现有 \(n\) 个太空站位于地球与月球之间,且有 \(m\) 艘公共交通太空船在其间来回穿梭。每个太空站可容纳无限多的人,而太空船的容量是有限的,第 \(i\) 艘太空船只可容纳 \(h_i\) 个人。每艘太空船将周期性地停靠一系列的太空站,例如 \((1,3,4)\) 表示该太空船将周期性地停靠太空站 \(134134134\dots\)。每一艘太空船从一个太空站驶往任一太空站耗时均为 \(1\)。人们只能在太空船停靠太空站(或月球、地球)时上、下船。
初始时所有人全在地球上,太空船全在初始站。试设计一个算法,找出让所有人尽快地全部转移到月球上的运输方案。

解析:先想想怎么判定有无解。无解就意味着起点与终点不连通,用并查集判断即可。
这时会有一个隐藏问题:并查集和 dinic 都有一个 find 函数。但不必担心,因为 C++ 有函数重定向,只要函参不同就可以区分函数了。当然也可以设置不同的函数名,或者开一个名字空间。

判断有解后,按照天数构造分层图。源点向第 \(0\) 层的第一个空间站连一条容量为人数的边,汇点向每一层的最后一个空间站连一条容量正无穷的边。然后对于每一个可行的运载,连一条容量等于太空船承载量的边。此外由于空间站中可以住人,所以不同天之间的相同空间站连容量无穷大的边。

枚举进行的天数,做一遍最大流。如果天数内最大流大于等于运输人数,那么方案合法。由于网络流可以在当前图上继续增广,所以正向枚举可以在上一次枚举的基础上继续求最大流,效率甚至能够高于每次都要建图的二分。
代码如下:

// Problem: P2754 [CTSC1999]家园 / 星际转移问题
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2754
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)

#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int maxn = 1e6 + 10, maxm = 1e5 + 10, inf = 1e8;

int n, m, k, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
int fa[30];

struct node {
    int h, r, id[30];
} a[30];

int find(int x) {
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

inline int get(int i, int day) {
    return day * (n + 2) + i;
}

void add(int u, int v, int w) {
    nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, -1, sizeof(d));
    q[0] = S, d[S] = 0, cur[S] = head[S];
    while (hh <= tt) {
        int u = q[hh++];
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i]) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                q[++tt] = v;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if (d[v] == d[u] + 1 && c[i]) {
            int t = find(v, min(c[i], limit - flow));
            if (!t) d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0, flow;
    while (bfs())
        while ((flow = find(S, inf)))
            ans += flow;
    return ans;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m >> k;
    S = maxm - 2, T = maxm - 1;
    memset(head, -1, sizeof(head));
    for (int i = 0; i < 30; i++)
        fa[i] = i;
    for (int i = 1; i <= m; i++) {
        int h, r;
        cin >> h >> r;
        a[i] = {h, r};
        for (int j = 0; j < r; j++) {
            int id;
            cin >> id;
            if (id == -1) id = n + 1;
            a[i].id[j] = id;
            if (j) {
                int x = a[i].id[j - 1];
                fa[find(x)] = find(id);
            }
        }
    }
    if (find(0) != find(n + 1))
        cout << 0, exit(0);
    add(S, get(0, 0), k);
    add(get(n + 1, 0), T, inf);
    int day = 1, res = 0;
    while (true) {
        add(get(n + 1, day), T, inf);
        for (int i = 0; i <= n + 1; i++)
            add(get(i, day - 1), get(i, day), inf);
        for (int i = 1; i <= m; i++) {
            int r = a[i].r;
            int u = a[i].id[(day - 1) % r], v = a[i].id[day % r];
            add(get(u, day - 1), get(v, day), a[i].h);
        }
        res += dinic();
        if (res >= k) break;
        day++;
    }
    cout << day;
    return 0;
}

最大流拆点问题

拆点可以解决很多变式最大流问题。

[USACO07OPEN]Dining G

洛谷传送门
约翰一共烹制了 \(F\) 种食物,并提供了 \(D\) 种饮料,每种食物和饮料都只有一份。
\(N\) 头奶牛,其中第 \(i\) 头奶牛有 \(F_i\) 种喜欢的食物以及 \(D_i\) 种喜欢的饮料。
约翰需要给每头奶牛分配一种食物和一种饮料,并使得有吃有喝的奶牛数量尽可能大。

解析:这题看起来有点像二分图匹配,但实际上有三排点:食物,饮料,奶牛。

一种可能想到的方法为建立源点与食物相连,汇点与饮料相连,奶牛和喜欢的食物与饮料相连。但这时不能保证每一头奶牛都只对应一份食物与饮料,所以要改变方法。

使用拆点的方法,把奶牛拆成入点与出点,且入点与出点连容量为 \(1\) 的边,这样就可以保证解的正确性了。
代码如下:

// Problem: P2891 [USACO07OPEN]Dining G
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2891
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
// 
// Powered by CP Editor (https://cpeditor.org)

#include <cstring>
#include <iostream>

using namespace std;

const int maxn = 5e4 + 10, inf = 1e8;

int n, F, D, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];

void add(int u, int v, int w) {
    nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, -1, sizeof(d));
    q[0] = S, d[S] = 0, cur[S] = head[S];
    while (hh <= tt) {
        int u = q[hh++];
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i]) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                q[++tt] = v;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if (d[v] == d[u] + 1 && c[i]) {
            int t = find(v, min(c[i], limit - flow));
            if (!t) d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic() {
    int res = 0, flow;
    while (bfs())
        while ((flow = find(S, inf)))
            res += flow;
    return res;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> F >> D;
    S = 0, T = 2 * n + F + D + 1;
    memset(head, -1, sizeof(head));
    for (int i = 1; i <= F; i++)
        add(S, n * 2 + i, 1);
    for (int i = 1; i <= D; i++)
        add(n * 2 + F + i, T, 1);
    for (int i = 1; i <= n; i++) {
        add(i, n + i, 1);
        int x, y;
        cin >> x >> y;
        for (int j = 1; j <= x; j++) {
            int t;
            cin >> t;
            add(n * 2 + t, i, 1);
        }
        for (int j = 1; j <= y; j++) {
            int t;
            cin >> t;
            add(i + n, n * 2 + F + t, 1);
        }
    }
    cout << dinic();
    return 0;
}

网络流 24 题:最长不下降子序列问题

洛谷传送门
给定正整数序列 \(x_1 \ldots, x_n\)

  1. 计算其最长不下降子序列的长度 \(s\)
  2. 如果每个元素只允许使用一次,计算从给定的序列中最多可取出多少个长度为 \(s\) 的不下降子序列。
  3. 如果允许在取出的序列中多次使用 \(x_1\)\(x_n\)(其他元素仍然只允许使用一次),则从给定序列中最多可取出多少个不同的长度为 \(s\) 的不下降子序列。

\(a_1, a_2, \ldots, a_s\) 为构造 \(S\) 时所使用的下标,\(b_1, b_2, \ldots, b_s\) 为构造 \(T\) 时所使用的下标。且 \(\forall i \in [1,s-1]\),都有 \(a_i \lt a_{i+1}\)\(b_i \lt b_{i+1}\)。则 \(S\)\(T\) 不同,当且仅当 \(\exists i \in [1,s]\),使得 \(a_i \neq b_i\)

解析:第一问就是一个简单的动态规划问题,略过。

对于第二问,要在第一问的基础上思考。假设 \(f_i\) 可以从 \(f_j\) 转移而来,那么我们给 \(i,j\) 连边,这样就得到了一张有向图。这时,长度为 \(n\) 的路径可以一一对应上长度为 \(n\) 的 LIS。点有限制,故用拆点法。

对于第三问,把限制用的边改成正无穷即可。
代码如下:

// Problem: P2766 最长不下降子序列问题
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2766
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Coel
// 
// Powered by CP Editor (https://cpeditor.org)

#include <cstring>
#include <iostream>

const int maxn = 5e5 + 10, inf = 1e8;

using namespace std;

int n, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int q[maxn], d[maxn], cur[maxn];
int dp[maxn], w[maxn];

void add(int u, int v, int w) {
    nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, -1, sizeof(d));
    q[0] = S, d[S] = 0, cur[S] = head[S];
    while (hh <= tt) {
        int u = q[hh++];
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i]) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                q[++tt] = v;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if (d[v] == d[u] + 1 && c[i]) {
            int t = find(v, min(c[i], limit - flow));
            if (!t) d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic() {
    int res = 0, flow;
    while (bfs())
        while ((flow = find(S, inf)))
            res += flow;
    return res;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    S = 0, T = n * 2 + 1;
    memset(head, -1, sizeof(head));
    for (int i = 1; i <= n; i++)
        cin >> w[i];
    int s = 0;
    for (int i = 1; i <= n; i++) {
        add(i, i + n, 1);
        dp[i] = 1;
        for (int j = 1; j < i; j++)
            if (w[j] <= w[i])
                dp[i] = max(dp[i], dp[j] + 1);
        for (int j = 1; j < i; j++)
            if (w[j] <= w[i] && dp[j] + 1 == dp[i]) 
                add(n + j, i, 1);
        s = max(s, dp[i]);
        if (dp[i] == 1) add(S, i, 1);
    }
    for (int i = 1; i <= n; i++)
        if (dp[i] == s) add(n + i, T, 1);
    cout << s << '\n';
    if (s == 1) cout << n << '\n' << n << '\n', exit(0);
    int res = dinic();
    cout << res << '\n';
    for (int i = 0; i < cnt; i += 2) {
        int u = to[i ^ 1], v = to[i];
        if (u == S && v == 1) c[i] = inf;
        if (u == 1 && v == n + 1) c[i] = inf;
        if (u == n && v == n + n) c[i] = inf;
        if (u == n + n && v == T) c[i] = inf;
    }
    cout << res + dinic();
    return 0;
}

[POJ3498] March of the Penguins 企鹅游行

洛谷传送门

给定 \(n\) 块冰的坐标和企鹅能跳的距离 \(d\),每块冰有 \(4\) 个属性,分别为 \(x\) 坐标,\(y\) 坐标,上面原有的企鹅的数量和最多能跳出多少次,求哪些冰块可以让所有企鹅都跳到上面。

解析:到达网络流第一道黑题,认真起来吧!

先想想怎么把这题转化为流网络。建立一个源点与所有浮冰相连,容量等于浮冰起始的企鹅数。然后对于每个可以跳到的浮冰,都可以连一条边。汇点不固定,但数据范围很小 (\(1\leq N \leq 100\)),可以直接枚举每个浮冰作为汇点。

再想想怎么解决起跳次数限制的问题。根据经验,点有限制时我们可以拆点解决。对于某个点 \(u\),把这个点拆成入点 \(u_1\) 和出点 \(u_2\),然后在这两点中间连一条边,容量等于最大起跳次数。

此时,这道题就转化成枚举汇点并判断最大流是否满流,如果满流,就意味着所选择的汇点可以满足要求。

另外在枚举汇点时要注意一个小问题:每次求完最大流之后要把网络还原。我们利用残留网络的流量定义(正向边等于可以增加的流量,反向边等于可以返回的流量)来还原,也就是给正向边加上可行流,反向边设为 \(0\)

由于这题的变量很多,所以在定义变量的时候一定要明确每个变量的含义,并且防止变量重名。

代码如下:

// Problem: UVA12125 March of the Penguins
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/UVA12125
// Memory Limit: 0 MB (Are you sure?)
// Time Limit: 3000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)

#include <cctype>
#include <cmath>
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int maxn = 3e4 + 10, inf = 1e8;
const double eps = 1e-8; // 考虑浮点误差

int n, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int d[maxn], cur[maxn];
double D;

struct node {
    int x, y;
} a[maxn];

double dis(node a, node b) {
    double x = a.x - b.x, y = a.y - b.y;
    return sqrt(x * x + y * y);
}

void add(int u, int v, int w) {
    nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}

bool bfs() {
    queue<int> Q;
    memset(d, -1, sizeof(d));
    Q.push(S), d[S] = 0, cur[S] = head[S];
    while (!Q.empty()) {
        int u = Q.front();
        Q.pop();
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i]) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                Q.push(v);
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if (d[v] == d[u] + 1 && c[i]) {
            int t = find(v, min(c[i], limit - flow));
            if (!t) d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic() {
    int res = 0, flow;
    while (bfs())
        while ((flow = find(S, inf)))
            res += flow;
    return res;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int Tt;
    cin >> Tt; // Tt 为数据组数, T 为汇点
    while (Tt--) {
        memset(head, -1, sizeof(head));
        int tot = 0, Cnt = 0;  // Cnt:企鹅总数 tot:可以跳到的浮冰总数
        cnt = S = 0; //小写的 cnt 用于链式前向星
        cin >> n >> D;
        for (int i = 1; i <= n; i++) {
            int num, lim; //num 为该点企鹅数,lim 为最大跳出次数
            cin >> a[i].x >> a[i].y >> num >> lim;
            add(S, i, num), add(i, n + i, lim);
            Cnt += num;
        }
        for (int i = 1; i <= n; i++)
            for (int j = i + 1; j <= n; j++)
                if (dis(a[i], a[j]) < D + eps)
                    add(n + i, j, inf), add(n + j, i, inf);
        for (T = 1; T <= n; T++) {
            for (int i = 0; i < cnt; i += 2)
                c[i] += c[i ^ 1], c[i ^ 1] = 0; //还原网络
            if (dinic() == Cnt) {
                if (tot != 0) cout << ' ';
                cout << T - 1; //原题中浮冰编号从 0 开始,所以要 -1
                tot++;
            }
        }
        if (!tot) cout << -1;
        cout << '\n';
    }
    return 0;
}
posted @ 2022-07-12 08:53  秋泉こあい  阅读(37)  评论(0编辑  收藏  举报