【学习笔记】二分图

当遇到「匹配题」或者「二维平面 / grid 中的行列限制 / 黑白染色」类题,常常使用二分图算法。

二分图最大匹配 / 最小点覆盖 / 最大独立集 / 最小边覆盖

所谓最大匹配,就是选最多的边,使得任意两条边不具有公共端点。

所谓最小点覆盖,就是选最少的点,使得每条边至少有一个端点被选。

所谓最大独立集,就是选择最多的点,使得它们两两间没有边直接相连。

所谓最小边覆盖,就是选择最少的边,使得覆盖到所有的点。

对于所有二分图,都有:\(|最大匹配|=|最小点覆盖|=点数-|最大独立集|=点数-|最小边覆盖|\)。证明在后面。

匈牙利算法

思路

算法核心:找“增广路”

遍历所有左侧点,每次进行以下流程:

  1. 尝试去寻找一个右侧点来匹配;
  2. 若该右侧点还没有匹配的左侧点,则找到了,回溯。否则进入该右侧点的匹配左侧点,回到1。

由于每次的 dfs 找匹配点是 $ \mathcal{O}(n+m)$ 的,因此匈牙利算法的时间复杂度为 $ \mathcal{O}(n(n+m))$。

代码

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

点击查看代码
const int N = 505;
int n, m, tag;
vector<int> g[N];
int match[N], vis[N];
int ans;

bool dfs(int u)
{
    vis[u] = tag;
    for (auto &&v : g[u])
        if (!match[v] || vis[match[v]] != tag && dfs(match[v])) // 要么v没有匹配点,要么v成功找到其他匹配点
        {
            match[v] = u;
            return true;
        }
    return false;
}

int main()
{
    cin >> n >> x >> m;
    int u, v;
    for (int i = 1; i <= m; i++)
        scanf("%d%d", &u, &v), g[u].push_back(v);
    for (int i = 1; i <= n; i++)
    {
        ++tag; // 每轮的tag不一样,vis与本轮的tag相同就访问过,这样免掉了每轮vis清0
        ans += dfs(i); // 为true表示成功匹配,否则失败
    }
    cout << ans << endl;
    return 0;
}

网络流

前置知识:网络流

对每个左侧点 \(u\)\(s \xrightarrow{1} u\),右侧点 \(v\)\(v \xrightarrow{1} t\),对于左侧点到右侧点的连边连 \(u\xrightarrow{\infty}v\),跑最大流即可。

流量为 \(1\)\(\infty\) 边就是选择的匹配。

根据时间复杂度分析,这个网络可以看成各容量为 1 的网络(inf 边可以改成 1),因此可以使用 \(O(m\min\{m^{\frac{1}{2}},n^{\frac{2}{3}}\})\)

但是!我们注意到还有一个 \(O(\sqrt{\sum \min\{in_u,out_u\}}\sum w_i)\),因此可以分析成 \(O(m\sqrt{n})\)!这是比匈牙利快的。

构造

最大匹配 / 最小边覆盖

匈牙利算法的构造就是 match 数组。

网络流,流量为 \(1\)\(\infty\) 边就是选择的匹配。

当确定了一个最大匹配,注意到选择一个匹配能覆盖 2 个点,于是贪心地先选择所有匹配,然后再挨个覆盖其他点,恰好能取到下界 \(n-|最大匹配|\)

最大独立集 / 最小点覆盖

考虑最小割的定义。

\[\sum_{E} w_ix_{u}(1-x_{v}) \]

其中 \(x_i\) 是布尔变量,表示在 \(S\) 连通块还是 \(T\)

而求解最大独立集可以看成最小化:

\[\sum -x_u +\sum_{E} \infty x_u x_v \]

其中 \(x_i\) 表示选没选。

对于一般图,难以转化成最小割,毕竟一般图最大匹配是 NP 问题。但是二分图的边都是左连向右,因此可以规定右边的点的 \(x\) 取反。转化成:

\[\sum_{u\in L} -x_u + \sum_{v\in R} (x_v-1) + \sum_{E} \infty x_u(1-x_v) =\sum_{u\in L}1(1-x_u)-|L|+\sum_{v\in R}x_v(1-0)-|R|+\sum_{E} \infty x_u(1-x_v) \]

恰好转为了最小割,\(x_s=1,x_t=0\)

