二分图与网络流

二分图与网络流

二分图

定义:可以将点集划分为左部 \(A\) 和右部 \(B\) 的图,满足同一部分内没有边。

由此得到:

  • 二分图是可以被二染色的图。
  • 若二分图 \(G\) 包含 \(C\) 个连通分量,则其二染色的方案为 \(2^C\)

二分图判定

判据:一张无向图是二分图,当且仅当图中无奇环。

对点黑白染色,判断是否出现矛盾即可,时间复杂度 \(O(n + m)\)

由此可以得到一些性质:

  • 二分图的任意子图为二分图。

  • 一张无向图是二分图当且仅当其每个连通分量都是二分图。

  • 二分图中任意两点间路径边数的奇偶性确定。

二分图最大匹配

P3386 【模板】二分图最大匹配

  • 匹配:一个满足任意两边无公共点的边集。
  • 完美匹配 : 匹配数达到 \(\min(|A|, |B|)\) 称之为完美匹配。
  • 完备匹配(完全匹配):\(|A| = |B|\) 时的完美匹配。

匈牙利算法

  • 增广路:连接两个非匹配点、长度为奇数、匹配边与非匹配边交替出现的路径。

若把增广路上所有边的匹配状态取反,那么得到的新的边集仍然是一组匹配,并且匹配的边数增加 \(1\)

有结论:\(P\) 是二分图的最大匹配,当且仅当图中不存在增广路。

匈牙利算法的核心就是不断找增广路:枚举左部的一个未匹配点 \(u\) ,枚举邻域 \(v\) 尝试匹配,若当 \(v\) 点未匹配或 \(v\) 的匹配点能递归找到未匹配的右部点,则说明找到了增广路。

时间复杂度 \(O(nm)\) ,可以采用时间戳优化减小常数(用 int\(vis\) 数组,每次打上不同的标记)。

事实上匈牙利算法的复杂度可以降为 \(O(\min(A, B) m)\) ,具体就是不采用时间戳优化,只在找到增广路时清空 \(vis\) 数组,正确性显然。

可以发现匈牙利算法基于贪心原则:一旦一个点进入匹配,就不会重新成为非匹配点,因此当找不到增广路时表示 \(i\) 在保持 \(1,\ldots,i-1\) 的匹配情况不变时一定无法加入最大匹配中。由此可以解决一些字典序最小/最大的匹配问题。

bool Hungary(int u, const int tag) {
    for (int v : G.e[u])
        if (vis[v] != tag) {
            vis[v] = tag;
            
            if (!obj[v] || Hungary(obj[v], tag))
                return obj[v] = u, true;
        }
    
    return false;
}
CF1728F Fishermen

给定 \(a_{1 \sim n}\) ,将其重排后生成序列 \(b\) ,生成方法如下:

  • \(b_1 = a_1\)
  • 对于 \(i = 2, 3, \cdots, n\)\(b_i\) 是满足 \(a_i \mid b_i\)\(b_i > b_{i - 1}\) 的最小整数。

最小化 \(\sum_{i = 1}^n b_i\)

\(n \le 1000\) ,TL = 6s

考虑将问题转化为求出一组 \(c_{1 \sim n}\) ,满足 \(a_i c_i\) 互异,这样将所有 \(a_i c_i\) 排序后即可得到 \(b_i\) ,需要最小化 \(\sum_{i = 1}^n a_i c_i\) 。由抽屉原理,显然 \(c_i \le n\)

考虑建立 \(n^2\) 个点 \(a_i, 2 a_i, \cdots, n a_i\) 作为左部,\(1 \sim n\) 为右部,问题转化为最小权完美匹配问题,边权为左部点权值。

考虑贪心,按左部点权值升序找增广路,正确性:

  • \(u\) 无法匹配,则说明右部未匹配点均不与左部匹配点连边,此时若加入 \(u\) 答案一定变大。
  • \(u\) 成功匹配 \(v\) ,则 \(v\) 无法匹配 \(< u\) 的数,且匹配 \(> u\) 的数答案一定变大。

使用匈牙利算法即可做到 \(O(n^3)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int a[N], obj[N];
bool vis[N];

int n;

bool Hungary(int u) {
    for (int v : G.e[u]) {
        if (vis[v])
            continue;

        vis[v] = true;

        if (obj[v] == -1 || Hungary(obj[v]))
            return obj[v] = u, true;
    }

    return false;
}

signed main() {
    scanf("%d", &n);
    vector<int> vec;

    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i);

        for (int j = 1; j <= n; ++j)
            vec.emplace_back(a[i] * j);
    }

    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());

    for (int i = 0; i < vec.size(); ++i)
        for (int j = 1; j <= n; ++j)
            if (!(vec[i] % a[j]))
                G.insert(i, j);

    memset(obj + 1, -1, sizeof(int) * n);
    ll ans = 0;

    for (int i = 0; i < vec.size(); ++i)
        if (Hungary(i))
            ans += vec[i], memset(vis, false, sizeof(bool) * vec.size());

    printf("%lld", ans);
    return 0;
}

网络流算法

建立源点 \(S\) 和汇点 \(T\) ,左部点向源点连边,右部点向汇点连边,左右之间连边,流量均为 \(1\)

最大流即为最大匹配数,残余流量为 \(0\) 的边即为最大匹配。

使用 Dinic 算法,时间复杂度 \(O(m \sqrt{n})\)

可行边和必须边

考虑残量网络上的一个环,可以让流沿着环流一圈,而最大流不变,即最大匹配不变,因此得到:

  • \((u,v)\) 是二分图最大匹配的可行边,当且仅当它属于当前匹配或 \(u,v\) 属于 \(G'\) 中同一 SCC。
  • \((u,v)\) 是二分图最大匹配的必经边,当且仅当它属于当前匹配且 \(u,v\) 不属于 \(G'\) 中同一 SCC 。

可行点和必须点

  • 可行点:任何非孤立点都是可行点(随便选择一条出边就能找到长度至少为 \(2\) 的半增广路)。

  • 必须点:从每个未匹配点 \(x\) 开始遍历,不断走半增广路,将经过的同侧点打上标记,未被打上标记的点就是必须点。

二分图最小点覆盖

点覆盖:一个点集 \(V\) 满足对于每条边均有至少一个端点在 \(V\) 中。

Konig 定理:最小点覆盖数 = 最大匹配数。

考虑如下构造:从左部未匹配点出发找半增广路,并给经过的节点上打标记,取所有左侧未标记点和右侧已标记点构成的点集即可。

首先,该集合的大小等于最大匹配。

对于每条匹配边,两端点被标记状态相同,因此必定恰有一个点被选。

对于每条非匹配边,若左侧点是非匹配点,则必然被标记;否则右侧点是非匹配点,则必然不被标记(否则找到了一条增广路)。

其次,该集合是一个点覆盖。

如果存在一条边两端都没有选,说明其左侧是标记点,右侧是非标记点,且其必为非匹配边。但在左侧点被标记后,右侧点随后就会被标记,矛盾。

最后,不存在更小的点覆盖。

为了覆盖最大匹配的所有边,至少要有最大匹配数个点。

实际上该点集就是残量网络上最后一次广搜到的点,取出残量网络上左部不可达点和右部可达点作为一组解即可。

二分图最大独立集

独立集:一个点集 \(V\) 满足每条边至少一端不在 \(V\) 中。

结论:独立集与点覆盖互补。

构造:取最小点覆盖的补集即可。

二分图最小边覆盖

边覆盖:一个覆盖所有点的边集。

结论:若存在孤立点则无边覆盖,否则二分图最小边覆盖等于最大独立集。

最大独立集中任何两个点一定不能由一条边覆盖,因此最小边覆盖不小于最大独立集。

构造:选出所有匹配边,再对于所有未匹配点选一条出边即可。

无向图最大团

对于一张无向图 \((V,E)\)​ ,若存在一个点集 \(V'\)​,满足 \(V' \subseteq V\)​ ,且对于任意 \(u,v \in V'\)​,\((u,v) \in E\)​,则称 \(V'\)​​ 为这张无向图的一组团。

结论:无向图最大团等于补图最大独立集。

P2423 [HEOI2012]朋友圈

一张图有 \(A + B\) 个点,\(A\)\(A\) 类点,\(B\)\(B\) 类点,点有权值。

  • 两个 \(A\) 类点有边当且仅当权值异或和是奇数。
  • 两个 \(B\) 类点有边当且仅当权值异或和是偶数,或权值按位或的结果在二进制下有奇数个 \(1\)

给出若干条 \(A\) 类点和 \(B\) 类点之间的边,求最大团。

  • Subtask 1:\(A, B \le 200\)
  • Subtask 2:\(A \le 10\)\(B \le 3000\)

观察补图可以发现:

  • \(A\) 类点所有权值为奇数的点和所有权值为偶数的点各构成两个完全图。
  • \(B\) 类点所有权值为奇数的点和所有权值为偶数的点构成一个二分图。

因为无向图最大团等于补图最大独立集,所以最大团中 \(A\)​​ 最多取两个点。

枚举 \(A\) 中的选点情况,然后在 \(B\) 的补图上跑最大独立集即可做到 \(O(A^2 B^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;

struct graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }

    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

bitset<N> mate[N], permit;

int a[N], b[N], obj[N], vis[N];

int A, B, m, Tag;

bool Hungary(int u, const int tag) {
    for (int v : G.e[u]) {
        if (!permit.test(v) || vis[v] == tag)
            continue;

        vis[v] = tag;

        if (!obj[v] || Hungary(obj[v], tag))
            return obj[v] = u, true;
    }

    return false;
}

inline int solve() {
    memset(obj + 1, 0, sizeof(int) * B);
    int res = 0;

    for (int i = 1; i <= B; ++i)
        if (permit.test(i) && Hungary(i, ++Tag))
            ++res;

    return res;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &A, &B, &m);

        for (int i = 1; i <= A; ++i)
            scanf("%d", a + i), mate[i].reset();

        for (int i = 1; i <= B; ++i)
            scanf("%d", b + i);

        G.clear(B);

        for (int i = 1; i <= m; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            mate[u].set(v);
        }

        for (int i = 1; i <= B; ++i)    
            for (int j = i + 1; j <= B; ++j)
                if (((b[i] ^ b[j]) & 1) && !__builtin_parity(b[i] | b[j])) {
                    if (b[i] & 1)
                        G.insert(i, j);
                    else
                        G.insert(j, i);
                }

        permit.set();
        int ans = B - solve();

        for (int i = 1; i <= A; ++i)
            permit = mate[i], ans = max(ans, (int)permit.count() - solve() + 1);

        for (int i = 1; i <= A; ++i)
            for (int j = i + 1; j <= A; ++j)
                if ((a[i] ^ a[j]) & 1)
                    permit = mate[i] & mate[j], ans = max(ans, (int)permit.count() - solve() + 2);

        printf("%d\n", ans);
    }

    return 0;
}

DAG 最小路径点覆盖

P2764 最小路径覆盖问题

最小路径点覆盖:用最少的点不交的简单路径覆盖所有点。

将点 \(x\) 拆为 \(x\)\(x+n\) 两个点。对原图中每条边 \(u \to v\) ,在新图中连 \(u \to v+n\) 的边,所构建的新二分图称为即为原图的拆点二分图。

则可以得到:最小路径点覆盖 = 总点数 - 拆点二分图的最大匹配,一对匹配相当于合并两条路径。

若不约束点不交(DAG 最小链覆盖),则对 DAG 传递闭包,求新图的最小路径点覆盖即可。

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v + n);
    }

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        if (Hungary(i, i))
            ++ans;

    for (int i = 1; i <= n; ++i)
        if (!obj[i]) {
            for (int j = i; j; j = obj[j + n])
                printf("%d ", j);

            puts("");
        }

    printf("%d", n - ans);
    return 0;
}

DAG 最长反链

Dilworth 定理:最长反链大小等于最小链覆盖大小。

构造方案:若拆出的出入点均不属于最大匹配,则选这个点到最长反链中。

最长上升/下降子序列的结论:一个序列可以被划分为 \(LIS\)\(DS\) ,同时可以被划分为 \(LDS\)\(IS\)

P4298 [CTSC2008] 祭祀

给出一张 DAG,求最长反链,并构造一组解,并求出每个点是否能存在于最长反链中。

\(n \le 100\)\(m \le 1000\)

判定某个点能否存在于最长反链内时,直接钦定这个点在,删除与其冲突的点,再跑一次算法判断能否取到最长即可。

inline int check(int u) {
    int S = n * 2 + 1, T = n * 2 + 2;
    Dinic::reset(n * 2 + 2, S, T);
    
    for (int i = 1; i <= n; ++i) {
        ban[i] = (e[i][u] || e[u][i] || i == u);

        if (!ban[i])
            Dinic::insert(S, i, 1), Dinic::insert(i + n, T, 1);
    }
    
    for (int i = 1; i <= n; ++i)
        if (!ban[i])
            for (int j = 1; j <= n; ++j)
                if (!ban[j] && e[i][j])
                    Dinic::insert(i, j + n, 1);
    
    Dinic::solve();
    return count(ban + 1, ban + n + 1, false) - Dinic::maxflow;
}

signed main() {
    n = read(), m = read();
    
    for (int i = 1; i <= m; ++i) {
        int u = read(), v = read();
        e[u].set(v);
    }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            if (e[j][i])
                e[j] |= e[i];
    
    int S = n * 2 + 1, T = n * 2 + 2;
    Dinic::reset(n * 2 + 2, S, T);
    
    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, 1), Dinic::insert(i + n, T, 1);
    
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            if (e[i][j])
                Dinic::insert(i, j + n, 1);
    
    Dinic::solve();
    int ans = n - Dinic::maxflow;
    printf("%d\n", ans);
    
    for (int i = 1; i <= n; ++i)
        putchar((Dinic::dep[i] && !Dinic::dep[i + n]) | '0');
    
    puts("");
    
    for (int i = 1; i <= n; ++i)
        putchar((check(i) == ans - 1) | '0');
    
    return 0;
}

Hall 定理

Hall 定理:不妨设 \(|A| \le |B|\) ,记 \(N(S)\) 表示 \(S\) 的邻域,则二分图存在完美匹配当且仅当 \(\forall S, |N(S)| \ge |S|\)

必要性显然,考虑证明充分性。

若条件成立但不存在完美匹配,考虑选出左侧一个非匹配点开始增广,记访问到的左右部点集为 \(L, R\) ,由于增广失败因此终止节点均在 \(L\) 中。

考虑递归树,每个 \(L\) 中的点的父亲均为 \(R\) 中的点,则 \(|L| = |R| + 1\) ,而 \(R = N(S)\) ,矛盾。

推论:二分图最大匹配为 \(|A| - \max(|S| - |N(S)|) = \min(|A| - |S| + |N(S)|)\) ,其中 \(|S| - |N(S)|\) 即为失配点数。

建立二分图匹配的网络流模型,把 \(|X| - |S|\) 看做左侧割掉的点,\(|N(S)|\) 即为右侧割掉的点,取个 \(\min\) 就是最小割,由最小割定理得证。

P3488 [POI 2009] LYZ-Ice Skates

初始有 \(1 \sim n\) 号码溜冰鞋各 \(k\) 双, \(x\) 号脚的人可以穿 \([x, x + d]\) 号码的鞋子。

\(m\) 次操作,每次两个数 \(r, x\),表示来了 \(x\)\(r\) 号脚的人,\(x\) 为负则表示离开。

每次操作之后判断溜冰鞋是否足够。

\(n \le 2 \times 10^5\)\(m \le 5 \times 10^5\)

完美匹配的可行性不难想到 Hall 定理,则需要判断 \(\max (|S| - |N(S)|) \le 0\)

显然 \(S\) 取一段区间时 \(|S| - |N(S)|\) 会尽可能大,区间 \([l, r]\) 合法当且仅当 \(\sum_{i = l}^r cnt_i \le k \times (d + r - l + 1)\) ,即 \(\sum_{i = l}^r (cnt_i - k) \le kd\)

