今日も、明日も、輝いている。|

Aquizahv

园龄:2年粉丝:0关注:7

2024-10-10 12:49阅读: 20评论: 0推荐: 0

【学习笔记】二分图

当遇到「匹配题」或者「矩阵中有行列限制」「黑白染色」类题,常常使用二分图算法。

二分图最大匹配 / 最小点覆盖

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

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

对于所有二分图,都有:||=||。证明不会qwq。

匈牙利算法

思路

算法核心:找“增广路”

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

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

由于每次的 dfs 找匹配点是 O(n+m) 的,因此匈牙利算法的时间复杂度为 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;
}

[HNOI2013]消毒

题意

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

你可以使用一种消毒剂,花费 min{x,y,z} 代价,给一个 x×y×z 的立方体消毒。

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

思路

Easy version

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

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

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

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

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

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

Hard version

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

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

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

注意要选用最短的棱长枚举。不妨设 abc,因为 a×b×c5×103,所以可以用反证法证明 a<=17

时间复杂度 O(2a×(b+c)×[(b+c)+bc])。(abc

瓶颈是 a=b=c=17,数量级约为 1.3×109,但是卡不满匈牙利。可以通过。

代码

点击查看代码
#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设为特殊值,详见代码)。若当前点没有匹配点,则必然不在最大匹配内(易得);否则若匹配点还能找到另一个除了本轮点以外的点匹配(可能有点绕,就是说本轮点的匹配点不一定是本轮点),则不必须在最大匹配内。否则必须在最大匹配内。

一开始要对棋盘每个点跑匈牙利,然后每走一步跑一次匈牙利,故时间复杂度 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=V1,V2,E 为二分图,|V1||V2|,则 G 中存在 V1V2 的完美匹配当且仅当对于任意的 SV1,均有 |S||N(S)|,其中 N(S)S 的邻域。

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

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

[ARC076F] Exhausted?

思路

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

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

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

max{numL(mR+1)}

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

maxRL{liL[riR]+R}lm1

L 从0开始往右扫,同时线段树上维护 liL[riR]+R 以及区间 max 即可。

时间复杂度 O(mlogm+n)

代码

使用霍尔定理的前提是 |V1||V2|,所以要特判 n>m 的情况,此时答案至少为 nm

点击查看代码
#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;
if (n > m)
ans = n - m; // Important!!
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;
}

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

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

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

对于所有二分图,都有:||=||=||=||。证明不会qwq。(挨揍ing)

二分图最大权匹配

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

KM算法

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

参考资料:Singercoder 的博客

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

实现方式有 O(n4) 的 dfs 和 O(n3) 的 bfs。

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

DFS version

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

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

我们先规定一些变量:

  • match:匹配点。
  • vis:访问标记。
  • val:顶标。
  • d:对于所有边 u,v,wmin{valu+valvw} 的值。

那么找到一组完美匹配等价于,对于每个左侧点 u,都有对应的 v 使得 min{valu+valvw}=0

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

  1. 先给每个点赋上权值为 inf 的顶标。然后枚举左侧点 i 作为增广路起点。
  2. 跑寻找增广路的 dfsinfd
  3. valu+valv>w,则 d=min(d,valu+valvw);否则说明 valu+valv=w,此时找到了可配对的一对点,尝试匹配两点。若 v 已有匹配点,重复3;否则匹配成功。
  4. 修改顶标,枚举左侧点 u,若只有 u 被遍历(而没有/不是其匹配点),说明 valu 应当 =slack,否则不用变(而下面的代码会把 valv 减回去,是一样的效果)。
  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 有可能遍历所有边,所以单次的复杂度是 O(n2) 的,因此这种写法的时间复杂度是 O(n4) 的。

接下来,有请——

BFS version

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

  • slack:对于每个右侧点 vmin{valu+valvw}
  • 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 即可,最后如果方案里选了非法边就说明无解。

本文作者:Aquizahv

本文链接:https://www.cnblogs.com/aquizahv/p/18435290

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Aquizahv  阅读(20)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起