于是建图 \(s\xrightarrow{1}u,u\xrightarrow{\infty}v,v\xrightarrow{1}t\)(同最大匹配),方案为最小割中 \(s\) 内的左部点和 \(t\) 内的右部点。

妙哉!

最小点覆盖就是最大独立集的补集。最大独立集中每个边至多有一个端点被选,所以补集中每个边至少有一个端点被选,就是最小点覆盖。

证明

证明:\(|最大匹配|=|最小点覆盖|=n-|最大独立集|=n-|最小边覆盖|\)


首先我们证明 \(|最大匹配|=|最小点覆盖|,|最大独立集|=|最小边覆盖|\)

发现对于最小点覆盖,多个匹配不可能被同一个点覆盖,因此 \(|最大匹配|\le |最小点覆盖|\),而根据上面的构造,下界可以取到,因此 \(|最大匹配|=|最小点覆盖|\)

对于最小边覆盖,多个独立集不可能被同一条边覆盖,因此 \(|最大独立集|\le |最小边覆盖|\),而根据上面的构造,下界可以取到,因此 \(|最大独立集|=|最小边覆盖|\)

然后证明 \(|最大匹配|=n-|最大独立集|\)

对于建图 \(s\xrightarrow{1}u,u\xrightarrow{\infty}v,v\xrightarrow{1}t\),最大匹配大小就是最大流。

而最大独立集大小是 \(\sum -x_u +\sum_{E} \infty x_u x_v\) 的最小值的相反数。

也就是 \(\sum_{u\in L}1(1-x_u)+\sum_{v\in R}x_v(1-0)+\sum_{E} \infty x_u(1-x_v)-n\) 的相反数的最大值。

也就是 \(n-(\sum_{u\in L}1(1-x_u)+\sum_{v\in R}x_v(1-0)+\sum_{E} \infty x_u(1-x_v))\) 的最大值。

也就是 \(n-最小割\)。也就是 \(n-最大流\)。也就是 \(n-|最大匹配|\)。Q.E.D.

题:[HNOI2013]消毒

有一个 \(a\times b\times c\) 的立方体,其中有一些格子需要消毒。

你可以使用一种消毒剂,花费 \(\min\lbrace x,y,z \rbrace\) 代价,给一个 \(x \times y \times z\) 的立方体消毒。

求将整个立方体消毒干净的最小代价。


Easy version

首先从简单的情况考虑,想想二维怎么做。

假设我们选择了一个长为 \(x\),宽为 \(y\) 的矩形(\(x>y\)),那么无论 \(x\) 如何变化,代价不变。因此 \(x\) 取最大值一定最优。

问题便转化成了在一个矩形上刷一行或一列,覆盖所有点的最小刷的次数。

即「选择最少的行、列,包含所有要选的点」,一眼最小点覆盖。

对每个行、列建点。若该格子需要消毒,则链接行点、列点。

最后跑一遍匈牙利即可。时间复杂度 \(\mathcal{O}((a+b)\times[(a+b)+ab])\)

Hard version

在立方体上,利用刚刚的结论,我们可以刷掉一些层。

然后我们可以把剩下的层拿出来,拍扁成一个二维矩形,就变成了 Easy version。

至于刷掉哪些层,暴力枚举即可。

注意要选用最短的棱长枚举。不妨设 \(a\leq b\leq c\),因为 \(a\times b\times c \leq 5\times 10^3\),所以可以用反证法证明 \(a<=17\)