问题转化为动态维护最大子段和,不难用线段树做到 \(O(m \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;

int n, m, k, d;

namespace SMT {
ll sum[N << 2], ans[N << 2], lmxsum[N << 2], rmxsum[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    sum[x] = sum[ls(x)] + sum[rs(x)];
    ans[x] = max(max(ans[ls(x)], ans[rs(x)]), rmxsum[ls(x)] + lmxsum[rs(x)]);
    lmxsum[x] = max(lmxsum[ls(x)], sum[ls(x)] + lmxsum[rs(x)]);
    rmxsum[x] = max(rmxsum[rs(x)], sum[rs(x)] + rmxsum[ls(x)]);
}

void build(int x, int l, int r) {
    if (l == r) {
        sum[x] = ans[x] = lmxsum[x] = rmxsum[x] = -k;
        return;
    }

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
    pushup(x);
}

void update(int x, int nl, int nr, int p, int k) {
    if (nl == nr) {
        sum[x] += k, ans[x] += k, lmxsum[x] += k, rmxsum[x] += k;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(ls(x), nl, mid, p, k);
    else
        update(rs(x), mid + 1, nr, p, k);

    pushup(x);
} 
} // namespace SMT

signed main() {
    scanf("%d%d%d%d", &n, &m, &k, &d);
    SMT::build(1, 1, n - d);

    while (m--) {
        int r, x;
        scanf("%d%d", &r, &x);
        SMT::update(1, 1, n - d, r, x);
        puts(SMT::ans[1] <= 1ll * k * d ? "TAK" : "NIE");
    }

    return 0;
}

CF1519F Chests and Keys

\(n\) 个宝箱和 \(m\) 把钥匙,第 \(i\) 个宝箱有 \(a_i\) 元,第 \(i\) 把钥匙需要 \(b_i\) 元。

可以给每个宝箱上若干锁(可以不上锁),给第 \(i\) 个宝箱上第 \(j\) 把锁需要 \(c_{i, j}\) 元。

对手会买若干钥匙,其中钥匙和锁一一对应。对手购买钥匙后可以开宝箱,若一个宝箱上的所有锁对手均买得,则他可以打开宝箱获得钱。

求一个花费最小的上锁方案,使得无论如何买锁,获得的钱均不多于购买的钱,或报告无解。

\(n, m \le 6\)\(a_i, b_i \le 4\)

\(K_i\) 表示第 \(i\) 个宝箱上锁的集合,则对于所有宝箱集合 \(S\) ,条件转化为:

\[\sum_{i \in S} a_i \le \sum_{j \in \bigcup_{i \in S} K_i} b_j \]

可以发现这个式子很像 Hall 定理的形式,由于要最小化 \(\sum a_i\) ,考虑将原问题转化为二分图最大匹配。

将第 \(i\) 个宝箱拆为 \(a_i\) 个点,将第 \(i\) 个锁拆为 \(b_i\) 个点。若宝箱 \(i\) 上有锁 \(j\) ,则将宝箱 \(i\) 拆出的所有点向锁 \(j\) 拆出的所有点连边,得到一个二分图。其中左部点为宝箱,右部点为锁,一个宝箱 \(i\) 拆出的点与锁 \(j\) 拆出的点匹配需要花费 \(c_{i, j}\) 的代价。则限制条件为左部所有点都能匹配。

\(f_{i, s}\) 表示考虑到第 \(i\) 个宝箱,右部点未匹配的数量按五进制状压为 \(s\) 的最小花费。转移时枚举当前宝箱对应的每个点都匹配上了哪个锁拆成的点,若匹配上至少一个 \(j\) 拆出的点则花费加上 \(c_{i, j}\)

时间复杂度 \(O(n \times 5^m \times \binom{a_i + m - 1}{m - 1})\)

#include <bits/stdc++.h>
using namespace std;
const int pw[] = {0, 5, 25, 125, 625, 3125, 15625};
const int inf = 0x3f3f3f3f;
const int N = 7, S = 1.6e4 + 7;

int a[N], b[N], c[N][N], f[N][S];

int n, m;

inline int encode(const vector<int> &vec) {
    int res = 0;

    for (int i = m - 1; ~i; --i)
        res = res * 5 + vec[i];

    return res;
}

inline vector<int> decode(int res) {
    vector<int> vec;

    for (int i = 0; i < m; ++i)
        vec.emplace_back(res % 5), res /= 5;

    return vec;
}

void dfs(int p, int s, int x, int sur, int res) {
    if (x == m) {
        if (!sur)
            f[p + 1][s] = min(f[p + 1][s], res);

        return;
    }

    dfs(p, s, x + 1, sur, res);
    vector<int> vec = decode(s);

    for (int i = 1; i <= min(vec[x], sur); ++i)
        vec[x] -= i, dfs(p, encode(vec), x + 1, sur - i, res + c[p][x]), vec[x] += i;
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 0; i < n; ++i)
        scanf("%d", a + i);

    for (int i = 0; i < m; ++i)
        scanf("%d", b + i);

    if (accumulate(a, a + n, 0) > accumulate(b, b + m, 0))
        return puts("-1"), 0;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j)
            scanf("%d", c[i] + j);

    memset(f, inf, sizeof(f)), f[0][encode(vector<int>(b, b + m))] = 0;

    for (int i = 0; i < n; ++i)
        for (int s = 0; s < pw[m]; ++s)
            if (f[i][s] != inf)
                dfs(i, s, 0, a[i], f[i][s]);

    printf("%d", *min_element(f[n], f[n] + pw[m]));
    return 0;
}

P10208 [JOI 2024 Final] 礼物交换 / Gift Exchange

\(n\) 个物品,第 \(i\) 个物品有 \(a_i, b_i\) 两个权值,其中 \(b_i < a_i\)

定义 \(p_{1 \sim m}\) 的一组匹配 \(q_{1 \sim m}\) 合法当且仅当:

  • \(q_{1 \sim m}\)\(p_{1 \sim m}\) 重排后的结果。
  • \(\forall i, p_i \ne q_i\)
  • \(\forall i, a_{q_i} \ge b_{p_i}\)

\(q\) 次询问区间 \([l, r]\) 是否存在合法匹配。

\(n \le 5 \times 10^5\)\(q \le 2 \times 10^5\)

考虑建立二分图,左部点为 \(b\) ,右部点为 \(a\)\(i\) 能匹配 \(j'\) 当且仅当 \(b_i \le a_{j'}\)\(i \ne j\)

考虑 Hall 定理,则需要判定是否存在 \(S\) 满足 \(|S| > N(S)\) 。考虑 \(S\)\(a\) 最大的 \(x\) ,若不存在 \(a_y \ge b_x\) ,则 \(x\) 无法匹配。

形式化地,将 \([b_i, a_i]\) 视为线段,则存在合法匹配当且仅当对于任意线段,都存在一条其它线段与其有交。

必要性:若存在一个线段与其余线段均不交,则把它右边的线段都删去后发现它无法匹配。

充分性:考虑 \(S\)\(b\) 最小的 \(x\) ,则其邻域至少为 \(S \setminus \{ x \}\) 。又因为存在线段 \([b_y, a_y]\) 与其有交,分类讨论:

  • \(b_y \le b_x\) ,则 \(y \notin S\) ,而 \(a_y \ge b_x\) ,因此 \(y \in N(S)\)
  • \(b_x < b_y\) ,则 \(b_y \le a_x\) ,继续分类讨论:
    • \(y \notin S\) ,情况与上面一致。
    • \(y \in S\) ,则 \(x\)\(y\) 的领域。

因此 \(|N(S)| \ge |S|\) ,由 Hall 定理得证。

对于每条线段,找到 \(L_i, R_i\) 表示最近的与其有交的线段,则 \(L_i < l \le i \le r < R_i\) 的区间 \([l, r]\) 均非法。

问题转化为二维数点,不难离线扫描线做到 \(O((n + q) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

vector<pair<int, int> > upd[N], qry[N];

int a[N], b[N], L[N], R[N], ans[N];

int n, q;

namespace SMT {
int mn[N << 2], mx[N << 2], cov[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void spread(int x, int k) {
    mn[x] = mx[x] = cov[x] = k;
}

inline void pushdown(int x) {
    if (~cov[x])
        spread(ls(x), cov[x]), spread(rs(x), cov[x]), cov[x] = -1;
}

void build(int x, int l, int r) {
    mn[x] = n + 1, mx[x] = 0, cov[x] = -1;

    if (l == r)
        return;

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);

    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);

    mn[x] = min(mn[ls(x)], mn[rs(x)]), mx[x] = max(mx[ls(x)], mx[rs(x)]);
}

int querymin(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return mn[x];

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return querymin(ls(x), nl, mid, l, r);
    else if (l > mid)
        return querymin(rs(x), mid + 1, nr, l, r);
    else
        return min(querymin(ls(x), nl, mid, l, r), querymin(rs(x), mid + 1, nr, l, r));
}

int querymax(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return mx[x];

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return querymax(ls(x), nl, mid, l, r);
    else if (l > mid)
        return querymax(rs(x), mid + 1, nr, l, r);
    else
        return max(querymax(ls(x), nl, mid, l, r), querymax(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT

namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x; x -= x & -x)
        c[x] += k;
}

inline int query(int x) {
    int res = 0;

    for (; x <= n; x += x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= n; ++i)
        scanf("%d", b + i);

    SMT::build(1, 1, n * 2);

    for (int i = 1; i <= n; ++i)
        L[i] = SMT::querymax(1, 1, n * 2, b[i], a[i]), SMT::update(1, 1, n * 2, b[i], a[i], i);

    SMT::build(1, 1, n * 2);

    for (int i = n; i; --i)
        R[i] = SMT::querymin(1, 1, n * 2, b[i], a[i]), SMT::update(1, 1, n * 2, b[i], a[i], i);

    for (int i = 1; i <= n; ++i) {
        upd[i].emplace_back(L[i], 1), upd[i].emplace_back(i, -1);
        upd[R[i]].emplace_back(L[i], -1), upd[R[i]].emplace_back(i, 1);
    }

    scanf("%d", &q);

    for (int i = 1; i <= q; ++i) {
        int l, r;
        scanf("%d%d", &l, &r);
        qry[r].emplace_back(l, i);
    }

    for (int i = 1; i <= n; ++i) {
        for (auto it : upd[i])
            BIT::update(it.first, it.second);

        for (auto it : qry[i])
            ans[it.second] = !BIT::query(it.first);
    }

    for (int i = 1; i <= q; ++i)
        puts(ans[i] ? "Yes" : "No");

    return 0;
}

二分图边染色

CF600F Edge coloring of bipartite graph

结论:二分图的最小边染色数等于点的最大度数。

下界是显然的,上界可以通过构造证明。

考虑不断向图中加入边 \((u, v)\) ,记 \(u\) 出边的颜色集合为 \(E_u\)

\(\mathrm{mex}(E_u) = \mathrm{mex}(E_v)\) ,则直接染上 \(\mathrm{mex}\) 即可。

否则不妨设 \(\mathrm{mex}(E_u) < \mathrm{mex}(E_v)\) ,考虑强制让这条边染 \(\mathrm{mex}(E_u)\) ,但是这样在 \(E_v\) 中会冲突。找到冲突的那条边 \((v, x)\) ,将其染上 \(\mathrm{mex}(E_v)\) ,但是可能还会冲突。不断做类似的操作直至不冲突为止,由于是二分图因此一定会走到底。

时间复杂度 \(O((a + b) m) = O(nm)\) 。存在重边时复杂度分析会出现问题,原因在于求 \(\mathrm{mex}\) 的复杂度退化为 \(O(m)\) 而非 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7, M = 2e5 + 7;

struct Edge {
    int u, v;
} e[M];

int deg[N], E[N][N];

int a, b, m;

signed main() {
    scanf("%d%d%d", &a, &b, &m);

    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &e[i].u, &e[i].v), ++deg[e[i].u], ++deg[e[i].v += a];
    
    int ans = *max_element(deg + 1, deg + 1 + a + b);
    printf("%d\n", ans);

    for (int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v, x = 1, y = 1;
        
        while (E[u][x])
            ++x;
        
        while (E[v][y])
            ++y;
        
        E[u][x] = v, E[v][y] = u;

        if (x != y) {
            for (int w = v, j = y; w; w = E[w][j], j ^= x ^ y)
                swap(E[w][x], E[w][y]);
        }
    }

    for (int i = 1; i <= m; ++i)
        printf("%d ", find(E[e[i].u] + 1, E[e[i].u] + ans + 1, e[i].v) - E[e[i].u]);

    return 0;
}

P10062 [SNOI2024] 拉丁方

定义一个 \(n \times n\) 的矩阵为拉丁方,当且仅当每行每列都是一个 \(1 \sim n\) 的排列。

给定一个 \(n \times n\) 矩阵左上角 \(r \times c\) 的子矩阵,构造一个合法的 \(n \times n\) 的拉丁方矩阵,或报告无解。

\(n \le 500\) ,保证 \(r \times c\) 的子矩阵不存在一行或者一列有两个相同的数

先考虑 \(r = n\)\(c = n\) 的情况,此时直接跑二分图边染色即可,一定有解。

再考虑 \(r, c < n\) 的情况,记 \(cnt_x\) 表示 \(x\)\(r \times c\) 的子矩阵中的出现次数。若 \(cnt_x + (n - r) + (n - c) < n\) 则无解,否则可以构造证明一定有解。

考虑先把 \(r \times c\) 补全为 \(r \times n\) ,建立二分图模型,左部点为前 \(r\) 行,右部点为还没填的数,则左部点的度数均为 \(n - c\) ,右部点度数均 \(\le n - c\) ,类似的求出一组边染色即可。

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7;

int a[N][N];

int n, r, c;

namespace Solver {
int e[N << 1][N << 1];

int a, b;

inline void prework(int _a, int _b) {
    a = _a, b = _b;

    for (int i = 1; i <= a + b; ++i)
        memset(e[i] + 1, 0, sizeof(int) * (a + b));
}

inline void insert(int u, int v) {
    int x = 1, y = 1;

    while (e[u][x])
        ++x;

    while (e[v][y])
        ++y;
    
    e[u][x] = v, e[v][y] = u;

    if (x != y) {
        for (int w = v, j = y; w; w = e[w][j], j ^= x ^ y)
            swap(e[w][x], e[w][y]);
    }
}
} // namespace Solver

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &r, &c);
        vector<int> cnt(n + 1);

        for (int i = 1; i <= r; ++i)
            for (int j = 1; j <= c; ++j)
                scanf("%d", a[i] + j), ++cnt[a[i][j]];

        if (*min_element(cnt.begin() + 1, cnt.end()) + (n - r) + (n - c) < n) {
            puts("No");
            continue;
        }

        Solver::prework(r, n);

        for (int i = 1; i <= r; ++i) {
            vector<int> vis(n + 1);

            for (int j = 1; j <= c; ++j)
                vis[a[i][j]] = 1;

            for (int j = 1; j <= n; ++j)
                if (!vis[j])
                    Solver::insert(i, r + j);
        }

        for (int i = 1; i <= r; ++i)
            for (int j = 1; j <= n - c; ++j)
                a[i][c + j] = Solver::e[i][j] - r;

        Solver::prework(n, n);

        for (int i = 1; i <= n; ++i) {
            vector<int> vis(n + 1);

            for (int j = 1; j <= r; ++j)
                vis[a[j][i]] = 1;

            for (int j = 1; j <= n; ++j)
                if (!vis[j])
                    Solver::insert(i, n + j);
        }

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n - r; ++j)
                a[r + j][i] = Solver::e[i][j] - n;

        puts("Yes");

        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j)
                printf("%d ", a[i][j]);

            puts("");
        }
    }

    return 0;
}

正则二分图匹配

QOJ265. 正则二分图匹配

\(k\) -正则二分图:每个点度数均为 \(k\) 的二分图,用 Hall 定理可以证明其一定存在完美匹配。

\(k = 2^d\) 时有一个做法:直接找出一条欧拉回路,这样就给所有边定了向;且每个点出度入度相同。删掉某一个方向的所有边,然后忽略掉定向,就变成了 \(2^{d - 1}\) -正则二分图,递归直到 \(d = 0\) 即可,时间复杂度 \(O(nk)\)

一般情况考虑随机化,每次随机选一个左边的未匹配点,然后沿增广路随机游走,直到走到一个右边的非匹配点为止。再把走出来的环去掉,具体就是找到最后一个出现过多次的点,然后把第一次走到它到最后一次走到它中间的这段路砍掉。这样就找到了一条增广路,匹配数加一。

可以证明该做法的期望时间复杂度为 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;

int to[N], match[N], ans[N];
bool vis[N];

mt19937 myrand(time(0));
int n, d;

signed main() {
    scanf("%d%d", &n, &d);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= d; ++j)
            scanf("%d", to + (i - 1) * d + j);

    vector<int> id(n);
    iota(id.begin(), id.end(), 1), shuffle(id.begin(), id.end(), myrand);

    for (int x : id) {
        vector<int> path;
        int u = x;

        while (u) {
            int v = 0;

            do
                v = to[(u - 1) * d + myrand() % d + 1];
            while (match[v] == u);

            u = match[v];

            if (!vis[v])
                vis[v] = true, path.emplace_back(v);
            else {
                while (path.back() != v)
                    vis[path.back()] = false, path.pop_back();
            }
        }

        u = x;

        for (int it : path)
            vis[it] = false, swap(match[it], u);
    }

    for (int i = 1; i <= n; ++i)
        ans[match[i]] = i;

    for (int i = 1; i <= n; ++i)
        printf("%d ", ans[i]);

    return 0;
}

正则二分图的边染色可以用上述算法优化:

  • \(2 \mid k\) 时用欧拉回路递归成两个 \(\frac{k}{2}\) 的子问题。
  • \(2 \nmid k\) 时跑随机算法找到一组完美匹配并删去。

时间复杂度 \(T(k) = 2 T(\frac{k}{2}) + O(nk + n \log n) = O(nk \log k)\)

如果偷懒每次都用随机化算法,复杂度会退化为 \(O(nk^2)\) ,瓶颈在于删边,在 \(n\) 比较大时比暴力边染色优秀。

QOJ10045. Permutation Recovery

有一个 \(2k \times n\) 的矩阵,其中每一行均为 \(1 \sim n\) 的排列,并且对于 \(1 \le i \le k\) ,第 \(2i - 1\) 行和第 \(2i\) 行互为逆排列。

现在将每一列都打乱,构造一种可能的原矩阵,对于 \(1 \le i \le k\) 依次输出第 \(2i - 1\) 行的排列,或报告无解。

\(k \le 7\)\(n \le 40000\)

首先可以发现,一个排列中置换环的所有边,在其逆排列中依旧存在,并且方向相反。

如果存在一条边,那么这条环边的两个方向都要有边。即如果存在一个排列第 \(i\) 个位置为 \(j\) ,则需要消耗一个第 \(i\) 列的 \(j\) 和一个第 \(j\) 列的 \(i\)

考虑对每个数为点建图,记 \(cnt_{i, j}\) 表示第 \(i\)\(j\) 的出现次数,此时若 \(cnt_{i, j} \ne cnt_{j, i}\)\(2 \nmid cnt_{i, i}\) 则无解。否则对于 \(i \ne j\)\(cnt_{i, j}\)\((i, j)\) 边和 \((j, i)\) 边,再连 \(cnt_{i, i}\)\((i, i)\) 的自环。

