二分图与网络流

二分图与网络流

二分图

定义:可以将点集划分为左部 \(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 定理:最小点覆盖数 = 最大匹配数。

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

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

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

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

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

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

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

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

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

CF1948G MST with Matching

给定一张 \(n\) 个点的图和常数 \(c\) ,定义一棵生成树的权值为边权和加上 \(c\) 倍的最大匹配数,求该图的最小生成树。

\(n \le 20\)\(1 \le c \le 10^6\)

考虑将最大匹配数转化为最小点覆盖数,然后枚举所有的点覆盖,则只会保留至少有一端属于该点覆盖的边,直接求最小生成树即可,显然钦定的一组点覆盖不小于实际的点覆盖,时间复杂度 \(O(m \log m + 2^n m)\)

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

struct Edge {
    int u, v, w;

    inline bool operator < (const Edge &rhs) const {
        return w < rhs.w;
    }
} e[N * N];

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

int n, c, m;

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

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

            if (i < j && w)
                e[++m] = (Edge){i, j, w};
        }

    sort(e + 1, e + m + 1);
    ll ans = 1e18;

    for (int state = 0; state < (1 << n); ++state) {
        dsu.prework(n);
        ll res = 1ll * __builtin_popcount(state) * c;
        int cnt = 0;

        for (int i = 1; i <= m && cnt < n - 1; ++i) {
            if ((~state >> (e[i].u - 1) & 1) && (~state >> (e[i].v - 1) & 1))
                continue;

            int u = dsu.find(e[i].u), v = dsu.find(e[i].v);

            if (u == v)
                continue;

            res += e[i].w, ++cnt, dsu.merge(u, v);
        }

        if (cnt == n - 1)
            ans = min(ans, res);
    }

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

二分图最大独立集

独立集:一个点集 \(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)\)

posted @ 2024-09-29 21:20  wshcl  阅读(221)  评论(0)    收藏  举报