时间复杂度 \(\mathcal{O}(2^a\times (b+c)\times[(b+c)+bc])\)。(\(a\leq b\leq c\)

瓶颈是 \(a=b=c=17\),数量级约为 \(1.3 \times 10^9\),但是卡不满匈牙利。可以通过。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 5;
int a, b, c, mark[N][N], ans;
vector<pair<int, int> > pos[N];
int vis[N], match[N], tag;
vector<int> g[N];

bool Hungary(int u)
{
    vis[u] = tag;
    for (auto v : g[u])
        if (!match[v] || vis[match[v]] != tag && Hungary(match[v]))
        {
            match[v] = u;
            return true;
        }
    return false;
}

int cal()
{
    for (int i = 1; i <= b; i++)
    {
        vis[i] = 0;
        g[i].clear();
        for (int j = 1; j <= c; j++)
            if (mark[i][j])
                g[i].push_back(j);
    }
    for (int i = 1; i <= c; i++)
        match[i] = 0;
    int res = 0;
    for (int i = 1; i <= b; i++)
    {
        tag = i;
        if (Hungary(i))
            res++;
    }
    return res;
}

void dfs(int dep, int cnt) // 暴搜应该刷掉哪些层
{
    if (dep > a)
    {
        ans = min(ans, cal() + cnt);
        return;
    }
    dfs(dep + 1, cnt + 1);
    for (auto i : pos[dep])
        mark[i.first][i.second]++; // 记录哪些格子需要消毒
    dfs(dep + 1, cnt);
    for (auto i : pos[dep])
        mark[i.first][i.second]--;
}

void solve()
{
    int x;
    pair<int, pair<int, int> > t[5];
    scanf("%d%d%d", &a, &b, &c);
    for (int i = 1; i <= 20; i++)
        pos[i].clear();
    for (int i = 1; i <= a; i++)
        for (int j = 1; j <= b; j++)
            for (int k = 1; k <= c; k++)
            {
                scanf("%d", &x);
                if (!x)
                    continue;
                t[1] = {a, {1, i}}, t[2] = {b, {2, j}}, t[3] = {c, {3, k}}; //pair套pair是为了防止棱长相同时,由于下标不同导致的交换
                sort(t + 1, t + 4); // 小的棱长对应小的下标
                pos[t[1].second.second].push_back({t[2].second.second, t[3].second.second});
            }
    a = t[1].first, b = t[2].first, c = t[3].first; // 最后将a、b、c从小到大排序
    ans = 0x3f3f3f3f;
    dfs(1, 0);
    printf("%d\n", ans);
}

int main()
{
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}

题:[NOI2011] 兔兔与蛋蛋游戏

一张棋盘上有黑、白两种颜色的棋子和一个空位。

兔兔要选一个与空位相邻的白棋子并移到空位,而蛋蛋要选黑的。如果某轮有一方不能移动了他就输了。

现在给你一个蛋蛋赢的棋局,你需要求出哪些步兔兔走错了。


很有趣的一道题。难点在于把「判断哪方必胜」转化成二分图问题。

因为把棋子移到空格内比较难理解,所以不妨想象成移动空格。

首先找一找性质。经过手玩样例,不难发现空格的路径不能形成环,也就是说不能走到以前走过的位置。

然后作为一道博弈题,我们考虑如何判断必胜方。

看到黑白棋子可以试试变成二分图,这时思路就基本出来了:

若两格子相邻且颜色不同,则连边。

若本轮玩家的起点必须在最大匹配里,则其必胜,否则必输。

证明很简单,因为可以走到它的匹配点上,而下一步要么走进另一个有匹配的点上,要么输。如图:(S为起点,由于一开始要走向白色,不妨将S设为黑色)

若下一步走到了一个没有匹配的点上,则可以将匹配方案沿路径前移一格,如图:

此时起点不必须在最大匹配内,与条件不符。故命题成立。

这样一来,每到下一个点,就把上一个点以及其匹配点抹去(可以将vis设为特殊值,详见代码)。若当前点没有匹配点,则必然不在最大匹配内(易得);否则若匹配点还能找到另一个除了本轮点以外的点匹配(可能有点绕,就是说本轮点的匹配点不一定是本轮点),则不必须在最大匹配内。否则必须在最大匹配内。

一开始要对棋盘每个点跑匈牙利,然后每走一步跑一次匈牙利,故时间复杂度 \(\mathcal{O}((nm)^2 + knm)\)


思路很巧,代码也很好写。
注:由于本轮是否必胜看的是当前状态,而不是走完后的状态,故将输入放在循环最后(见代码)。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int L = 45, N = 2005;
int a, b, m, win[N];
char s[L][L];
bool c[N];
int n, st, tag, vis[N], match[N];
vector<int> g[N], ans;

int trans(int x, int y) // 将坐标转为点编号
{
    return (x - 1) * b + y;
}

bool dfs(int u)
{
    vis[u] = tag;
    for (auto v : g[u])
        if (vis[v] != -1 && (!match[v] || vis[match[v]] != tag && vis[match[v]] != -1 && dfs(match[v])))
        {
            match[v] = u;
            match[u] = v;
            return true;
        }
    return false;
}

int main()
{
    cin >> a >> b;
    n = a * b;
    for (int i = 1; i <= a; i++)
    {
        scanf("%s", s[i] + 1);
        for (int j = 1; j <= b; j++)
        {
            int u = trans(i, j), v;
            if (s[i][j] == '.')
                st = u;
            c[u] = s[i][j] != 'O';
            v = trans(i - 1, j);
            if (i > 1 && c[u] != c[v])
                g[u].push_back(v), g[v].push_back(u);
            v = trans(i, j - 1);
            if (j > 1 && c[u] != c[v])
                g[u].push_back(v), g[v].push_back(u);
        }
    }
    for (int i = 1; i <= n; i++) // 对所有点跑匈牙利
        if (c[i])
        {
            tag++;
            dfs(i);
        }
    cin >> m;
    m *= 2; // 每人m轮,共2m轮
    for (int k = 1; k <= m; k++)
    {
        vis[st] = -1;
        if (!match[st])
            win[k] = false;
        else
        {
            match[match[st]] = 0;
            tag++;
            win[k] = !dfs(match[st]); // 是否还有别的点能和匹配点匹配,没有说明必胜
        }
        if (!(k & 1) && win[k] == win[k - 1]) // 正常来说两人应该轮流胜负,如果连续一样的结果说明出错了
            ans.push_back(k / 2);
        int x, y;
        scanf("%d%d", &x, &y); // 到下一个状态
        st = trans(x, y);
    }
    cout << ans.size() << endl;
    for (auto i : ans)
        printf("%d\n", i);
    return 0;
}

霍尔定理

霍尔定理:设 \(G=〈V_1,V_2,E〉\) 为二分图,\(|V_1|\le|V_2|\),则 \(G\) 中存在 \(V_1\)\(V_2\) 的完美匹配当且仅当对于任意的 \(S\subset V_1\),均有 \(|S|\le |N(S)|\),其中 \(N(S)\)\(S\) 的邻域。

霍尔定理还有一条重要的推论:二分图的最大匹配为 \(|V_1| - \max_{S \subset V_1} \{|S|-|N(S)|\}\)

这条结论看似没什么用处,实则常常能把一些问题中「判断是否存在完美匹配」「动态最大匹配」等的问题大大简化。

题:[ARC076F] Exhausted?

原问题显然在求一个最大匹配。于是我们考虑使用霍尔定理。

但我们不能枚举子集,所以考虑枚举邻域。

假设当前前缀为 \(L\),后缀为\(R\),因为 要求 \(\max_{S \subset V_1} \{|S|-|N(S)|\}\),所以统计前后缀都被包含的人的个数 \(num\),答案就为

\[\max \lbrace num - min\{m, L+ (m-R+1)\}\rbrace \]

(注意 \(L\) 不一定要 \(\le R\)

于是可以枚举 \(L\),那么对于每个 \(L\),答案为

\[\max\{\max_{R \ge L} \lbrace \sum_{l_i \le L} [r_i\ge R] +R \rbrace - l - m - 1, \max_{R<L}\{\sum_{l_i \le L} [r_i\ge R] -m\}\} \]

注意到当 \(R<L\) 时,左边不会大于右边;另外,右边是单调的。所以等于

\[\max\{\max \lbrace \sum_{l_i \le L} [r_i\ge R] +R \rbrace - l - m - 1, n-m\} \]

\(L\) 从0开始往右扫,同时线段树上维护 \(\sum_{l_i \le L} [r_i\ge R] +R\) 以及区间 \(\max\) 即可。

时间复杂度 \(\mathcal{O}(m\log m+n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define lson u + u
#define rson u + u + 1
const int N = 2e5 + 5, ND = N << 2;
struct segtree
{
    int tag[ND], mx[ND];
    void pushup(int u)
    {
        mx[u] = max(mx[lson], mx[rson]);
    }
    void add(int u, int x)
    {
        tag[u] += x;
        mx[u] += x;
    }
    void pushdown(int u)
    {
        if (!tag[u])
            return;
        add(lson, tag[u]);
        add(rson, tag[u]);
        tag[u] = 0;
    }
    void build(int u, int l, int r)
    {
        if (l == r)
        {
            mx[u] = l; // 初值为下标
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid);
        build(rson, mid + 1, r);
        pushup(u);
    }
    void update(int u, int l, int r, int L, int R)
    {
        if (L <= l && r <= R)
        {
            add(u, 1);
            return;
        }
        if (R < l || r < L)
            return;
        pushdown(u);
        int mid = (l + r) >> 1;
        update(lson, l, mid, L, R);
        update(rson, mid + 1, r, L, R);
        pushup(u);
    }
    int query()
    {
        return mx[1];
    }
} t;
int n, m, ans;
pair<int, int> a[N];

int main()
{
    cin >> n >> m;
    ans = n - m;
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i].first, &a[i].second);
    sort(a + 1, a + n + 1);
    int pos = 0;
    t.build(1, 1, m + 1);
    for (int i = 0; i <= m; i++)
    {
        while (pos < n && a[pos + 1].first == i)
            t.update(1, 1, m + 1, 1, a[++pos].second); // 做一个前缀区间加即可维护所有>=R的r[i]个数
        ans = max(ans, t.query() - m - i - 1); // 如上式
    }
    cout << ans << endl;
    return 0;
}

二分图最大权匹配

二分图的最大权匹配是指二分图中边权和最大的匹配。

KM算法

学习此算法前,请确保您已经掌握「匈牙利算法」。

参考资料:Singercoder 的博客

KM 算法可以求出最大权完美匹配(即边权和最大的完美匹配),其本质也是找增广路。

实现方式有 \(\mathcal{O}(n^4)\) 的 dfs 和 \(\mathcal{O}(n^3)\) 的 bfs。

另外,如果单纯要求最大权匹配的话,可以通过建立虚边虚点来解决,后面会讲到。

DFS version

想要理解效率较高的 \(bfs\) 版本,首先要理解基本的 dfs 版本。

KM 算法的精髓是「顶标」。

我们先规定一些变量:

  • \(match\):匹配点。
  • \(vis\):访问标记。
  • \(val\):顶标。
  • \(d\):对于所有边 \(〈u,v,w〉\)\(\min\lbrace val_u+val_v-w\rbrace\) 的值。

那么找到一组完美匹配等价于,对于每个左侧点 \(u\),都有对应的 \(v\) 使得 \(min\lbrace val_u+val_v-w\rbrace=0\)

其具体实现过程如下:(以下过程可能较难理解,看代码会好很多)

  1. 先给每个点赋上权值为 \(inf\) 的顶标。然后枚举左侧点 \(i\) 作为增广路起点。
  2. 跑寻找增广路的 \(dfs\)\(inf\rightarrow d\)
  3. \(val_u+val_v>w\),则 \(d=\min(d,val_u+val_v-w)\);否则说明 \(val_u+val_v=w\),此时找到了可配对的一对点,尝试匹配两点。若 \(v\) 已有匹配点,重复3;否则匹配成功。
  4. 修改顶标,枚举左侧点 \(u\),若只有 \(u\) 被遍历(而没有/不是其匹配点),说明 \(val_u\) 应当 \(-=slack\),否则不用变(而下面的代码会把 \(val_v\) 减回去,是一样的效果)。
  5. \(i\) 匹配成功,则继续枚举下一个左侧点 \(i\),执行2;否则,回到2继续匹配。

是不是和匈牙利很像?代码也很好写,如下:

洛谷P6577 【模板】二分图最大权完美匹配

点击查看代码
#define ll long long
#define inf 0x3f3f3f3f3f3f3f3f
const int N = 1005;
int n, m;
int match[N];
bool vis[N];
ll g[N][N], val[N], d, ans;

bool dfs(int u)
{
    vis[u] = true;
    for (int v = n + 1; v <= n + n; v++)
    {
        ll w = g[u][v];
        if (vis[v])
            continue;
        if (val[u] + val[v] > w)
            d = min(d, val[u] + val[v] - w);
        else
        {
            vis[v] = true;
            if (!match[v] || dfs(match[v]))
            {
                match[u] = v;
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}

int main()
{
    cin >> n >> m;
    int u, v;
    ll w;
    for (int i = 1; i <= n + n; i++)
        fill(g[i] + 1, g[i] + n + n + 1, -1e10);
    for (int i = 1; i <= m; i++)
        scanf("%d%d%lld", &u, &v, &w), g[u][v + n] = w;
    fill(val + 1, val + n + n + 1, 1e7);
    for (int i = 1; i <= n; i++)
        while (true)
        {
            memset(vis, 0, sizeof(vis));
            d = inf;
            if (dfs(i))
                break;
            for (int u = 1; u <= n; u++)
                if (vis[u])
                    val[u] -= d;
            for (int u = n + 1; u <= n + n; u++)
                if (vis[u])
                    val[u] += d;
        }
    for (int i = 1; i <= n; i++)
        ans += val[i] + val[match[i]];
    cout << ans << endl;
    for (int i = n + 1; i <= n + n; i++)
        printf("%d%c", match[i], " \n"[i == n + n]);
    return 0;
}

先别急着交啊,这份代码会T

交上去之后会发现,正确性可以保证,但是会TLE。我们分析一下时间复杂度:因为 dfs 有可能遍历所有边,所以单次的复杂度是 \(\mathcal{O}(n^2)\) 的,因此这种写法的时间复杂度是 \(\mathcal{O}(n^4)\) 的。

接下来,有请——

BFS version

首先,规定一些新的变量:

  • \(slack\):对于每个右侧点 \(v\)\(\min \lbrace val_u+val_v-w\rbrace\)
  • \(pre\):寻找增广路时,右侧点 \(v\) 准备匹配的左侧点。

bfs 的优点在于,它可以把在一次寻找增广路时中断的位置 push 到 queue 里,这样下次就可以直接将这个点作为起点,省掉了 dfs 做法每次重新找增广路的过程。

代码量虽然偏大,但是比较好写,不过细节颇多。

我的建议是,理解当然好(毕竟这个不难理解),但是最好背一下,毕竟即使理解透彻了也很难在考场一字不差地写好。

其实这种比较死的模版不如直接背

点击查看代码
#define ll long long
#define inf 0x3f3f3f3f3f3f3f3f
const int N = 1005, M = N * N;
int n, m;
bool vis[N];
int match[N], pre[N];
ll g[N][N], val[N], slack[N], ans;

void dfs_match(int v)
{
    int u = pre[v];
    int nxt = match[u];
    match[v] = u;
    match[u] = v;
    if (nxt)
        dfs_match(nxt);
}

void bfs(int st)
{
    memset(vis, 0, sizeof(vis));
    memset(slack, 0x3f, sizeof(slack));
    queue<int> q;
    q.push(st);
    while (true)
    {
        while (!q.empty())
        {
            int u = q.front(); q.pop();
            vis[u] = true;
            for (int v = n + 1; v <= n + n; v++)
            {
                ll w = g[u][v];
                if (vis[v])
                    continue;
                if (val[u] + val[v] - w < slack[v])
                {
                    slack[v] = val[u] + val[v] - w;
                    pre[v] = u;
                    if (slack[v] == 0)
                    {
                        vis[v] = true;
                        if (!match[v])
                        {
                            dfs_match(v);
                            return;
                        }
                        else
                            q.push(match[v]);
                    }
                }
            }
        }
        ll d = inf;
        for (int i = n + 1; i <= n + n; i++)
            if (!vis[i])
                d = min(d, slack[i]);
        for (int i = 1; i <= n; i++)
            if (vis[i])
                val[i] -= d;
        for (int i = n + 1; i <= n + n; i++)
        {
            if (vis[i])
                val[i] += d;
            else
                slack[i] -= d;
        }
        for (int v = n + 1; v <= n + n; v++)
            if (!vis[v] && slack[v] == 0)
            {
                vis[v] = true;
                if (!match[v])
                {
                    dfs_match(v);
                    return;
                }
                else
                    q.push(match[v]);
            }
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n + n; i++)
        fill(g[i] + 1, g[i] + n + n + 1, -1e10);
    int u, v;
    for (int i = 1; i <= m; i++)
        scanf("%d%d", &u, &v), scanf("%lld", &g[u][n + v]);
    fill(val + 1, val + n + n + 1, 1e7);
    for (int i = 1; i <= n; i++)
        bfs(i);
    for (int i = 1; i <= n; i++)
        ans += val[i] + val[match[i]];
    cout << ans << endl;
    for (int i = n + 1; i <= n + n; i++)
        printf("%d%c", match[i], " \n"[i == n + n]);
    return 0;
}

好了,现在你已经掌握了最大权完美匹配,那么最大权匹配肯定也难不倒你。

显然只需建立虚边虚点,虚边赋权为0,KM 照样跑即可。但是注意根据题目输出要求进行适当判断。

此外,还有些题目可能无法保证有完美匹配,会让你判断无解。这种情况下还是一样的套路,把虚边权赋成 \(-inf\) 即可,最后如果方案里选了非法边就说明无解。

posted @ 2024-10-10 12:49  Aquizahv  阅读(74)  评论(0)    收藏  举报