注意到图中每个点的度数都是 \(2k\) ,因此可以用若干条欧拉回路覆盖整个图。找到这些欧拉回路,而每个排列的置换环都从中产生,问题转化为选一些置换环覆盖整个点集的。

由于此时每个点都有 \(k\) 条入边和 \(k\) 条出边,因此考虑拆点。对于一条欧拉回路上的有向边 \(u \to v\) ,考虑连边 \(u^{out} \to v^{in}\) ,此时形成了一个二分图,其中每个点的度数都是 \(k\) ,跑正则二分图边染色即可。

由于数据范围不大,直接每次跑随机化算法即可做到 \(O(nk \log(nk) + nk^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 4e4 + 7, M = 15;

struct Graph {
    struct Edge {
        int nxt, v;
        bool vis;
    } e[N * M];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge){head[u], v, false}, head[u] = tot;
    }
} G;

map<int, int> mp[N];
vector<int> e[N];

int a[M][N];

mt19937 myrand(time(0));
int n, m;

void Hierholzer(int u) {
    for (int &i = G.head[u]; i; i = G.e[i].nxt)
        if (!G.e[i].vis)
            e[u].emplace_back(G.e[i].v), G.e[i].vis = G.e[i ^ 1].vis = true, Hierholzer(G.e[i].v);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m * 2; ++i)
        for (int j = 1; j <= n; ++j)
            scanf("%d", a[i] + j), ++mp[j][a[i][j]];

    for (int i = 1; i <= n; ++i)
        for (auto it : mp[i]) {
            int j = it.first, cnt = it.second;

            if (j == i) {
                if (cnt & 1)
                    return puts("-1"), 0;

                for (int k = 1; k <= cnt / 2; ++k)
                    G.insert(i, i), G.insert(i, i);
            } else if (j > i) {
                if (cnt != mp[j][i])
                    return puts("-1"), 0;

                for (int k = 1; k <= cnt; ++k)
                    G.insert(i, j), G.insert(j, i);
            }
        }

    for (int i = 1; i <= n; ++i)
        Hierholzer(i);

    while (m--) {
        vector<int> id(n), vis(n + 1), match(n + 1);
        iota(id.begin(), id.end(), 1), shuffle(id.begin(), id.end(), myrand);

        for (int x : id) {
            vector<int> path;
            int u = x;

            while (u) {
                int v = 0;

                do
                    v = e[u][myrand() % e[u].size()];
                while (match[v] == u);

                u = match[v];

                if (!vis[v])
                    vis[v] = 1, path.emplace_back(v);
                else {
                    while (path.back() != v)
                        vis[path.back()] = 0, path.pop_back();
                }
            }

            u = x;

            for (int it : path)
                vis[it] = 0, swap(match[it], u);
        }

        for (int i = 1; i <= n; ++i)
            printf("%d ", match[i]), e[match[i]].erase(find(e[match[i]].begin(), e[match[i]].end(), i));

        puts("");
    }

    return 0;
}

网络流

一个网络 \(G = (V, E)\) 是一张有向图,有源点 \(S\) 与汇点 \(T\) ,每条有向边 \((x, y) \in E\) 都有一个容量 \(c[x, y]\)

一个合法的流 \(f(x, y)\) 满足:

  • 容量限制: \(f(x, y) \le c[x, y]\)
  • 反对称性: \(f(x, y) = -f(y, x)\)
  • 流量守恒: \(\forall x \not = S \and x \not = T, \sum_{(u, x) \in E} f(u, x) = \sum_{(x, v) \in E} f(x, v)\)

\(r[x, y] = f(x, y) - c[x, y]\) 为残量网络,残量网络中 \(S \to T\) 的一条 \(r > 0\) 路径称为增广路。

Dinic 算法

P3376 【模板】网络最大流

反复寻找增广路,更新残量网络,直到找不到为止,此时得到的就是最大流。需要引入反向边满足撤销操作。

流程:

  • BFS 分层:在残量网络上广搜求出每个节点的层次,构造分层图
  • DFS 增广:在分层图上深搜找增广路,回溯时实时更新剩余容量

优化:

  • 当前弧优化:引入 \(cur\) 数组,表示上一次走邻接表走到的边,这样每次只要从 \(cur\) 开始走,就不会走重复的边。类

  • 点优化:假如从一个点流不出流量,则打上标记不走。

  • 不完全 BFS 优化:BFS 到 \(T\) 后直接停止。

时间复杂度 \(O(n^2 m)\) ,实际很松。

namespace Dinic {
struct Edge {
    int nxt, v, f;
} e[M << 1];

int head[N], cur[N], dep[N];
bool vis[N];

int n, S, T, tot, maxflow;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1, maxflow = 0;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f) {
    e[++tot] = (Edge){head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, 0}, head[v] = tot;
}

inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v;

            if (e[i].f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }

    return dep[T];
}

int dfs(int u, int flow) {
    if (u == T)
        return flow;

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;

        if (f && dep[v] == dep[u] + 1 && !vis[v]) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow)
                return vis[u] = false, outflow;
        }
    }

    return outflow;
}

inline int solve() {
    while (bfs())
        maxflow += dfs(S, inf);

    return maxflow;
}
} // namespace Dinic

最小割

有源汇割:一个边权和最小的边集,删去之后使得源汇不连通。

定理:最大流 = 最小割

证明:

  • \(最大流 \le 最小割\) :首先根据割的定义,所有的流都必然经过割边集中的某一条边,那么流量总和最大就是割边集总和。
  • \(最大流 \ge 最小割\) :考虑我们求出了一个最大流,那么某些边会成为瓶颈,即残量网络上为 \(0\) ,这些边一定分布成为一个割,否则仍然会有增广路。

构造:在残量网络上称 \(S\) 遍历到的点称为 \(S\) 集,一端在 \(S\) 集一端不在的边即为一组解。

可行边和必须边

P4126 [AHOI2009] 最小割

不难发现可行边即为所有割集的并,必须边即为所有割集的交。

首先有可行边和必须边必须满流,考虑现有的满流边 \((u, v)\) 如何被替代。若残量网络中存在包含 \(u, v\) 的环,让流沿着环流动一圈,最大流不变,但是满流被破坏。也就是 \((u, v)\) 边不会是瓶颈,于是残量网络上两个端点在同一 SCC 内的边必然总不是最小割。

将当前残量网络缩点,DAG 上的边才有可能成为最小割。在这些边里面,直接将 \(S, T\) 相连的边就是必须边。对于其他边都能分别够构造割与不割的方案,它们是可行边。

在这个 DAG 上,每一种紧的割(不考虑权值)都是最小割。

左端点:靠近 \(S\) 的一端;右端点:靠近 \(T\) 的一端。

  • 割的构造:把这条边左端点到 \(S\) 的路径钦定为 \(S\) 集合,其余为 \(T\) 集合,然后把所有 \(S, T\) 之间的边割断,这是紧的,而且该边是最小割的一部分。
  • 不割的构造:如果右端点不是 \(T\) ,把这条边右端点到 \(S\) 的路径钦定为 \(S\) 集合。否则左端点必然不是 \(S\) ,把这条边左端点到 \(T\) 的路径钦定为 \(T\) 集合即可。这样整条边总是被完整地包含在 \(S\)\(T\) 集中。

具体实现的细节:

  • 可行边:满流,两端不在一个强连通分量内。
  • 必须边:满流,一端在 \(S\) 的 SCC 内,另一端在 \(T\) 的 SCC 内。

费用流

P3381 【模板】最小费用最大流

  • 费用流:给定一个网络,每条边除了有容量限制还有一个费用。
  • 最小费用最大流:该网格中总花费最小的最大流被称为最小费用最大流。
  • 最大费用最大流:该网格中总花费最大的最大流被称为最大费用最大流。
    • 实现时把费用设为负数,跑最小费用最大流,再取相反数即可。

通常采用 Dinic 算法,只要把广搜改为 SPFA 即可,边权为每条边的费用。

namespace Dinic {
struct Edge {
    int nxt, v, f, c;
} e[M << 1];

int head[N], cur[N], dis[N];
bool vis[N], inque[N];

int n, S, T, tot, maxflow, mincost;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1, maxflow = mincost = 0;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f, int c) {
    e[++tot] = (Edge){head[u], v, f, c}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, 0, -c}, head[v] = tot;
}

inline bool SPFA() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dis + 1, inf, sizeof(int) * n);
    queue<int> q;
    dis[S] = 0, q.emplace(S), inque[S] = true;
    
    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;
        
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, c = e[i].c;
            
            if (e[i].f && dis[v] > dis[u] + c) {
                dis[v] = dis[u] + c;
                
                if (!inque[v])
                    q.emplace(v), inque[v] = true;
            }
        }
    }
    
    return dis[T] != inf;
}

int dfs(int u, int flow) {
    if (u == T) {
        mincost += flow * dis[T];
        return flow;
    }
    
    vis[u] = true;
    int outflow = 0;
    
    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f, c = e[i].c;
        
        if (f && !vis[v] && dis[v] == dis[u] + c) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;
            
            if (outflow == flow)
                return vis[u] = false, outflow;
        }
    }
    
    return outflow;
}

inline void solve() {
    while (SPFA())
        maxflow += dfs(S, inf);
}
} // namespace Dinic

上下界网络流

上下界限制:第 \(i\) 条边 \((x_i, y_i)\) 的流量介于 \([l_i, r_i]\) 之间,并且整个网络满足流量守恒。

无源汇上下界可行流

LOJ115. 无源汇有上下界可行流

因为每条边都有下界,先令每条边流 \(l\) ,称之为“初始流”。

此后,每条边还能在不超过上界的情况下继续流,可以构造一个差网络,令边权为 \(r - l\) ,为能额外流的流量,最终在差网络上的流量称作附加流。

初始流不一定满足流平衡,要通过适当地调整差网络使得两个网络的流量加起来平衡,这样我们就构造出了一个可行的循环流。

现在问题转化为一个差网络上的流问题。只是每个点并不是要求流量守恒,而是要求流量等于给定的数好与初始网络抵消。我们查看每个点的初始流量代数和 \(s\)

  • 如果是正数,则在差网络上需要负数的流。而源点具有负数的流,所以这个点要当源点。
  • 如果是负数,则在差网络上需要正数的流。而汇点具有正数的流,所以这个点要当汇点。

多源多汇钦定流量,只需要采用超级源超级汇即可。在这个网络上跑一次普通的有源汇最大流,查看每条源汇边是否流满,都满了则有解。原网络的流量就相当于初始流加上附加流。

namespace NSTFlow {
int d[N];

int n, S, T;

inline void prework(int _n) {
    n = _n, S = n + 1, T = n + 2;
    memset(d + 1, 0, sizeof(int) * n);
    Dinic::prework(n + 2, S, T);
}

inline void insert(int u, int v, int l, int r) {
    Dinic::insert(u, v, r - l);
    d[v] += l, d[u] -= l;
}

inline bool solve() {
    int s = 0;

    for (int i = 1; i <= n; ++i) {
        if (d[i] > 0)
            Dinic::insert(S, i, d[i]), s += d[i];
        else if (d[i] < 0)
            Dinic::insert(i, T, -d[i]);
    }

    return Dinic::solve() == s;
}
} // namespace NSTFlow

有源汇上下界可行流

\(T\)\(S\) 连一条上下界为 \([0, +\infty]\) 的边,把 \(T\) 流入的流量转移给 \(S\) ,转化为无源汇上下界可行流求解。

有源汇上下界最大流

LOJ116. 有源汇有上下界最大流 / P5192 【模板】有源汇上下界最大流

\(T\)\(S\) 连一条上下界为 \([0, +\infty]\) 的边,转化为无源汇网络,先流一次可行流满足流量守恒,然后在差网络上跑最大流即可。

namespace YSTMaxFlow {
int n, S, T;

inline void prework(int _n, int _S, int _T) {
	n = _n, S = _S, T = _T;
	NSTFlow::prework(n);
}

inline void insert(int u, int v, int l, int r) {
	NSTFlow::insert(u, v, l, r);
}

inline int solve() {
	NSTFlow::insert(T, S, 0, inf);

	if (!NSTFlow::solve())
		return -1;

	Dinic::S = S, Dinic::T = T, Dinic::maxflow = 0;
	return Dinic::solve();
}
} // namespace YSTMaxFlow

有源汇上下界最小流

LOJ117. 有源汇有上下界最小流

用类似有源汇上下界可行流的构图方法,但先不添加 \(T\)\(S\) 的边,先求一次超级源到超级汇的最大流,尽可能填充循环流以减小最小流的代价。

然后再从 \(T\)\(S\) 连一条上下界为 \([0, +\infty]\) 的边,在残量网络上再求一次超级源到超级汇的最大流,流经 \(T\)\(S\) 的边的流量就是最小流的值。

namespace YSTMinFlow {
int n, S, T;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T;
    NSTFlow::prework(n);
}

inline void insert(int u, int v, int l, int r) {
    NSTFlow::insert(u, v, l, r);
}

inline int solve() {
    NSTFlow::solve();
    NSTFlow::insert(T, S, 0, inf);
    Dinic::solve(); // 不清空 Dinic::maxflow
    return Dinic::maxflow == NSTFlow::s ? Dinic::e[Dinic::tot].f : -1;
}
} // YSTMinFlow

上下界最小费用可行流

类似上下界可行流,求最大流改为求最小费用最大流。注意要预先加上初始流的费用,即初始流的费用减去从汇点到源点的最大流。

但是无源汇的题目很有可能产生负环,需要用到带负环的费用流。

有源汇上下界最小费用流

关于有源汇上下界最小费用可行流,就是连边 \(t\to s\) 后变为无源汇情况,和之前的转化是一样的。

而如果是最小费用最大流,其实也一样,跑完 \(S\to T\) 的最小费用最大流后再跑 \(s\to t\) 的最小费用最大流即可。

这里最大流改成最小流,最小费用改成最大费用也都一样,这里不再赘述。

有负圈的费用流

P7173 【模板】有负圈的费用流

对于网络中负的费用边 \((x, y)\) ,我们先让其满流,然后加入边 \((y, x)\) ,费用为原来费用的相反数,用于退流。

满流直接用上下界费用流的技术解决,跑一个有源汇上下界最小费用最大流即可。

namespace NCFlow {
int d[N];

int n, S, T;

inline void reset(int _n, int _S, int _T) {
    n = _n + 2, S = _S, T = _T;
    Dinic::reset(n + 2, S, T);
    Dinic::insert(T, S, inf, 0);
    memset(d + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f, int c) {
    if (c >= 0)
        Dinic::insert(u, v, f, c);
    else {
        Dinic::insert(v, u, f, -c);
        d[u] -= f, d[v] += f;
        Dinic::mincost += f * c;
    }
}

inline void solve() {
    int _S = n + 1, _T = n + 2;
    
    for (int i = 1; i <= n; ++i) {
        if (d[i] > 0)
            Dinic::insert(_S, i, d[i], 0);
        else if (d[i] < 0)
            Dinic::insert(i, _T, -d[i], 0);
    }
    
    Dinic::S = _S, Dinic::T = _T;
    Dinic::solve();
    Dinic::S = S, Dinic::T = T, Dinic::maxflow = 0;
    Dinic::solve();
}
} // namespace NCFlow

最小割树

P4897 【模板】最小割树(Gomory-Hu Tree)

对于 GHT 上的一条边 \((u, v)\) ,去掉这条边之后最小割树上的两棵子树为原图中去掉 \((u, v)\) 的最小割的两个点集。

构建 GHT 可以考虑分治处理,先求出最小割后连边,根据残余网络连通性将图分成两个点集继续考虑。

void solve(int l, int r) {
	if (l == r)
		return;
	
	int res = 0;
	
	while (bfs(a[l], a[r]))
		res += dfs(a[l], a[r], inf);
	
	G.insert(a[l], a[r], res), G.insert(a[r], a[l], res);
	sort(a + l, a + r + 1, [](const int &x, const int &y) { return dep[x] < dep[y]; });
	int cut;
	
	for (int i = l; i <= r; ++i)
		if (dep[a[i]]) {
			cut = i;
			break;
		}
	
	for (int i = 2; i <= tot; ++i)
		e[i].f = e[i].orif;
	
	solve(l, cut - 1), solve(cut, r);
}

性质:两个点之间的最小割为最小割树上的两点之间边权最小值。

证明:设 \(f(x, y)\) 为原图上 \(x \to y\) 的最小割

定理一:\(\forall q \in V_x, p \in V_y, f(x, y) \ge f(q, p)\)

反证法,假设 \(f(x, y) < f(q, p)\) ,那么割掉 \(x \to y\) 的最小割 \(p, q\) 仍然联通,则 \(x \to q \to p \to y\) ,显然与最小割矛盾。

定理二:\(\forall z, f(x, y) \ge \min(f(x, z), f(z, y))\)

因为最小割等于最大流,所以根据最大流的性质这是显然的。

推论:对于一个排列 \(p_{1 \sim k}\) ,存在:

\[f(x, y) \ge \min \{ f(x, p_1), f(p_1, p_2), \cdots, f(p_k, y) \} \]

假设在最小割树上 \((x, y)\) 之间的路径权值最小值的为 \((p, q)\) 这条边,那么,根据定理二推论可以得到 \(f(x, y) \ge f(p, q)\) 。又根据最小割树性质可以得到 \(x, y\)\(p, q\) 两旁,所以根据定理一可以得到 \(f(p, q) \ge f(x, y)\)

综上,\(f(p, q) = f(x, y)\)

网络流建模应用

最大流

多源多汇

如果一道题中有多个可行的源点 \(s_1,\ldots,s_a\) 和多个可行的汇点 \(t_1,\ldots,t_b\),那么可以建立超级源汇 \(S,T\),从 \(S\)\(s_i\) 连容量无穷的边,\(t_i\)\(T\) 连容量无穷的边,转化成单源汇的问题。

P2472 [SCOI2007] 蜥蜴

一个 \(r \times c\) 的矩阵中有若干石柱,每个石柱有高度 \(h_{i, j}\)

有一些蜥蜴站在石柱上,蜥蜴可以跳到欧几里得距离不超过 \(d\) 的地方。

蜥蜴每次起跳,脚下的石柱都会降低 \(1\) ,高度为 \(0\) 则石柱消失。

求最多能有多少条蜥蜴能够跳出边界。

\(r, c \le 20\)\(d \le 4\)\(h \le 3\)

把行动视作流,高度视为流量限制(拆点即可),转化为多源多汇建模。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e1 + 7;

char h[N][N], a[N][N];

int n, m, d;

namespace Dinic {
const int N = 1e3 + 7, M = 1e5 + 7;

struct Edge {
    int nxt, v, f;
} e[M << 1];
 
int head[N], cur[N], dep[N];
bool vis[N];
 
int n, S, T, tot, maxflow;
 
inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1, maxflow = 0;
    memset(head + 1, 0, sizeof(int) * n);
}
 
inline void insert(int u, int v, int f) {
    e[++tot] = (Edge) {head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge) {head[v], u, 0}, head[v] = tot;
}
 
inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);
 
    while (!q.empty()) {
        int u = q.front();
        q.pop();
 
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v;
 
            if (e[i].f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }
 
    return dep[T];
}
 
int dfs(int u, int flow) {
    if (u == T)
        return flow;
 
    vis[u] = true;
    int outflow = 0;
 
    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;
 
        if (f && dep[v] == dep[u] + 1 && !vis[v]) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;
 
            if (outflow == flow)
                break;
        }
    }
 
    if (outflow == flow)
        vis[u] = false;
 
    return outflow;
}
 
inline int solve() {
    while (bfs())
        maxflow += dfs(S, inf);
 
    return maxflow;
}
} // namespace Dinic

signed main() {
    scanf("%d%d%d", &n, &m, &d);

    for (int i = 1; i <= n; ++i)
        scanf("%s", h[i] + 1);

    for (int i = 1; i <= n; ++i)
        scanf("%s", a[i] + 1);

    int S = n * m * 2 + 1, T = S + 1;
    Dinic::prework(T, S, T);
    int cnt = 0;

    auto getid = [](int i, int j, int k) {
        return (i - 1) * m + j + k * n * m;
    };

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if (a[i][j] == 'L')
                ++cnt, Dinic::insert(S, getid(i, j, 0), 1);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            Dinic::insert(getid(i, j, 0), getid(i, j, 1), h[i][j] & 15);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            for (int x = max(i - d, 1); x <= min(i + d, n); ++x)
                for (int y = max(j - d, 1); y <= min(j + d, m); ++y)
                    if ((x - i) * (x - i) + (y - j) * (y - j) <= d * d)
                        Dinic::insert(getid(i, j, 1), getid(x, y, 0), inf);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if (i - d < 1 || i + d > n || j - d < 1 || j + d > m)
                Dinic::insert(getid(i, j, 1), T, inf);

    printf("%d", cnt - Dinic::solve());
    return 0;
}

拆点法(点转边)

常见的情形是限制了一个点的流量上限,考虑将其拆成两个点,分别称作入点和出点。连入的边统一连到入点,连出的边统一从出点连出,随后在入点和出点中间连一条流量上限的边即可。

P1402 酒店之王

\(n\)\(A\) 类节点,\(p\)\(B\) 类节点, \(q\)\(C\) 类节点。每个 \(A\) 与一个 \(B\) 和一个 \(C\) 构成一组匹配(每个 \(A\) 只能与给定的 \(B\)\(C\) 匹配,且每个 \(B\)\(C\) 只能匹配一个 \(A\) ),求最大匹配数。

\(n, p, q \le 100\)

一个很自然的想法是建立 \(S \to B \to A \to C \to T\) 的模型,但是每个 \(A\) 只能匹配一次,拆点即可。

signed main() {
    n = read(), p = read(), q = read();
    int S = p + n * 2 + q + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= p; ++i)
        Dinic::insert(S, i, 1);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= p; ++j)
            if (read())
                Dinic::insert(j, p + i, 1);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(p + i, p + n + i, 1);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= q; ++j)
            if (read())
                Dinic::insert(p + n + i, p + n * 2 + j, 1);

    for (int i = 1; i <= q; ++i)
        Dinic::insert(p + n * 2 + i, T, 1);

    printf("%d", Dinic::solve());
    return 0;
}
P2053 [SCOI2007] 修车

\(n\) 位车主和 \(m\) 位技术人员,给出每个技术人员维修每辆车的时间,安排这 \(m\) 位技术人员所维修的车及顺序,使得顾客平均等待时间最短。

\(n \le 60\)\(m \le 9\)

考虑一位技术人员的修车顺序为 \(1 \sim k\) ,其修车花费时间为 \(t_{1 \sim k}\) ,其对等待时间的贡献为 \(\sum_{i = 1}^k (k - i + 1) t_i\)

接下来将每个技术人员拆成 \(n\) 个点 \(u_{i, j}\) ,代表第 \(i\) 个技术人员修倒数第 \(j\) 辆车的状态。考虑建模:

  • 源点向每个客户连容量为 \(1\) 的边。
  • 每个客户向每个 \(u_{i, j}\) 连容量为 \(1\) ,费用为 \(j \times w_i\) 的边。
  • 每个点 \(u_{i, j }\) 向汇点连容量为 \(1\) 的边。

跑最小费用最大流即可求出总费用,即最小等待时间和。

signed main() {
    m = read(), n = read();
    int S = n + n * m + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, 1, 0);

    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            Dinic::insert(i * n + j, T, 1, 0);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();

            for (int k = 1; k <= n; ++k)
                Dinic::insert(i, j * n + k, 1, k * w);
        }

    Dinic::solve();
    printf("%.2lf", (double)Dinic::mincost / n);
    return 0;
}
P4662 [BalticOI 2008] 黑手党

给定一张无向图,点带点权,最小化删除的点权和使得 \(s, t\) 不连通,并构造方案。

\(n \le 200\)\(m \le 2 \times 10^4\)

考虑拆点,流量为 \(1\) ,边权为点权即可。

signed main() {
    n = read(), m = read(), S = read(), T = read();
    Dinic::prework(n * 2, S, T + n);
    
    for (int i = 1; i <= n; ++i)
        Dinic::insert(i, i + n, read());
    
    for (int i = 1; i <= m; ++i) {
        int u = read(), v = read();
        Dinic::insert(u + n, v, inf);
        Dinic::insert(v + n, u, inf);
    }
    
    Dinic::solve();
    
    for (int i = 1; i <= n; ++i)
        if (Dinic::dep[i] && !Dinic::dep[i + n])
            printf("%d ", i);
    
    return 0;
}

分层图

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

现有 \(n + 2\) 个太空站,月球编号为 \(-1\) ,地球编号为 \(0\) ,剩下的编号为 \(1 \sim n\) ,有 \(m\) 艘公共交通太空船。

每个太空站可容纳无限多的人,而第 \(i\) 艘太空船只可容纳 \(h_i\) 个人。

每艘太空船将周期性地停靠一系列的太空站,每一艘太空船从一个太空站驶往任一太空站耗时均为 \(1\)

人们只能在太空船停靠太空站时上、下船。

初始时所有 \(k\) 个人全在地球上,太空船全在初始站,求所有人转移到月球上的最短用时。

\(n \le 13\)\(m \le 20\)\(k \le 50\)

先用并查集判掉无解的情况。

观察到数据范围非常小,考虑枚举时间,将图分层,然后动态加边跑网络流,当能够到达月球的人数 \(\ge k\) 的时候退出。

首先将 \(S\) 连向当前时刻的地球,当前时刻的月球连向 \(T\) ,通过太空船从上一时刻的太空站转移到当前时刻的太空船,然后继续跑最大流,一直到结束条件被满足就直接退出。

signed main() {
    n = read(), m = read(), k = read();
    dsu.clear(n + 2);
    
    for (int i = 1; i <= m; ++i) {
        h[i] = read(), s[i].resize(read());
        int pre = -1;
        
        for (int &it : s[i]) {
            it = read();
            
            if (!it)
                it = n + 1;
            else if (it == -1)
                it = n + 2;
            
            if (~pre)
                dsu.merge(pre, it);
            
            pre = it;
        }
    }
    
    if (dsu.find(n + 1) != dsu.find(n + 2))
        return puts("0"), 0;
    
    int S = Dinic::N - 2, T = Dinic::N - 1;
    Dinic::prework(S, T);
    
    for (int tim = 1;; ++tim) {
        Dinic::insert(S, (tim - 1) * (n + 1) + n + 1, inf);
        
        for (int i = 1; i <= m; ++i) {
            int x = (tim - 1) % s[i].size(), y = tim % s[i].size();
            
            if (s[i][x] == n + 2)
                x = T;
            else
                x = (tim - 1) * (n + 1) + s[i][x];
            
            if (s[i][y] == n + 2)
                y = T;
            else
                y = tim * (n + 1) + s[i][y];
            
            Dinic::insert(x, y, h[i]);
        }
        
        if (Dinic::solve() >= k)
            return printf("%d\n", tim), 0;
        
        for (int i = 1; i <= n + 1; ++i)
            Dinic::insert((tim - 1) * (n + 1) + i, tim * (n + 1) + i, inf);
    }
    
    return 0;
}
[ABC397G] Maximize Distance

给出一张有向图,可以将 \(k\) 条边权值定为 \(1\) ,其他边权值定为 \(0\) ,最大化 \(1 \to n\) 的最短路。

\(n \le 30\)\(m \le 100\)

先考虑能否使 \(1 \to n\) 的最短路变为非 \(0\) ,不难发现只要令最小割的边为 \(1\) 即可,即只要判定最小割是否 \(\le k\)

接下来考虑原问题,考虑二分答案 \(ans\) 。判定考虑建立分层图,共 \(ans\) 层,记 \((i, j)\) 表示第 \(j\) 层的 \(i\) 。如果割掉一条边,则每一层这条边都要割掉,可以建立虚点处理,此时只能走到下一层。对于原图中的第 \(i\) 条边 \((u, v)\) ,连边:

  • \(((u, j), x_i, +\infty), (x_i, y_i, 1), (y_i, (v, j), +\infty)\) 表示割掉这条边。
  • \(((u, j), (v, j + 1), +\infty)\) :表示走到下一层。

最小割是否 \(\le k\) 即可。

事实上无需建立虚点,直接连 \(((u, i), (v, i), 1)\) 即可。因为最终的图上无负环,因此走一个环显然是不优的。如果当前边被割掉,选择走到下一层,则后面也不会再走回来。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7, M = 1e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct Edge {
    int u, v;
} e[M];

int dis[N];

int n, m, k;

inline void bfs(int S) {
    memset(dis, -1, sizeof(dis));
    queue<int> q;
    dis[S] = 0, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : G.e[u])
            if (dis[v] == -1)
                dis[v] = dis[u] + 1, q.emplace(v);
    }
}

namespace Dinic {
struct Edge {
    int nxt, v, f;
} e[M << 1];
 
int head[N], cur[N], dep[N];
bool vis[N];
 
int n, S, T, tot, maxflow;
 
inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1, maxflow = 0;
    memset(head + 1, 0, sizeof(int) * n);
}
 
inline void insert(int u, int v, int f) {
    e[++tot] = (Edge){head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, 0}, head[v] = tot;
}
 
inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);
 
    while (!q.empty()) {
        int u = q.front();
        q.pop();
 
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v;
 
            if (e[i].f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }
 
    return dep[T];
}
 
int dfs(int u, int flow) {
    if (u == T)
        return flow;
 
    vis[u] = true;
    int outflow = 0;
 
    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;
 
        if (f && dep[v] == dep[u] + 1 && !vis[v]) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;
 
            if (outflow == flow)
                return vis[u] = false, outflow;
        }
    }
 
    return outflow;
}
 
inline int solve() {
    while (bfs())
        maxflow += dfs(S, inf);
 
    return maxflow;
}
} // namespace Dinic

inline bool check(int layer) {
    int S = 1, T = n * layer;
    Dinic::prework(n * layer, S, T);

    for (int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v;

        for (int j = 1; j <= layer; ++j)
            Dinic::insert(u + (j - 1) * n, v + (j - 1) * n, 1);

        for (int j = 1; j < layer; ++j)
            Dinic::insert(u + (j - 1) * n, v + j * n, inf);
    }

    return Dinic::solve() <= k;
}

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &e[i].u, &e[i].v), G.insert(e[i].u, e[i].v);

    bfs(1);
    int l = 1, r = dis[n], ans = 0;

    while (l <= r) {
        int mid = (l + r) >> 1;

        if (check(mid))
            ans = mid, l = mid + 1;
        else
            r = mid - 1;
    }

    printf("%d", ans);
    return 0;
}

最小割

划分模型

P4313 文理分科

\(n \times m\) 个同学组成一个矩阵,每个人需要从文科和理科中选择一个,产生贡献有四种:

  • \((i, j)\) 选文科:产生 \(art_{i, j}\) 的贡献。
  • \((i, j)\) 选理科:产生 \(science_{i, j}\) 的贡献。
  • \((i, j)\) 与其相邻的同学都选文科:产生 \(same\_art_{i, j}\) 的贡献。
  • \((i, j)\) 与其相邻的同学都选理科:产生 \(same\_science_{i, j}\) 的贡献。

最大化贡献和。

\(n, m \le 100\)

若仅考虑前两类贡献,最大贡献和等价于总贡献和减去最小取不到的贡献和,考虑转化为最小割模型:

  • 源点表示文科,\(S\)\((i, j)\) 连流量为 \(art_{i, j}\) 的边。
  • 汇点表示理科,\((i, j)\)\(T\) 连流量为 \(science_{i, j}\) 的边。

最小取不到的贡献和即为最小割。

再考虑后两类贡献,以文科为例。建立一个虚点,\(S\) 向其连一条流量 \(same\_art_{i, j}\) 的边,其向 \((i, j)\) 及相邻的点连流量为 \(+ \infty\) 的边。

考虑实际意义,若这些点有一个选理科,则会产生 \(S\) 到这个虚点再到选理科的点再到 \(T\) 的路径,就要割掉这条边,即这条边不产生贡献。

signed main() {
    n = read(), m = read();
    int S = n * m * 3 + 1, T = S + 1, ext = n * m, sum = 0;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();
            sum += w, Dinic::insert(S, getid(i, j), w);
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();
            sum += w, Dinic::insert(getid(i, j), T, w);
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();
            sum += w;
            Dinic::insert(S, ++ext, w), Dinic::insert(ext, getid(i, j), inf);

            for (int k = 0; k < 4; ++k) {
                int x = i + dx[k], y = j + dy[k];

                if (1 <= x && x <= n && 1 <= y && y <= m)
                    Dinic::insert(ext, getid(x, y), inf);
            }
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();
            sum += w;
            Dinic::insert(++ext, T, w), Dinic::insert(getid(i, j), ext, inf);

            for (int k = 0; k < 4; ++k) {
                int x = i + dx[k], y = j + dy[k];

                if (1 <= x && x <= n && 1 <= y && y <= m)
                    Dinic::insert(getid(x, y), ext, inf);
            }
        }

    printf("%d", sum - Dinic::solve());
    return 0;
}

染色法

P2774 方格取数问题

给定一个 \(n \times m\) 的矩阵,选取若干互不相邻的格子最大化权值和。

\(n, m \le 100\)

考虑对矩阵黑白染色,将相邻的格子连边,构建二分图结构,问题转化为选出一个最大权独立集。

直接考虑是否选取是困难的,考虑划分模型,先钦定所有的点都选,再考虑删掉权值和最小的点集使得剩下的点不冲突。考虑网络流建模:

  • 从源点向黑点连流量为其权值的边。
  • 从黑点向相邻白点连流量为 \(+ \infty\) 的边。
  • 从白点向汇点连流量为其权值的边。

最小权值和即为最小割。

signed main() {
    n = read(), m = read();
    int S = n * m + 1, T = S + 1;
    Dinic::prework(T, S, T);
    ll sum = 0;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w = read();
            sum += w;

            if ((i + j) & 1)
                Dinic::insert(S, getid(i, j), w);
            else
                Dinic::insert(getid(i, j), T, w);
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if ((i + j) & 1) {
                for (int k = 0; k < 4; ++k) {
                    int x = i + dx[k], y = j + dy[k];

                    if (1 <= x && x <= n && 1 <= y && y <= m)
                        Dinic::insert(getid(i, j), getid(x, y), inf);
                }
            }

    printf("%lld", sum - Dinic::solve());
    return 0;
}

互不攻击问题

棋盘上的互不攻击问题常用的方法是将格子分为两类,使得攻击关系与这两类点构成二分图,然后套用划分模型。

P3355 骑士共存问题

\(n \times n\) 的棋盘有 \(m\) 个障碍,最大化放置骑士的数量使骑士之间互不攻击。一个骑士的攻击范围如下图所示:

\(n \le 200\)

考虑黑白染色,则每个攻击都是异色点之间,形成二分图的结构,跑最小割即可。

signed main() {
    n = read(), m = read();
    
    for (int i = 1; i <= m; ++i) {
        int x = read(), y = read();
        ban[x][y] = true;
    }

    int S = n * n + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            if (ban[i][j])
                continue;

            if ((i + j) & 1) {
                Dinic::insert(S, getid(i, j), 1);

                for (int k = 0; k < 8; ++k) {
                    int x = i + dx[k], y = j + dy[k];

                    if (1 <= x && x <= n && 1 <= y && y <= n && !ban[x][y])
                        Dinic::insert(getid(i, j), getid(x, y), inf);
                }
            } else
                Dinic::insert(getid(i, j), T, 1);
        }

    printf("%d\n", n * n - m - Dinic::solve());
    return 0;
}
P5030 长脖子鹿放置

\(n \times m\) 的棋盘有 \(k\) 个障碍,最大化放置长脖子鹿的数量使长脖子鹿之间互不攻击。一个长脖子鹿的攻击范围如下图所示:

\(n, m \le 200\)

注意到普通的黑白染色不能套用。但是注意到攻击都在同色点中,那么考虑同色点之间分别建立二分图,则黑白点是互不影响的。

可以发现攻击关系的两点的行、列奇偶性均不同,于是可以构建二分图关系。

事实上这样构建二分图关系等价于按行(或列)的奇偶性分类,从这个角度考虑同样是可行的。

signed main() {
    n = read(), m = read(), k = read();
    
    for (int i = 1; i <= k; ++i) {
        int x = read(), y = read();
        ban[x][y] = true;
    }

    k = 0;

    for (int i = 1; i <= n; ++i)
        k += count(ban[i] + 1, ban[i] + 1 + m, true);

    int S = n * m + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            if (ban[i][j])
                continue;

            if (i & 1) {
                Dinic::insert(S, getid(i, j), 1);

                for (int k = 0; k < 8; ++k) {
                    int x = i + dx[k], y = j + dy[k];

                    if (1 <= x && x <= n && 1 <= y && y <= m && !ban[x][y])
                        Dinic::insert(getid(i, j), getid(x, y), inf);
                }
            } else
                Dinic::insert(getid(i, j), T, 1);
        }

    printf("%d\n", n * m - k - Dinic::solve());
    return 0;
}

切糕模型

P3227 [HNOI2013] 切糕

有一个长宽高分别为 \(P, Q, R\) 的长方体点阵,每个整点都有权值 \(v_{x, y, z}\)

一个切面合法当且仅当满足以下两个条件:

  • 与每个纵轴(一共 \(P \times Q\) 个)有且仅有一个交点。
  • 相邻纵轴上的切割点的高度差 \(\le D\)

最小化 \(P \times Q\) 个切割点的权值和。

\(P, Q, R \le 40\)\(v \le 1000\)

考虑最小割模型,对每个纵轴建立 \(R\) 个点构成一条链,\((i, j, k) \to (i + 1, j, k)\) 的流量为 \(v(i, j, k)\) ,则割掉链上的某条边则说明选择这个权值。

下面考虑高度差的限制,即对于相邻纵轴上高度差 \(> D\) 的一对边,需要满足同时割掉这两条边后 \(S, T\) 仍连通。对于所有 \(i \in [d + 1, r + 1]\) ,以及所有相邻的点对 \((x_1, y_1), (x_2, y_2)\) ,从 \((i, x_1, y_1)\)\((i - d, x_2, y_2)\) 连边即可,意义就是若割掉了 \((x_1, y_1)\) 上更小的边,则 \(S\) 仍能到达 \(T\)

signed main() {
    scanf("%d%d%d%d", &p, &q, &r, &d);
    int S = p * q * (r + 1) + 1, T = S + 1;
    Dinic::prework(T, S, T);

    auto getid = [](int x, int y, int z) {
        return (x - 1) * p * q + (y - 1) * q + z;
    };

    for (int i = 1; i <= p; ++i)
        for (int j = 1; j <= q; ++j)
            Dinic::insert(S, getid(1, i, j), inf), Dinic::insert(getid(r + 1, i, j), T, inf);

    for (int i = 1; i <= r; ++i)
        for (int j = 1; j <= p; ++j)
            for (int k = 1; k <= q; ++k) {
                int val;
                scanf("%d", &val);
                Dinic::insert(getid(i, j, k), getid(i + 1, j, k), val);
            }

    for (int i = d + 1; i <= r + 1; ++i)
        for (int x = 1; x <= p; ++x)
            for (int y = 1; y <= q; ++y)
                for (int k = 0; k < 4; ++k) {
                    int nx = x + dx[k], ny = y + dy[k];

                    if (1 <= nx && nx <= p && 1 <= ny && ny <= q)
                        Dinic::insert(getid(i, x, y), getid(i - d, nx, ny), inf);
                }

    printf("%d", Dinic::solve());
    return 0;
}
P6054 [RC-02] 开门大吉

\(n\) 位选手和 \(m\) 套题,每套题里有 \(p\) 个题目,第 \(i\) 位选手答对第 \(j\) 套题中的第 \(k\) 道题的概率为 \(f_{i, j, k}\) 。若一位选手答对第 \(i\) 题,则会得到 \(c_i\) 元的奖金。

选手总是从第一道开始按顺序答题的,若某题答错则该选手的答题流程将直接结束。

\(y\) 条限制,限制 \((i, j, k)\) 表示第 \(i\) 位选手做的套题编号必须至少比第 \(j\) 位选手做的套题编号大 \(k\)

试给每一位选手分配一套题(不同选手可以相同),最小化期望奖金和,或判定无解。

\(n, m, p \le 80\)\(y \le 1000\)\(k \in [-m, m]\)

首先不难算出 \(g_{i, j}\) 表示第 \(i\) 位选手做第 \(j\) 套题的期望奖金。

考虑建立网络流模型,建 \(n\) 条长度为 \(m + 1\) 的链,设第 \(i\) 条链链上的点为 \(p_{i, 1 \sim m + 1}\) ,连边 \((S, p_{i, 1}, +\infty)\)\((p_{i, j}, p_{i, j + 1}, g_{i, j})\)\((p_{i, m + 1}, T, +\infty)\) ,最小割即为答案。

考虑表示限制,对于 \(x \in [max(1, 1 - k), m + 1]\) ,连边 \((p_{j, x}, p_{i, min(x + k, m + 1)}, +\infty)\) 即可,也可以通过连反向边把限制传递到没被限制到的点上。

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d%d", &n, &m, &p, &q);

        for (int i = 1; i <= p; ++i)
            scanf("%d", val + i), val[i] += val[i - 1];

        for (int j = 1; j <= m; ++j)
            for (int i = 1; i <= n; ++i)
                for (int k = 1; k <= p; ++k)
                    scanf("%lf", f[i][j] + k);

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                g[i][j] = 0;
                double mul = 1;

                for (int k = 1; k <= p; ++k)
                    mul *= f[i][j][k], g[i][j] += mul * (1 - f[i][j][k + 1]) * val[k];
            }

        auto getid = [](int i, int j) {
            return (i - 1) * (m + 1) + j;
        };

        int S = n * (m + 1) + 1, T = S + 1;
        Dinic::prework(T, S, T);

        for (int i = 1; i <= n; ++i) {
            Dinic::insert(S, getid(i, 1), inf), Dinic::insert(getid(i, m + 1), T, inf);

            for (int j = 1; j <= m; ++j)
                Dinic::insert(getid(i, j), getid(i, j + 1), g[i][j]);
        }

        while (q--) {
            int i, j, k;
            scanf("%d%d%d", &i, &j, &k);

            for (int x = max(1, 1 - k); x <= m + 1; ++x)
                Dinic::insert(getid(j, x), getid(i, min(x + k, m + 1)), inf);
        }

        if (Dinic::solve())
            printf("%lf\n", Dinic::maxflow);
        else
            puts("-1");
    }

    return 0;
}

对偶图最短路

  • 平面图:对于一个图,若其同构图或其本身没有任意两边相交,则称其为平面图。
  • 对偶图:对于一个平面图中边分割出的多个面,用若干点表示这若干面,只要有一条边在这两个面之间,就将这两个面所对应的点连边,权值等于该边。

下图中,图 \(G'\) 是平面图 \(G\) 的对偶图

定理:平面图最大流 = 平面图最小割 = 对偶图最短路。

理解就是每条 \(S \to T\) 在平面图上的割与对偶图上的路径一一对应。

通常求最短路的复杂度优于网络流的复杂度,看到平面图一定要考虑往对偶图发现思考。

P2046 [NOI2010] 海拔

在带边权的边长为 \(n\) 的网格图上,每条边平行于坐标轴的边的两个方向均有一定的人流量。

左上角高度为 \(0\) ,右下角高度为 \(1\) 。爬坡要花费体力,下坡和平地不需要花费体力。

给每个点分配一个高度,最小化所有人行走的消耗体力和。

\(n \le 500\)

有观察:

  • 每个点分配的高度应为 \(0\)\(1\) ,否则可以调整使得其更优。
  • 左上到右下的路径上应当只出现一次高度变化,否则可以调整使得其更优。

问题转化为平面图最小割,进一步转化为对偶图最短路,将网格图的边顺时针旋转 \(90^\circ\) 得到对偶图的边,时间复杂度 \(O(n^2 \log n^2)\)

signed main() {
    n = read();
    int S = n * n + 1, T = S + 1;

    for (int i = 1; i <= n + 1; ++i)
        for (int j = 1; j <= n; ++j) {
            if (i == 1)
                G.insert(S, getid(1, j), read());
            else if (i == n + 1)
                G.insert(getid(n, j), T, read());
            else
                G.insert(getid(i - 1, j), getid(i, j), read());
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n + 1; ++j) {
            if (j == 1)
                G.insert(getid(i, 1), T, read());
            else if (j == n + 1)
                G.insert(S, getid(i, n), read());
            else
                G.insert(getid(i, j), getid(i, j - 1), read());
        }

    for (int i = 1; i <= n + 1; ++i)
        for (int j = 1; j <= n; ++j) {
            if (i == 1)
                G.insert(getid(1, j), S, read());
            else if (i == n + 1)
                G.insert(T, getid(n, j), read());
            else
                G.insert(getid(i, j), getid(i - 1, j), read());
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n + 1; ++j) {
            if (j == 1)
                G.insert(T, getid(i, 1), read());
            else if (j == n + 1)
                G.insert(getid(i, n), S, read());
            else
                G.insert(getid(i, j - 1), getid(i, j), read());
        }

    printf("%d", Dijkstra(S, T));
    return 0;
}

费用流

通常在最大流不足以表示条件时使用,用流量判定合法性,用费用求解答案。

方格取数模型

P2045 方格取数加强版

给出一个 \(n\times n\) 的矩阵,每一格有一个非负整数 \(A_{i,j}\)

\((1,1)\) 出发,可以往右或者往下走,最后到达 \((n,n)\)

每达到一格,把该格子的数取出来,该格子的数就变成 \(0\)

一共走 \(K\) 次,最大化 \(K\) 次所到达的方格的数的和。

\(n \le 50\)\(k \le 10\)

先拆点,把每个格子 \((i,j)\) 拆成一个入点一个出点。

  • 从每个入点向对应的出点连两条有向边:一条容量为 \(1\) ,费用为格子 \((i,j)\) 中的数;另一条容量为 \(k-1\) ,费用为 \(0\)
  • \((i,j)\) 的出点到 \((i,j+1)\)\((i+1,j)\) 的入点连有向边,容量为 \(k\) ,费用为 \(0\)

\((1,1)\) 的入点为源点, \((n,n)\) 的出点为汇点,跑最大费用最大流即可。

signed main() {
    n = read(), k = read();
    int S = getid(1, 1, 0), T = getid(n, n, 1);
    Dinic::prework(n * n * 2, S, T);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            int w = read();
            Dinic::insert(getid(i, j, 0), getid(i, j, 1), 1, -w);
            Dinic::insert(getid(i, j, 0), getid(i, j, 1), k - 1, 0);
            
            if (i < n)
                Dinic::insert(getid(i, j, 1), getid(i + 1, j, 0), k, 0);
            
            if (j < n)
                Dinic::insert(getid(i, j, 1), getid(i, j + 1, 0), k, 0);
        }

    Dinic::solve();
    printf("%d", -Dinic::mincost);
    return 0;
}

区间模型

P3358 最长k可重区间集问题

给定 \(n\) 个开区间和一个数字 \(k\) ,要求从中选出若干区间,使得任意点被覆盖的次数 \(\le k\) ,最大化所选区间总长度和。

\(n \le 500\)\(k \le 3\)

因为两个区间不交就可以同时选,考虑将选出的区间分为若干组不交区间,则最多分 \(k\) 组。

将点离散化为 \(1 \sim m\) ,考虑费用流建模,将每组不交区间视为一个流,则有建模:

  • \(S \to 1 \to \cdots \to m \to T\) 均连流量为 \(k\) ,费用为 \(0\) 的边。
  • 对于每一条线段,起点向终点连一条流量为 \(1\) ,费用为 \(len\) 的边。

跑最大费用最大流即可。

signed main() {
    n = read(), k = read();
    vector<int> vec;

    for (int i = 1; i <= n; ++i)
        vec.emplace_back(a[i].l = read()), vec.emplace_back(a[i].r = read());

    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
    int S = vec.size() + 1, T = S + 1;
    Dinic::prework(vec.size() + 2, S, T);
    Dinic::insert(S, 1, k, 0);

    for (int i = 1; i < vec.size(); ++i)
        Dinic::insert(i, i + 1, k, 0);

    Dinic::insert(vec.size(), T, k, 0);

    for (int i = 1; i <= n; ++i) {
        int l = lower_bound(vec.begin(), vec.end(), a[i].l) - vec.begin() + 1,
            r = lower_bound(vec.begin(), vec.end(), a[i].r) - vec.begin() + 1;
        Dinic::insert(l, r, 1, -(a[i].r - a[i].l));
    }

    Dinic::solve();
    printf("%d", -Dinic::mincost);
    return 0;
}
登机

\(n\) 架飞机,第 \(i\) 架飞机有 \(x\) 人,登记时刻为 \(s_i\) ,起飞时刻为 \(t_i\)

飞机需在登机时间选择空闲停机位(起飞时该停机位同时空闲),停机位分两种:

  • 带登机桥的停机位:有 \(a\) 个,此处登机无需代价。
  • 不带登机桥的停机位:有 \(b\) 个,此处登机需要与登机人数相等的代价。

飞机在登机到起飞期间可切换停机位,若在 \(t\) 时刻切换,则需保证 \(t + 1\) 时刻另一个停机位空闲,且设人数为 \(x\) ,则会产生 \(\lfloor px \rfloor\) 的代价,其中 \(p \in [0, 1]\)

最小化登机总代价,或判定无解。

\(n \le 200\)

无解的判定是简单的,对 \([s_i, t_i)\) 做区间覆盖,若一个点被覆盖了 \(> a + b\) 次则无解。

称有登机桥的停机位为 A 类停机位,其他的是 B 类停机位。

先设初始代价为所有飞机总人数,则停在 A 会减少 \(x_i\) 的代价,停在 B 没有代价。

显然一架飞机只有三种决策:

  • 一直待在 A:在 \([s_i, t_i)\) 的时间占用 A,减少 \(x_i\) 的代价。
  • 一直待在 B:在 \([s_i, t_i)\) 的时间占用 B。
  • 在 A 处登机后马上到 B,在 \(s_i\) 的时间占用 A,减少 \(x_i - \lfloor p x_i + 10^{-5} \rfloor\) 的代价。

显然能不用第三种决策就不用,所以判断掉无解的情况之后就可以不管 B 类停机位的数量。

建立费用流模型,对每个时刻离散化后建点,相邻时刻给出 \(a\) 的流量。对第 \(i\) 架飞机建点 \(u_i\)

  • \(s_i \to u_i\) 连容量为 \(1\) 、费用为 \(0\) 的边。
  • \(u_i \to s_i + 1\) 连容量为 \(1\) 、费用为 \(x_i - \lfloor p x_i + 10^{-5} \rfloor\) 的边。
  • \(u_i \to t_i\) 连容量为 \(1\) 、费用为 \(x_i\) 的边。

跑最大费用最大流即可。

inline bool check() {
    vector<int> c(vec.size());

    for (int i = 1; i <= n; ++i)
        ++c[nd[i].l], --c[nd[i].r];

    for (int i = 0, res = 0; i < c.size(); ++i)
        if ((res += c[i]) > A + B)
            return false;

    return true;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d%lf", &n, &A, &B, &p);
        vec.clear();

        for (int i = 1; i <= n; ++i) {
            scanf("%d%d%d", &nd[i].x, &nd[i].l, &nd[i].r);
            vec.emplace_back(nd[i].l), vec.emplace_back(nd[i].r);
        }

        sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());

        for (int i = 1; i <= n; ++i) {
            nd[i].l = lower_bound(vec.begin(), vec.end(), nd[i].l) - vec.begin(),
            nd[i].r = lower_bound(vec.begin(), vec.end(), nd[i].r) - vec.begin();
        }

        if (!check()) {
            puts("impossible");
            continue;
        }

        int S = n + vec.size() + 1, T = S + 1;
        Dinic::prework(T, S, T), Dinic::insert(S, n + 1, A, 0), Dinic::insert(n + vec.size(), T, A, 0);

        for (int i = 0; i + 1 < vec.size(); ++i)
            Dinic::insert(n + i + 1, n + i + 2, A, 0);

        int ans = 0;

        for (int i = 1; i <= n; ++i) {
            Dinic::insert(n + 1 + nd[i].l, i, 1, 0);
            Dinic::insert(i, n + 1 + nd[i].l + 1, 1, (int)(p * nd[i].x + 1e-5) - nd[i].x);
            Dinic::insert(i, n + 1 + nd[i].r, 1, -nd[i].x);
            ans += nd[i].x;
        }

        Dinic::solve();
        printf("%d\n", ans + Dinic::mincost);
    }

    return 0;
}

后效性模型

后效性:当前时刻的决策会影响之后的状态。

通常的特征是出现时刻变化且当前操作会影响后续点,处理方法是建立时间轴,找到改变后续状态的点,对其进行操作。

P1251 餐巾计划问题

一个餐厅在之后的 \(n\) 天里,第 \(i\) 天需要 \(r_i\) 块干净餐巾,每天可以选择:

  • 购买一块餐巾,花费 \(p\) 元。
  • 把脏餐巾送到快洗部,花费 \(c_f\) 元,且需要 \(t_f\) 天后取。
  • 把脏餐巾从到慢洗部,花费 \(c_s\) 元,且需要 \(t_s\) 天后取。

最小化花费。

\(n \le 2 \times 10^3\)

建立时间轴,每一天都要使用干净餐巾,又会制造脏餐巾,所以需要拆点处理干净餐巾转化为脏餐巾的过程。将一天 \(d_i\) 拆成两个点 \(d_i\)\(d_i'\) ,可以对应看做早上和晚上,每天早上收到干净餐巾,每天晚上向后发送脏餐巾。

考虑费用流建模:

  • 每天的需求:\(d_i\)\(T\) 连容量为 \(r_i\) ,费用为 \(0\) 的边。
  • 每天产生脏餐巾:\(S\)\(d_i'\) 连连容量为 \(r_i\) ,费用为 \(0\) 的边。
  • 没用完的干净餐巾留到明天:\(d_i\)\(d_{i + 1}\) 连容量为 \(+ \infty\) ,费用为 \(0\) 的边。
  • 购买餐巾:\(S\)\(d_i\) 连容量为 \(+ \infty\) ,费用为 \(p\) 的边。
  • 送到快洗店:\(d_i'\)\(d_{i + t_f}\) 连容量为 \(+ \infty\) ,费用为 \(c_f\) 的边。
  • 送到慢洗店:\(d_i'\)\(d_{i + t_s}\) 连容量为 \(+ \infty\) ,费用为 \(c_s\) 的边。

跑最小费用最大流即可。

signed main() {
    n = read();
    int S = n * 2 + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i) {
        int r = read();
        Dinic::insert(S, i + n, r, 0);
        Dinic::insert(i, T, r, 0);
    }

    for (int i = 1; i < n; ++i)
        Dinic::insert(i, i + 1, inf, 0);

    p = read(), t1 = read(), c1 = read(), t2 = read(), c2 = read();

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, inf, p);

    for (int i = 1; i + t1 <= n; ++i)
        Dinic::insert(i + n, i + t1, inf, c1);

    for (int i = 1; i + t2 <= n; ++i)
        Dinic::insert(i + n, i + t2, inf, c2);

    Dinic::solve();
    printf("%lld", Dinic::mincost);
    return 0;
}

最短/长往返路

P2770 航空路线问题

给定无向图,找到两条路径 \(P_1, P_2\) ,满足:

  • \(P_1\)\(1\)\(n\) 的一条路径,满足所有边都是编号小的到编号大的。
  • \(P_1\)\(n\)\(1\) 的一条路径,满足所有边都是编号大的到编号小的。
  • \(P_1 \cap P_2 = \{ 1, n \}\)

最大化 \(|P_1| + |P_2| - 2\)

\(n \le 100\)

先把 \(P_2\) 反向,问题转化为找两条 \(1\)\(n\) 的不交路径使得经过节点数最多。

由于每个点只能经过一次,将点 \(x\) 拆分成 \(x_1,x_2\)

  • 对于除了起点和终点外的点连 \(x_1 \to x_2\),流量为 \(1\) ,费用为 \(0\) 的边。

  • 对于起点和终点,连 \(x_1 \to x_2\),流量为 \(2\) ,费用为 \(0\) 的边。

  • 对于 \(u \to v\) 的边,连一条 \(u_2 \to v_1\),流量为 \(1\),费用为 \(1\) 的边。

\(1\)\(n\) 的最大费用最大流即可,最大流为 \(2\) 则有解,费用即为经过的城市数。

特别地,当最大流为 \(1\) 时,需要特判 \(s \to t \to s\) 的情况。

输出方案直接在残量网络上 DFS 两次寻找路径即可。

using Dinic::e;
using Dinic::head;

void dfs1(int u) {
    vis[u] = true, puts(str[u - n].c_str());
    
    if (u == Dinic::T)
        return;
    
    for (int i = head[u]; i; i = e[i].nxt)
        if (e[i].v <= n && !e[i].f) {
            dfs1(e[i].v + n);
            break;
        }
}

void dfs2(int u) {
    vis[u] = true;
    
    if (u == Dinic::T)
        return;
    
    for (int i = head[u]; i; i = e[i].nxt)
        if (e[i].v <= n && !vis[e[i].v + n] && !e[i].f) {
            dfs2(e[i].v + n);
            break;
        }
    
    puts(str[u - n].c_str());
}

signed main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; ++i) {
        cin >> str[i];
        mp[str[i]] = i;
    }
    
    Dinic::prework(n * 2, 1, n * 2);
    Dinic::insert(1, n + 1, 2, 0), Dinic::insert(n, n * 2, 2, 0);
    
    for (int i = 2; i < n; ++i)
        Dinic::insert(i, i + n, 1, 0);
    
    bool flag = false;
    
    for (int i = 1; i <= m; ++i) {
        string str1, str2;
        cin >> str1 >> str2;
        int u = mp[str1], v = mp[str2];
        Dinic::insert(u + n, v, 1, -1);
        flag |= (u == 1 && v == n);
    }
    
    Dinic::solve();
    
    if (Dinic::maxflow == 2) {
        printf("%d\n", -Dinic::mincost);
        dfs1(1 + n), dfs2(1 + n);
    } else if (flag)
        puts("2"), puts(str[1].c_str()), puts(str[n].c_str()), puts(str[1].c_str());
    else
        puts("No Solution!");
    
    return 0;
}

哈密顿路模型

P2469 [SDOI2010] 星际竞速

给定一张有向图,可以走有向边,也可以花 \(a_i\) 的代价直接到达点 \(i\)

可以花费 \(a_i\) 的代价选择起点 \(i\) ,求到达每个点恰好一次的最小代价。

\(n \le 800\)\(m \le 1.5 \times 10^4\)

考虑将这个路径抽象为接力赛,每个人拿到接力棒之后需要打卡,并传给下一个人,限制打卡只能打一次,每个人必须打卡。

看到恰好一次,显然考虑拆 \(u\) 为入点 \(P_u\) 和出点 \(Q_u\) ,具体建模为:

  • 准备接棒的等待者:\(S \to P_u\) ,流量为 \(1\) ,费用为 \(0\)
  • 只能打卡恰好一次:\(Q_u \to T\) ,流量为 \(1\) ,费用为 \(0\)
  • 走一条边 \(u \to v\)\(P_u \to Q_v\) ,流量为 \(1\) ,费用为边权。
  • 直接到达 \(u\)\(S \to Q_u\) ,流量为 \(1\) ,费用为 \(a_u\)

最小费用即为答案。

signed main() {
    n = read(), m = read();
    int S = n * 2 + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, 1, 0), Dinic::insert(n + i, T, 1, 0), Dinic::insert(S, n + i, 1, read());

    for (int i = 1; i <= m;++i) {
        int u = read(), v = read(), w = read();

        if (u > v)
            swap(u, v);

        Dinic::insert(u, n + v, 1, w);
    }

    Dinic::solve();
    printf("%d", Dinic::mincost);
    return 0;
}

多人游走模型

一类图上游走模型中,多个人可以同时移动,这可以模拟为流,那么费用即为答案。

CF2046D For the Emperor!

给定一张有向图,第 \(i\) 个点有 \(a_i\) 个信使。每个信使可以随身携带一份计划副本沿有向边移动,每次到达一个点时可以在这里通知计划,并复制若干计划副本。最小化选出发放计划的点数使得最终每个点都有带着计划(或副本)信使,或报告无解。

\(n \le 200\)\(m \le 800\)

先将原图缩点,考虑建立费用流模型,用流模拟信使的移动。钦定经过一个点的费用是 \(-B\)\(B\) 为一个极大值),而在一个点发放计划的费用为 \(1\) ,那么跑最小费用最大流后利用最小费用除以 \(B\) 的整数部分就是最多能让多少个点收到计划,余数就是答案。

本质就是利用商和余数表示了额优先级的关系,可以视为两个流和一个费用。

接下来将每个点 \(u\) 拆成三个点:约束点 \(F_u\) 、入点 \(P_u\) ,出点 \(Q_u\) ,考虑如下建模:

  • \(S \to F_u\) ,流量为 \(a_u\) ,费用为 \(0\)
  • \(Q_u \to T\) ,流量为 \(+ \infty\) 、费用为 \(0\)
  • 为了仅在第一次经过时产生 \(-B\) 的贡献,考虑 \(P_u \to Q_u\) 建两种边,一种流量为 \(1\) ,费用为 \(-B\) ;另一种流量为 \(+ \infty\) ,费用为 \(0\)
  • 为了仅在第一次发放时产生 \(1\) 的贡献,考虑先连 \(F_u \to P_u\) 流量费用均为 \(1\) 的边,再连 \(F_u \to Q_u\) 流量为 \(+ \infty\) 、费用为 \(0\) 的边。这样可以保证走后者时一定会先走前者(获得 \(-B\) 的贡献以最小化费用)。
  • 对于原图的有向边 \(u \to v\) 连边 \(Q_u \to P_v\) 流量为 \(+ \infty\) 、费用为 \(0\) 的边。
signed main() {
    int T = read();

    while (T--) {
        n = read(), m = read();

        for (int i = 1; i <= n; ++i)
            a[i] = read();

        G.clear(n);

        for (int i = 1; i <= m; ++i) {
            int u = read(), v = read();
            G.insert(u, v);
        }

        memset(dfn + 1, 0, sizeof(int) * n);
        memset(low + 1, 0, sizeof(int) * n);
        memset(leader + 1, 0, sizeof(int) * n);
        dfstime = scc = 0;

        for (int i = 1; i <= n; ++i)
            if (!dfn[i])
                Tarjan(i);

        int S = scc * 3 + 1, T = S + 1;
        Dinic::prework(T, S, T);

        for (int i = 1; i <= scc; ++i) {
            if (sum[i]) {
                Dinic::insert(S, i, sum[i], 0);
                Dinic::insert(i, scc + i, 1, 1);
                Dinic::insert(i, scc * 2 + i, inf, 0);
            }

            Dinic::insert(scc + i, scc * 2 + i, 1, -B);
            Dinic::insert(scc + i, scc * 2 + i, inf, 0);
            Dinic::insert(scc * 2 + i, T, inf, 0);
        }

        for (int u = 1; u <= n; ++u)
            for (int v : G.e[u])
                if (leader[u] != leader[v])
                    Dinic::insert(scc * 2 + leader[u], scc + leader[v], inf, 0);

        Dinic::solve();
        int ans = (Dinic::mincost % B + B) % B, cnt = (ans - Dinic::mincost) / B;
        printf("%d\n", cnt == scc ? ans : -1);
    }

    return 0;
}
P4542 [ZJOI2011] 营救皮卡丘

给出一张 \(n + 1\) 个点,\(m\) 条边的无向连通图,每一条边有一个代价。

\(k\) 个人从 \(0\) 号结点出发,可以分开。

对于每个 \(i\) 必须在经过 \(i - 1\) 之后才能通行。

求到达 \(n\) 的最小代价。

\(n \le 150\)\(m\le 2 \times 10^4\)\(k \le 10\) ,保证有解

考虑先求出 \(dis_{i, j}\) 表示 \(i \to j\)\(i < j\))不经过 \(> \max(i, j)\) 的点的最短路。将每个点 \(u\) 拆为入点 \(P_u\) 和出点 \(Q_u\) ,考虑如下建模:

  • \(k\) 个人从 \(0\) 出发:连 \(S \to P_0\) ,流量为 \(k\) ,费用为 \(0\)
  • 每个点至少经过一次:连 \(P_u \to Q_u\) ,一条流量为 \(1\) 、费用为 \(-B\)\(B\) 为一个极大值),另一条流量为 \(+ \infty\) 、费用为 \(0\)
  • 所有人可以在任意点结束:连 \(Q_u \to T\) ,流量为 \(+ \infty\) ,费用为 \(0\)
  • 一次 \(u \to v\) 的移动:\(Q_u \to P_v\) ,流量为 \(+\infty\) ,费用为 \(dis_{u, v}\)

考虑一下正确性,首先最大流肯定是 \(k\) 。因为保证有解,肯定会走掉 \(n\)\(-B\) 的边,保证了每个点都走一遍的限制。一条路径由于连的是 \(dis\) ,并且可以通过等待使得编号小的先被走掉,于是保证了按标号经过的限制。

signed main() {
    n = read() + 1, m = read(), k = read();
    memset(dis, inf, sizeof(dis));

    for (int i = 1; i <= m; ++i) {
        int u = read() + 1, v = read() + 1, w = read();
        dis[u][v] = dis[v][u] = min(dis[u][v], w);
    }

    for (int k = 1; k <= n; ++k)
        for (int i = k; i <= n; ++i)
            for (int j = k; j <= n; ++j)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);

    int S = n * 2 + 1, T = S + 1;
    Dinic::prework(T, S, T);
    Dinic::insert(S, 1, k, 0);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(i, n + i, 1, -B), Dinic::insert(i, n + i, inf, 0), Dinic::insert(n + i, T, inf, 0);

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            if (dis[i][j] != inf)
                Dinic::insert(n + i, j, inf, dis[i][j]);

    Dinic::solve();
    printf("%d", Dinic::mincost + B * n);
    return 0;
}

最大权匹配模型

[AGC034D] Manhattan Max Matching

给定平面上 \(n\) 个 A 类点和 \(n\) 个 B 类点,每个点上各有 \(a_i, b_i\) 个球,需要将 A 类球与 B 类球匹配,一对匹配的价值为两点曼哈顿距离,求最大匹配。

\(n \le 1000\)\(a_i, b_i \le 10\)

一个暴力是 \(S\) 连 A 类点,B 类点连 \(T\) ,再连 \(n^2\) 条的匹配边,流量为 \(+ \infty\) ,费用为曼哈顿距离,然后跑最大费用最大流。

注意到曼哈顿距离可以把绝对值拆开,分为四种情况取 \(\max\) 即:

\[|x_1 - x_2| + |y_1 - y_2| = \max \begin{Bmatrix} (x_1 - x_2) + (y_1 - y_2) \\ (x_2 - x_1) + (y_1 - y_2) \\ (x_1 - x_2) + (y_2 - y_1) \\ (x_2 - x_1) + (y_2 - y_1) \end{Bmatrix} \]

把两个点独立,得到:

\[|x_1 - x_2| + |y_1 - y_2| = \max \begin{Bmatrix} (x_1 + y_1) + (-x_2 - y_2) \\ (-x_1 + y_1) + (x_2 - y_2) \\ (x_1 - y_1) + (-x_2 + y_2) \\ (-x_1 - y_1) + (x_2 + y_2) \end{Bmatrix} \]

由于价值需要最大化,因此直接建在 A 类点和 B 类点之间建四个虚点,分别表示四种情况即可。

由于曼哈顿距离正负是对称的,因此直接跑最小费用最大流后答案即为费用的相反数。

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].k);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d%d", &b[i].x, &b[i].y, &b[i].k);

    int S = n * 2 + 5, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i) {
        Dinic::insert(S, i, a[i].k, 0);

        Dinic::insert(i, n * 2 + 1, a[i].k, a[i].x + a[i].y);
        Dinic::insert(n * 2 + 1, n + i, b[i].k, -b[i].x - b[i].y);

        Dinic::insert(i, n * 2 + 2, a[i].k, -a[i].x + a[i].y);
        Dinic::insert(n * 2 + 2, n + i, b[i].k, b[i].x - b[i].y);

        Dinic::insert(i, n * 2 + 3, a[i].k, a[i].x - a[i].y);
        Dinic::insert(n * 2 + 3, n + i, b[i].k, -b[i].x + b[i].y);

        Dinic::insert(i, n * 2 + 4, a[i].k, -a[i].x - a[i].y);
        Dinic::insert(n * 2 + 4, n + i, b[i].k, b[i].x + b[i].y);

        Dinic::insert(n + i, T, b[i].k, 0);
    }

    Dinic::solve();
    printf("%lld\n", -Dinic::mincost);
    return 0;
}
QOJ9047. Knights of Night

给定一张二分完全图,左右部分别有 \(n\) 个点 \(a_{1 \sim n}, b_{1 \sim n}\) ,左部点 \(i\) 与右部点 \(j\) 连边的权值为 \((a_i + b_j) \bmod 998244353\)

去掉指定的 \(m\) 条边后,对于 \(i = 1, 2, \cdots, k\) ,求匹配数量为 \(i\) 的最大权匹配。

\(n \le 10^5\)\(m \le 3 \times 10^5\)\(k \le \min(n, 200)\) ,TL = 7s

显然对于每个点只要保留前 \(k\) 大的边即可,在剩下的图中有结论:只保留前 \((2k - 1)(k - 1) + 1\) 大的边即可包含最大权匹配。

证明:由于每个点的度数 \(\le k\) ,因此对于每条匹配边,其最多会挡住 \(2k - 2\) 条边。匹配 \(k - 1\) 条边至多会挡住 \((2k - 2)(k - 1)\) 条边,此时只需要再保留一条边即可,共消耗 \((2k - 1)(k - 1) + 1\) 条边。

保留这些边可以直接维护候选集合,之后直接跑费用流即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3fll;
const int Mod = 998244353;
const int N = 1e5 + 7;

set<int> st[N];

int a[N], b[N], id[N], rka[N], rkb[N], p[N], cnta[N], cntb[N];

int n, m, k;

namespace Dinic {
const int N = 2e5 + 7, M = 6e5 + 7;

struct Edge {
    int nxt, v, f, c;
} e[M];

ll dis[N];
int head[N], cur[N];
bool vis[N], inque[N];

ll maxcost;
int n, S, T, tot;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f, int c) {
    e[++tot] = (Edge){head[u], v, f, c}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, 0, -c}, head[v] = tot;
}

inline bool SPFA() {
    fill(dis + 1, dis + n + 1, -inf);
    memset(vis + 1, false, sizeof(bool) * n);
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    queue<int> q;
    dis[S] = 0, q.emplace(S), inque[S] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, f = e[i].f, c = e[i].c;

            if (f && dis[v] < dis[u] + c) {
                dis[v] = dis[u] + c;

                if (!inque[v])
                    q.emplace(v), inque[v] = true;
            }
        }
    }

    return dis[T] != -inf;
}

int dfs(int u, int flow) {
    if (u == T) {
        maxcost += dis[T] * flow;
        return flow;
    }

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f, c = e[i].c;

        if (f && dis[v] == dis[u] + c && !vis[v]) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow)
                return vis[u] = false, flow;
        }
    }

    return outflow;
}
} // namespace Dinic

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    iota(id + 1, id + n + 1, 1);

    sort(id + 1, id + n + 1, [](const int &x, const int &y) {
        return a[x] < a[y];
    });

    for (int i = 1; i <= n; ++i)
        rka[id[i]] = i;

    for (int i = 1; i <= n; ++i)
        scanf("%d", b + i);

    sort(id + 1, id + n + 1, [](const int &x, const int &y) {
        return b[x] < b[y];
    });

    for (int i = 1; i <= n; ++i)
        rkb[id[i]] = i;

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        st[rka[u]].emplace(rkb[v]);
    }

    sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
    priority_queue<tuple<int, int, int> > q;

    for (int i = 1; i <= n; ++i) {
        p[i] = lower_bound(b + 1, b + n + 1, Mod - a[i]) - b;

        if (p[i] > 1)
            q.emplace(a[i] + b[p[i] - 1], i, p[i] - 1);

        if (p[i] <= n)
            q.emplace(a[i] + b[n] - Mod, i, n);
    }

    int S = n * 2 + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, 1, 0), Dinic::insert(i + n, T, 1, 0);

    for (int i = 1; i <= (k * 2 - 1) * (k - 1) + 1 && !q.empty(); ++i) {
        int x = get<1>(q.top()), y = get<2>(q.top());
        q.pop();

        if (y < p[x]) {
            if (st[x].find(y) == st[x].end() && cnta[x] < k && cntb[y] < k)
                Dinic::insert(x, y + n, 1, a[x] + b[y]), ++cnta[x], ++cntb[y];
            else
                --i;

            if (y > 1 && cnta[x] < k)
                q.emplace(a[x] + b[y - 1], x, y - 1);
        } else {
            if (st[x].find(y) == st[x].end() && cnta[x] < k && cntb[y] < k)
                Dinic::insert(x, y + n, 1, a[x] + b[y] - Mod), ++cnta[x], ++cntb[y];
            else
                --i;

            if (y > p[x] && cnta[x] < k)
                q.emplace(a[x] + b[y - 1] - Mod, x, y - 1);
        }
    }

    for (int i = 1; i <= k; ++i)
        printf("%lld ", Dinic::SPFA() ? (Dinic::dfs(S, 1), Dinic::maxcost) : -1);

    return 0;
}

上下界网络流

CF843E Maximum Flow

给出源点 \(S\) 、汇点 \(T\) 以及 \(m\) 条边 \((u_i, v_i, g_i)\) ,构造一个 \(n\) 个点 \(m\) 条边的网络满足第 \(i\) 条边为 \((u_i, v_i, f_i)\)\(f_i\) 可以任意选取),且存在一个最大流满足第 \(i\) 条边有流量当且仅当 \(g_i = 1\) 。构造一组满流边数量最少的方案,保证有解。

\(n \le 100\)\(m \le 1000\)

不难发现最小割的边数即为满流边数量,因此考虑残量网络的情况,建立网络流模型:

  • 对于 \(g_i = 0\) ,连 \((u_i, v_i, +\infty)\) ,表示 \((u_i, v_i)\) 在残量网络上一定连通。
  • 对于 \(g_i = 1\) ,连 \((u_i, v_i, 1)\)\((v_i, u_i, +\infty)\) ,第一条边表示可以花费 \(1\) 的代价割掉 \((u_i, v_i)\) ,第二条边表示 \((v_i, u_i)\) 在残量网络上一定连通。

跑最小割即可求出最小满流边数量,此时记与 \(S\) 相连的集合为 \(A\) ,则满流边满足 \(g_i = 1 \and u_i \in A \and v_i \not \in A\)

考虑构造方案,对于 \(g_i = 1\) 的边,连一条上下界为 \([1, +\infty]\) 的边,跑有源汇上下界可行流即可。

void dfs(int u) {
    vis[u] = true;

    for (int i = Dinic::head[u]; i; i = Dinic::e[i].nxt) {
        int v = Dinic::e[i].v, f = Dinic::e[i].f;

        if (f && !vis[v])
            dfs(v);
    }
}

signed main() {
    scanf("%d%d%d%d", &n, &m, &S, &T);
    Dinic::prework(n, S, T);

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].g);

        if (e[i].g)
            Dinic::insert(e[i].u, e[i].v, 1), Dinic::insert(e[i].v, e[i].u, inf);
        else
            Dinic::insert(e[i].u, e[i].v, inf);
    }

    printf("%d\n", Dinic::solve());
    dfs(S), NSTFlow::prework(n), Dinic::insert(T, S, inf);

    for (int i = 1; i <= m; ++i)
        if (e[i].g)
            NSTFlow::insert(e[i].u, e[i].v, 1, inf), eid[i] = Dinic::tot;

    NSTFlow::solve();

    for (int i = 1; i <= m; ++i) {
        if (e[i].g)
            printf("%d %d\n", Dinic::e[eid[i]].f + 1, vis[e[i].u] && !vis[e[i].v] ? Dinic::e[eid[i]].f + 1 : inf);
        else
            printf("0 %d\n", inf);
    }

    return 0;
}

最小割树

CF343E Pumping Stations

一个 \(n\) 个点 \(m\) 条边的无向图,求出一个排列 \(a_{1 \sim n}\) 使得

\[\sum_{i = 2} \operatorname{mincut}(a_{i - 1}, a_i) \]

最大,要求你输出最大值以及排列。

\(n \le 200\)\(m \le 10^3\)

可以发现的是,在最小割树建好以后,一定每条边都会对答案产生影响(必定有两个点会经过此边),那么最好情况就是每条边只被计算一次。

考虑分治。假如我们现在需要对当前的子树构造出一个最优排列,先找到边权最小的边,然后把该子树从此边分裂成两棵子树继续构造。

正确性:因为分裂出的子树 \(1\) 的末位和子树 \(2\) 的首位所产生的贡献必定是边权最小边。

namespace Tree {
struct MinEdge {
	int u, v, w;
	
	inline bool operator < (const MinEdge &rhs) const {
		return w < rhs.w;
	}
};

set<pair<int, int> > st;

MinEdge dfs(int u, int f) {
	MinEdge res = (MinEdge) {-1, -1, inf};
	
	for (auto it : G.e[u]) {
		int v = it.first, w = it.second;
		
		if (v == f || st.find(make_pair(u, v)) != st.end())
			continue;
		
		res = min(res, min((MinEdge) {u, v, w}, dfs(v, u)));
	}
	
	return res;
}

void solve(int u) {
	MinEdge res = dfs(u, 0);
	
	if (res.w == inf) {
		Answer.emplace_back(u);
		return;
	}
	
	st.insert(make_pair(res.u, res.v));
	st.insert(make_pair(res.v, res.u));
	ans += res.w;
	solve(res.u), solve(res.v);
}
} // namespace Tree

P3729 曼哈顿计划EX

给定一张无向图,每个点有点权。每次给定一个 \(x\) ,询问当限制选出的点集权值和 \(\ge x\) 时最大的整数 \(k\) ,满足点集内任意两点间均有至少 \(k\) 条不相交路径。

\(n \le 550, m \le 3000, q \le 2017, w \le 10^6\)

两个点之间不相交路径条数就可以使用最小割树查询出来(即两点间最小割)。

把询问离线下来,考虑枚举当前最小割为 \(w\) ,显然所有最小割树上边权比它大的都可以使用。对于当前可以使用的边,我们可以建出一个图。我们可以记录下来每个联通块的点权和(注意这些图上的联通块在原图上不一定是联通块)。

如果有联通块的权值和大于某个询问的 \(k\) ,那么该询问的答案就一定不小于 \(w\)

考虑这样做为什么是对的,因为边权比它小的都不会出现,在当前合法的情况下一定最优。

sort(qry + 1, qry + 1 + q, [](const Query &x, const Query &y) { return x.k < y.k; });
dsu.clear(n, val);
int res = *max_element(val + 1, val + 1 + n), cur = 1;

while (qry[cur].k <= res && cur <= q)
    qry[cur++].ans = 0;

for (int i = 1; i < n; ++i) {
    res = max(res, dsu.merge(GHT::G.e[i].u, GHT::G.e[i].v));

    while (qry[cur].k <= res && cur <= q)
        qry[cur++].ans = GHT::G.e[i].w;
}

最大权闭合子图

定义:有向图的子图,满足没有指向子图外的出边。

P2762 太空飞行计划问题

\(n\) 个实验和 \(m\) 台仪器。每个实验会获得若干赞助商的赞助费用,同时需要若干仪器,而每台仪器需要花费若干价钱购买。求总收益最大的实验方案。

\(n, m \le 50\)

考虑最小割建模。

  • 正价点连源,流量为点权。
  • 负价点连汇,流量为负点权。
  • 原图中的有向边保留,流量为 \(+ \infty\)

此时有:

\[最大权闭合子图 = 正点权和 - 最小割 \]

构造:割掉的负权点表示选择,割掉的正权点表示不选。即所选的点为残量网络上 \(S\) 能到达的点。

证明考虑构造割与闭合子图的双射。

考虑选点的过程一定是从一个正权点开始不断选他的后继。若选了正权点 \(u\)

  • 存在一个负权点后继 \(v\) :那么必须割掉 \(v \to T\) 的边才能保证 \(S\) 不连通,即选 \(v\)
  • 存在一个正权点后继 \(v\) :那么存在 \(S \to u \to v\) 的路径,完全没必要割掉 \(S \to v\)
signed main() {
    n = read(), m = read();
    int S = n + m + 1, T = S + 1, ans = 0;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i) {
        int w = read();
        ans += w, Dinic::insert(S, i, w);
        w = 0;

        for (char c = getchar(); c != '\n'; c = getchar()) {
            if ('0' <= c && c <= '9')
                w = (w << 1) + (w << 3) + (c & 15);
            else
                Dinic::insert(i, n + w, inf), w = 0;
        }

        Dinic::insert(i, n + w, inf);
    }

    for (int i = 1; i <= m; ++i)
        Dinic::insert(n + i, T, read());

    ans -= Dinic::solve();

    for (int i = 1; i <= n; ++i)
        if (Dinic::dep[i])
            printf("%d ", i);

    puts("");

    for (int i = 1; i <= m; ++i)
        if (Dinic::dep[n + i])
            printf("%d ", i);

    printf("\n%d", ans);
    return 0;
}

最大密度子图

定义:在图 \(G = (V, E)\) 中使得 \(\dfrac{|E'|}{|V'|}\) 最大化的闭合子图。

UVA1389 Hard Life

求最大密度子图。

\(n \le 100\)\(m \le 1000\)

考虑考虑 01 分数规划,二分比值 \(g\)

\[\frac{|E'|}{|V'|} \ge g \iff |E'| - g \times |V'| \ge 0 \]

要使 \(|E'| - g \times |V'|\) 最大,考虑最大权闭合子图。不妨将边和点都看作网络图中的节点。将边的点权设为 \(1\),点的点权设为 \(-g\)。然后求最大权闭合子图看权是否非负即可。

  • \(S\) 向边节点连容量为 \(1\) 的边。

  • 点节点向 \(T\) 连容量为 \(g\) 的边。

  • 边节点向其两个端点连容量为 \(+ \infty\) 的边,表示选了边就必须选两端的节点。

得到最大密度后,根据这个密度建图,再求一遍最大权闭合子图。此时从源点能流到的节点构成最大权闭合子图,即所求最大密度子图。

注意由于边的容量为实数,因此判断一条边是否流完的时候的条件是 \(w > eps\)

然而,这样求最大密度子图的时候会出现本来不在最大密度子图中的节点被遍历到。因此要考虑排除这些点的贡献。

首先,表示边的节点是没有贡献的,其次汇点也是没有贡献的。对于剩下的那些表示点的节点,在最大密度子图中的点的贡献自然要算上;不在其中的点,由于边的容量小于 \(eps\),因此计算它的贡献带来的误差是极其微小的,算上也没关系。

将上述有贡献的节点输出即为答案。

inline bool check(double k) {
    int S = 0, T = m + n + 1;
    Dinic::prework(S, T);
    
    for (int i = 1; i <= m; ++i) {
        Dinic::insert(S, i, 1);
        Dinic::insert(i, e[i].u + m, inf);
        Dinic::insert(i, e[i].v + m, inf);
    }
    
    for (int i = m + 1; i <= m + n; ++i)
        Dinic::insert(i, T, k);

    return (double)m - Dinic::solve() > eps;
}

signed main() {
    while (~scanf("%d%d", &n, &m)) {
        if (!m) {
            puts("1\n1");
            continue;
        }
        
        for (int i = 1; i <= m; ++i)
            e[i].u = read(), e[i].v = read();
        
        double l = 0, r = m, res = 0;
        
        while (r - l > eps) {
            double mid = (l + r) / 2;
            
            if (check(mid))
                res = l = mid;
            else
                r = mid;
        }
        
        check(res);
        int ans = 0;

        for (int i = 1; i <= n; ++i)
            if (Dinic::dep[m + i])
                ++ans;

        printf("%d\n", ans);
        
        for (int i = 1; i <= n; ++i)
            if (Dinic::dep[m + i])
                printf("%d\n", i);
        
        puts("");
    }
    
    return 0;
}

混合图欧拉回路

UVA10735 混合图的欧拉回路 Euler Circuit

给出一张连通图,其中有些边是有向边,有些边是无向边,求一条欧拉回路,或告知无解。

\(n \le 100\)\(m \le 500\)

首先问题有解当且仅当存在一组给无向边定向的方案使图存在欧拉回路。

考虑判定欧拉回路的条件:图弱连通且所有点的入度等于出度。

一条有向边对一个点的入度和出度是固定的,而无向边有两种选择。考虑先给无向边随意指定一个方向,然后再调整。

\(d_i\) 表示此时 \(i\) 的出度减去入度,反转一条无向边 \((u, v)\) 时会令 \(d_u \gets d_u - 2, d_v \gets d_v + 2\) ,因此若存在 \(d\) 为奇数的点则无解。

判掉无解后令 \(d_i\) 表示 \(i\) 的出度减去入度的差的一半,则反转一条无向边 \((u, v)\) 时会令 \(d_u \gets d_u - 1, d_v \gets d_v + 1\) ,即将 \(u\) 的度分 \(1\)\(v\)

考虑网络流建模:

  • 对于 \(d_u > 0\) 的点,从源点向其连边,流量为 \(d_u\) ,表示它需要分出 \(d_u\) 的度。
  • 对于 \(d_u < 0\) 的点,将其向汇点连边,流量为 \(-d_u\) ,表示它需要 \(d_u\) 的度。
  • 对于一条无向边 \((u, v)\) ,连 \((u, v, 1)\) ,表示可以反转这条边使得 \(u\) 的度分 \(1\)\(v\)

若最大流等于所有正的 \(d\) 的和则合法,此时有流量的边就是要反转的边,最后跑一次欧拉回路即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7, M = 2e3 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

tuple<int, int, bool> e[M];

int d[N], eid[M];

int n, m;

namespace Dinic {
struct Edge {
    int nxt, v, f;
} e[M];

int head[N], cur[N], dep[N];
bool vis[N];

int n, S, T, tot, maxflow;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f) {
    e[++tot] = (Edge){head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, 0}, head[v] = tot;
}

inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, f = e[i].f;

            if (f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }

    return dep[T];
}

int dfs(int u, int flow) {
    if (u == T)
        return flow;

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;

        if (f && dep[v] == dep[u] + 1 && !vis[v]) {
            int res = dfs(v, min(flow - outflow, f));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow) {
                vis[u] = false;
                return outflow;
            }
        }
    }

    return outflow;
}

inline int solve() {
    maxflow = 0;

    while (bfs())
        maxflow += dfs(S, inf);

    return maxflow;
}
} // namespace Dinic

void Hierholzer(int u) {
    while (!G.e[u].empty()) {
        int v = G.e[u].back();
        G.e[u].pop_back(), Hierholzer(v);
    }

    printf("%d ", u);
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);
        memset(d + 1, 0, sizeof(int) * n);

        for (int i = 1; i <= m; ++i) {
            int u, v;
            char op[2];
            scanf("%d%d%s", &u, &v, op);
            e[i] = make_tuple(u, v, op[0] == 'U'), ++d[u], --d[v];
        }

        bool flag = true;

        for (int i = 1; i <= n; ++i)
            if (d[i] & 1) {
                flag = false;
                break;
            }

        if (!flag) {
            puts("No euler circuit exist");

            if (T)
                puts("");

            continue;
        }

        int S = n + 1, T = n + 2, sum = 0;
        Dinic::prework(T, S, T);

        for (int i = 1; i <= n; ++i) {
            d[i] /= 2;

            if (d[i] > 0)
                Dinic::insert(S, i, d[i]), sum += d[i];
            else if (d[i] < 0)
                Dinic::insert(i, T, -d[i]);
        }

        for (int i = 1; i <= m; ++i)
            if (get<2>(e[i]))
                Dinic::insert(get<0>(e[i]), get<1>(e[i]), 1), eid[i] = Dinic::tot;

        if (Dinic::solve() != sum) {
            puts("No euler circuit exist");

            if (T)
                puts("");

            continue;
        }

        for (int i = 1; i <= m; ++i) {
            if (get<2>(e[i]) && Dinic::e[eid[i]].f)
                G.insert(get<0>(e[i]), get<1>(e[i]));
            else
                G.insert(get<1>(e[i]), get<0>(e[i]));
        }

        Hierholzer(1), puts("");

        if (T)
            puts("");
    }

    return 0;
}

P3511 [POI2010] MOS-Bridges

给定一张无向图,边带权且正着走和逆着走有不同权值。求从 \(1\) 出发的最大边权最小的欧拉回路。

\(n \le 10^3\)\(m \le 2 \times 10^3\)

二分答案后转化为混合图(有向+无向)的欧拉回路求解。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e3 + 7, M = 2e3 + 7;

struct Graph {
    vector<pair<int, int> > e[N];

    int cur[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

struct Edge {
    int u, v, w1, w2, id;
} e[M];

stack<int> Answer;

int deg[N];

int n, m;

namespace Dinic {
const int N = 3e3 + 7, M = 7e3 + 7;

struct Edge {
    int nxt, v, f;
} e[M << 1];

int head[N], cur[N], dep[N];
bool vis[N];

int n, S, T, tot, maxflow;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f) {
    e[++tot] = (Edge) {head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge) {head[v], u, 0}, head[v] = tot;
}

inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, f = e[i].f;

            if (f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }

    return dep[T];
}

int dfs(int u, int flow) {
    if (u == T)
        return flow;

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;

        if (f && dep[v] == dep[u] + 1 && !vis[v]) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow)
                break;
        }
    }

    if (outflow == flow)
        vis[u] = false;

    return outflow;
}

inline int solve() {
    maxflow = 0;

    while (bfs())
        maxflow += dfs(S, inf);

    return maxflow;
}
} // namespace Dinic

inline bool check(int lambda) {
    int S = m + n + 1, T = m + n + 2;
    Dinic::prework(m + n + 2, S, T);

    for (int i = 1; i <= m; ++i)
        Dinic::insert(S, i, 1);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(i + m, T, deg[i] / 2);

    for (int i = 1; i <= m; ++i) {
        if (e[i].w1 <= lambda)
            Dinic::insert(i, e[i].u + m, 1), e[i].id = Dinic::tot;
        else
            e[i].id = -1;

        if (e[i].w2 <= lambda)
            Dinic::insert(i, e[i].v + m, 1);
    }

    Dinic::solve();
    return Dinic::maxflow == m;
}

void hierholzer(int u, int pre) {
    for (int i = G.cur[u]++; i < G.e[u].size(); i = G.cur[u]++)
        hierholzer(G.e[u][i].first, G.e[u][i].second);

    if (pre)
        Answer.emplace(pre);
}

signed main() {
    scanf("%d%d", &n, &m);
    int l = 1e3, r = 1;

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].w1, &e[i].w2);
        ++deg[e[i].u], ++deg[e[i].v];
        l = min(l, min(e[i].w1, e[i].w2)), r = max(r, max(e[i].w1, e[i].w2));
    }
    
    for (int i = 1; i <= n; ++i)
        if (deg[i] & 1)
            return puts("NIE"), 0;

    int ans = r;

    while (l <= r) {
        int mid = (l + r) >> 1;

        if (check(mid))
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    check(ans);
    printf("%d\n", ans);

    for (int i = 1; i <= m; ++i) {
        if (~e[i].id && Dinic::e[e[i].id].f)
            G.insert(e[i].u, e[i].v, i);
        else
            G.insert(e[i].v, e[i].u, i);
    }

    hierholzer(1, 0);

    while (!Answer.empty())
        printf("%d ", Answer.top()), Answer.pop();

    return 0;
}

隐式图

P2765 魔术球问题

\(n\) 根柱子,现要按下述规则在这 \(n\) 根柱子中依次放入编号为 \(1, 2, 3, \cdots\) 的球

  • 每次只能在某根柱子的最上面放球。

  • 同一根柱子中,任何两个相邻球的编号之和为完全平方数。

求最多能放多少个球,并构造方案。

\(n \le 55\)

考虑枚举点数,若两个点和为完全平方数则连边,判定最小路径点覆盖是否 \(\le n\) 即可。

signed main() {
    scanf("%d", &n);
    int S = 0, T = 1, ans = 0;
    Dinic::prework(S, T);

    while (++ans) {
        Dinic::insert(S, ans * 2, 1), Dinic::insert(ans * 2 + 1, T, 1);

        for (int i = 1; i < ans; ++i) {
            int sq = sqrt(ans + i);

            if (sq * sq == ans + i)
                Dinic::insert(i * 2, ans * 2 + 1, 1);
        }

        if (ans - Dinic::solve() > n)
            break;
    }

    printf("%d\n", --ans);
    Dinic::prework(S, T);

    for (int i = 1; i <= ans; ++i) {
        Dinic::insert(S, i * 2, 1), Dinic::insert(i * 2 + 1, T, 1);

        for (int j = 1; j < i; ++j) {
            int sq = sqrt(i + j);

            if (sq * sq == i + j)
                Dinic::insert(j * 2, i * 2 + 1, 1);
        }
    }

    Dinic::solve();
    int tot = 0;

    for (int i = 1; i <= ans; ++i) {
        if (!id[i])
            id[i] = ++tot;

        vec[id[i]].emplace_back(i);

        for (int j = Dinic::head[i * 2]; j; j = Dinic::e[j].nxt)
            if (!Dinic::e[j].f && Dinic::e[j].v >= 2)
                id[Dinic::e[j].v / 2] = id[i];
    }

    for (int i = 1; i <= tot; ++i) {
        for (int it : vec[i])
            printf("%d ", it);

        puts("");
    }

    return 0;
}

枚举法

UVA1104 芯片难题 Chips Challenge

有一个 \(n \times n\) 的棋盘,有些格子不能放棋子,有些格子必须放棋子,剩下的格子随意。要求放好棋子之后满足:

  • \(i\) 行和第 \(i\) 列的棋子数相同。
  • 任何一行的棋子数不能超过总的棋子数目的 \(\frac{A}{B}\)

求最多可以另外放多少个棋子。

\(n \le 40\)

考虑放满整个图后删点,一个想法是令 \(x_i\) 表示第 \(i\) 行,\(y_i\) 表示第 \(i\) 列。

由于第二个限制和总棋数有关,不要直接处理,于是考虑枚举限制每行最多能放的棋子数,最后求最大总棋数判定合法性。

接下来考虑第一个限制,设 \(X_i, Y_i\) 表示第 \(i\) 行/列放满时的棋子数,\(k\) 为枚举的限制。考虑如下建模:

  • \(S\)\(x_i\) 连边,流量为 \(X_i\) ,费用为 \(0\)
  • \(y_i\)\(T\) 连边,流量为 \(Y_i\) ,费用为 \(0\)
  • \(x_i\)\(y_i\) 连边,容量为 \(k\) ,费用为 \(0\)
  • 对于每个可以选择放或不放的点 \((i, j)\)\(x_i\)\(y_j\) 连容量为 \(1\) ,费用为 \(1\) 的边。

跑最小费用最大流,最小费用即为删去的点数。一个方案合法当且仅当 \(S \to x_i\)\(y_i \to T\) 的边均满流且剩余棋子数 \(\ge \frac{B}{A} \times k\)

signed main() {
    for (int task = 1;; ++task) {
        scanf("%d%d%d", &n, &A, &B);

        if (!n && !A && !B)
            break;

        int sum = 0, cntc = 0;

        for (int i = 1; i <= n; ++i) {
            scanf("%s", a[i] + 1);
            sum += n - count(a[i] + 1, a[i] + n + 1, '/');
            cntc += count(a[i] + 1, a[i] + n + 1, 'C');
        }

        memset(Y + 1, 0, sizeof(int) * n);
        memset(X + 1, 0, sizeof(int) * n);

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                if (a[i][j] != '/')
                    ++X[i], ++Y[j];

        int S = n * 2 + 1, T = S + 1, ans = -1;

        for (int lim = 0; lim <= n; ++lim) {
            Dinic::prework(T, S, T);

            for (int i = 1; i <= n; ++i) {
                Dinic::insert(S, i, X[i], 0);
                Dinic::insert(i, n + i, lim, 0);
                Dinic::insert(n + i, T, Y[i], 0);
            }

            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= n; ++j)
                    if (a[i][j] == '.')
                        Dinic::insert(i, n + j, 1, 1);

            Dinic::solve();

            if (Dinic::maxflow == sum && lim * B <= (sum - Dinic::mincost) * A)
                ans = max(ans, sum - Dinic::mincost - cntc);
        }

        printf("Case %d: ", task);

        if (ans == -1)
            puts("impossible");
        else
            printf("%d\n", ans);
    }

    return 0;
}

二分法

P2402 奶牛隐藏

给定一张无向图,每个点初始有 \(s_i\) 头牛,其牛棚有 \(p_i\) 的容纳量。下雨时牛必须全部躲进牛棚,求使得所有牛都躲进牛棚的最小时间,或报告无解。

\(n \le 200\)\(m \le 1500\)

先用 Floyd 预处理任意两点最短路。发现直接做不好处理所有牛可以同时移动的限制,考虑二分答案。每次将每个点与距离 \(\le mid\) 的点连边,跑网络流。但是直接跑会出现牛沿着走很多条连着的边的情况,考虑拆点,每个点拆为牛和棚即可。具体建模如下:

  • \(S\) 向牛连流量为 \(s_i\) 的边。
  • 牛向棚连流量为 \(+ \infty\) 的边。
  • 棚向 \(T\) 连流量为 \(p_i\) 的边。
  • 对于距离 \(\le mid\) 的两点 \(u \to v\) ,从 \(u\) 牛向 \(v\) 棚连流量为 \(+\infty\) 的边。

最大流等于总牛数则合法。

inline bool check(ll k) {
    int S = n * 2 + 1, T = n * 2 + 2;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, s[i]), Dinic::insert(i, i + n, inf), Dinic::insert(i + n, T, p[i]);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            if (i != j && dis[i][j] <= k)
                Dinic::insert(i, n + j, inf);

    return Dinic::solve() == sum;
}

signed main() {
    n = read(), m = read();

    for (int i = 1; i <= n; ++i)
        sum += (s[i] = read<ll>()), p[i] = read<ll>();

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            if (i != j)
                dis[i][j] = inf;

    for (int i = 1; i <= m; ++i) {
        int u = read(), v = read();
        dis[u][v] = dis[v][u] = min(dis[u][v], read<ll>());
    }

    for (int k = 1; k <= n; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);

    ll l = 0, r = 2e17, ans = -1;

    while (l <= r) {
        ll mid = (l + r) >> 1;

        if (check(mid))
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }

    printf("%lld", ans);
    return 0;
}

其他

HDU5639 Deletion

给出一个无向图,每次可以删去一个边集满足其组成的每个连通块至多一个环,求删掉所有边的最少操作次数。

\(n, m \le 2000\)

可以发现每次删掉的边集的每个连通块形成一棵树或基环树,注意到这种结构均存在一种给边定向的方案使得每个点的出度 \(\le 1\) 。问题转化为将每条边定向,最小化最大出度,不难用网络流解决。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 7;

struct Edge {
    int u, v;
} e[N];

int n, m;

namespace Dinic {
const int N = 4e3 + 7, M = 1e5 + 7;

struct Edge {
    int nxt, v, f;
} e[M];

int head[N], cur[N], dep[N];
bool vis[N];

int n, S, T, tot, maxflow;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f) {
    e[++tot] = (Edge) {head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge) {head[v], u, 0}, head[v] = tot;
}

inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, f = e[i].f;

            if (f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }

    return dep[T];
}

int dfs(int u, int flow) {
    if (u == T)
        return flow;

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;

        if (!vis[v] && dep[v] == dep[u] + 1 && f) {
            int res = dfs(v, min(flow - outflow, f));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow)
                break;
        }
    }

    if (outflow == flow)
        vis[u] = false;

    return outflow;
}

inline int solve() {
    maxflow = 0;

    while (bfs())
        maxflow += dfs(S, inf);

    return maxflow;
}
} // namespace Dinic

inline bool check(int k) {
    int S = n + m + 1, T = n + m + 2;
    Dinic::prework(n + m + 2, S, T);

    for (int i = 1; i <= m; ++i)
        Dinic::insert(S, i, 1), Dinic::insert(i, e[i].u + m, 1), Dinic::insert(i, e[i].v + m, 1);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(i + m, T, k);

    return Dinic::solve() == m;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);

        for (int i = 1; i <= m; ++i)
            scanf("%d%d", &e[i].u, &e[i].v);

        int l = 1, r = m, ans = m;

        while (l <= r) {
            int mid = (l + r) >> 1;

            if (check(mid))
                ans = mid, r = mid - 1;
            else
                l = mid + 1;
        }

        printf("%d\n", ans);
    }

    return 0;
}

CF1592F2 Alice and Recoloring 2

有一个 \(n \times m\) 的矩阵,初始全为 \(0\) ,可以执行若干次操作,操作有:

  • 将一个包含 \((1, 1)\) 的矩阵异或 \(1\) ,代价为 \(1\)
  • 将一个包含 \((n, m)\) 的矩阵异或 \(1\) ,代价为 \(2\)
  • 将一个包含 \((n, 1)\) 的矩阵异或 \(1\) ,代价为 \(3\)
  • 将一个包含 \((1, m)\) 的矩阵异或 \(1\) ,代价为 \(4\)

求将其变为目标矩阵的最小代价。

\(n, m \le 500\)

为便于分析,考虑将问题转化为目标矩阵变为全 \(0\) 的代价。

首先可以发现后两个操作是没有用的,操作三可以通过两次操作一差分得到,操作四可以通过两次操作二差分得到,而代价显然不劣。

子矩阵异或是困难的,考虑构造新矩阵 \(b_{i, j} = a_{i, j} \oplus a_{i + 1, j} \oplus a_{i, j + 1} \oplus a_{i + 1, j + 1}\) ,其中矩阵外的值均为 \(0\) ,这样操作一相当于单点异或,操作二相当于将一个包含 \((n, m)\) 的矩阵的四个角做异或操作,目标仍为将新矩阵消成 \(0\)

注意到不会同时做两次行相等或列相等的操作二,这是因为这两次操作只翻转了四个格子,可以用操作一代替。

于是若干次操作二影响的行和列是独立的,此时可以发现只有在 \((x, y), (n, y), (m, x)\) 均为 \(1\) 时才会使用操作二,这是因为否则至少有一个格子要用操作一翻转回来,不如用操作一依次消去。

考虑将行和列建点,将满足操作二条件的行和列连边,跑最大匹配即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e2 + 7;

char str[N][N];
bool a[N][N];

int n, m;

namespace Dinic {
const int N = 1e3 + 7, M = 1e6 + 7;

struct Edge {
    int nxt, v, f;
} e[M];

int head[N], cur[N], dep[N];
bool vis[N];

int n, S, T, tot, maxflow;

inline void prework(int _n, int _S, int _T) {
    n = _n, S = _S, T = _T, tot = 1, maxflow = 0;
    memset(head + 1, 0, sizeof(int) * n);
}

inline void insert(int u, int v, int f) {
    e[++tot] = (Edge){head[u], v, f}, head[u] = tot;
    e[++tot] = (Edge){head[v], u, f}, head[v] = tot;
}

inline bool bfs() {
    memcpy(cur + 1, head + 1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(dep + 1, 0, sizeof(int) * n);
    queue<int> q;
    dep[S] = 1, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].v, f = e[i].f;

            if (f && !dep[v])
                dep[v] = dep[u] + 1, q.emplace(v);
        }
    }

    return dep[T];
}

int dfs(int u, int flow) {
    if (u == T)
        return flow;

    vis[u] = true;
    int outflow = 0;

    for (int &i = cur[u]; i; i = e[i].nxt) {
        int v = e[i].v, f = e[i].f;

        if (f && !vis[v] && dep[v] == dep[u] + 1) {
            int res = dfs(v, min(f, flow - outflow));
            e[i].f -= res, e[i ^ 1].f += res, outflow += res;

            if (outflow == flow)
                return vis[u] = false, outflow;
        }
    }

    return outflow;
}

inline int solve() {
    while (bfs())
        maxflow += dfs(S, inf);

    return maxflow;
}
} // namespace Dinic

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%s", str[i] + 1);

    int S = n + m + 1, T = S + 1;
    Dinic::prework(T, S, T);

    for (int i = 1; i <= n; ++i)
        Dinic::insert(S, i, 1);

    for (int i = 1; i <= m; ++i)
        Dinic::insert(n + i, T, 1);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            a[i][j] = (str[i][j] == 'B') ^ (str[i + 1][j] == 'B') ^ 
                (str[i][j + 1] == 'B') ^ (str[i + 1][j + 1] == 'B');

    for (int i = 1; i < n; ++i)
        for (int j = 1; j < m; ++j)
            if (a[i][j] && a[n][j] && a[i][m])
                Dinic::insert(i, n + j, 1);

    int ans = -Dinic::solve();

    if (ans & 1)
        a[n][m] ^= 1;

    for (int i = 1; i <= n; ++i)
        ans += count(a[i] + 1, a[i] + m + 1, true);

    printf("%d", ans);
    return 0;
}
posted @ 2024-09-29 21:20  wshcl  阅读(99)  评论(0)    收藏  举报