二分图匹配
二分图匹配
定义
-
二分图:是一张可以将结点分为两个集合,且每个集合内部不存在连边的图。
-
交错路:始于非匹配点且由匹配边与非匹配边交错而成。
-
增广路:是始于非匹配点且终于非匹配点(除了起始的点)的交错路。增广路中边的数量是奇数。
二分图最大匹配
例题:CSES - 1696
二分图最大匹配就是要在边集中选出一些边,使得这些边没有公共顶点,且边的数量最多。
匈牙利算法
由于增广路的性质,我们会发现,当我们找到一条增广路的时候,这条增广路上的非匹配边一定比匹配边多一条,那么我们可以直接放弃选择原本的匹配边,而是改成选择原本的非匹配边,这样匹配的边还会多一条。
由于二分图的所有结点可以分为两个集合,我们称这两个集合为 \(V_1, V_2\),那么,匈牙利算法大概就是对于每一个 \(u \in V_1\),判断是否存在与其相连的某个 \(v\) 是非匹配点,如果存在,直接选择,否则,判断 \(v\) 所连接的点是否可以换一个点连接,不断递归下去。
事实上就是在找增广路径。
bool dfs(int u) {
if (vis[u]) return 0;
vis[u] = 1;
for (int v : g[u]) {
if (!pr[v] || dfs(pr[v])) {
pl[u] = v, pr[v] = u;
return 1;
}
}
return 0;
}
Dinic
我们可以把二分图匹配转换成最大流的模型。
其中用方框框出来的部分是原本的二分图。
我们这样考虑,由于每个点只能被选择一次,因此,对于每一个 \(u \in V_2\),\((u, t)\) 这条边的边权为 \(1\),表示这条边的最大流量为 \(1\),也就是 \(u\) 只能被选择一遍。
同样的,对于每一个 \(u \in V_1\),\((s, u)\) 这条边的边权也应当为 \(1\)。
像这样建出图,然后直接跑 dinic
即可。
void addEdge(int u, int v, int w) {
int x = g[u].size(), y = g[v].size();
g[u].push_back({v, w, y});
g[v].push_back({u, 0, x});
}
bool bfs() {
fill(d, d + N, -1);
fill(vis, vis + N, 0);
d[s] = 0, que.push(s);
while (!que.empty()) {
int u = que.front(); que.pop();
for (auto [v, w, id] : g[u]) {
if (w > 0 && d[v] == -1) {
d[v] = d[u] + 1, que.push(v);
}
}
}
return d[t] != -1;
}
ll dfs(int u, ll f) {
if (u == t) return f;
if (vis[u]) return 0;
ll ret = 0;
for (auto &[v, w, id] : g[u]) {
if (w <= 0 || d[v] != d[u] + 1) continue;
ll tmp = dfs(v, min(f, 0ll + w));
auto &[_, vw, __] = g[v][id];
ret += tmp, vw += tmp, f -= tmp, w -= tmp;
if (f <= 0) break;
}
vis[u] = f > 0;
return ret;
}
最小点覆盖
对于图 \(G = (V, E)\),选取点集 \(V'\) 使得 \(\forall e \in E\),至少有一个端点在 \(V'\) 中,且 \(V'\) 中点的数量最少。
最小点覆盖$ \ = \ $最大匹配
最大独立集
对于图 \(G = (V, E)\),选取点集 \(V'\) 使得其中任意两个结点没有连边。
最大独立集$\ = \ n \ - $ 最小点覆盖
CF1765A
题意
有 \(n\) 个人,\(m\) 份文档,一张访问需求表 \(a\),其中 \(a_{i, j}\) 为 \(1\) 表示第 \(i\) 个人应该访问第 \(j\) 份文档,为 \(0\) 则表示第 \(i\) 个人不应该访问第 \(j\) 份文档。
为了限制访问,你需要将这 \(m\) 份文档分成 \(k \ (k \ge 1)\) 组,并给每份文档一个所需的访问级别,其中, \(k\) 是由你选择的。
同时,你需要给每个人分配 \(k\) 个访问级别,表示他对于某一个小组的访问级别。
具体来说就是对于属于第 \(i\) 组的每个 \(j\),只有当某个人对于 \(i\) 的访问级别超过的 \(j\) 所需的访问级别,这个人才可以访问 \(j\)。
请你求出 \(k\) 最小的一种方案。
思路
我们考虑两份文档在什么情况下可以被分到同一个小组,我们很容易发现,当可以访问 \(u\) 的人是可以访问 \(v\) 的人的子集时,意味着 \(u\) 的访问级别比 \(v\) 高,同时也意味着这两份文件可以被分到同一组。
因此,我们将每对这样的 \((u, v)\) 之间建一条有向边,显然的,\(k\) 就是这张图的最大独立集,在最大独立集中的点互相没有连边意味着它们没有任何一对点可以分到同一小组。
然后,我们再做一遍拓扑排序,求出每份文件的访问级别,最后算出每个人的访问级别即可。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 5e5 + 10, M = 510;
struct Node {
int v, w, id;
};
int T, n, m, k, d[N], s, t, p[M][M], id[M], mp[N], cnt;
int deg[M], nxt[M], col[M], val[N], ans[M], res;
bool vis[N], a[M][M];
vector<Node> g[N];
queue<int> que;
void addEdge(int u, int v, int w) {
int x = g[u].size(), y = g[v].size();
g[u].push_back({v, w, y});
g[v].push_back({u, 0, x});
}
bool bfs() {
fill(d, d + N, -1);
fill(vis, vis + N, 0);
d[s] = 0, que.push(s);
while (!que.empty()) {
int u = que.front(); que.pop();
for (auto [v, w, id] : g[u]) {
if (w > 0 && d[v] == -1) {
d[v] = d[u] + 1, que.push(v);
}
}
}
return d[t] != -1;
}
int dfs(int u, int f) {
if (u == t) return f;
if (vis[u]) return 0;
ll ret = 0;
for (auto &[v, w, id] : g[u]) {
if (w <= 0 || d[v] != d[u] + 1) continue;
ll tmp = dfs(v, min(f, w));
auto &[_, vw, __] = g[v][id];
ret += tmp, vw += tmp, f -= tmp, w -= tmp;
if (u <= cnt && v >= cnt && !w) nxt[u] = v - cnt, deg[v - cnt]++;
if (f <= 0) break;
}
vis[u] = f > 0;
return ret;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
char c; cin >> c, a[i][j] = c - '0';
}
}
for (int i = 1; i <= m; i++) id[i] = i;
for (int i = 1; i <= m; i++) {
for (int j = i + 1; j <= m; j++) {
bool flag1 = 1, flag2 = 1;
for (int k = 1; k <= n; k++) {
flag1 &= a[k][i] <= a[k][j]; // val[i] >= val[j]
flag2 &= a[k][j] <= a[k][i]; // val[i] <= val[j]
}
if (flag1 && flag2) id[j] = id[i];
else if (flag1 || flag2) p[i][j] = (flag1 ? 1 : 2);
}
}
for (int i = 1; i <= m; i++) {
if (!mp[id[i]]) mp[id[i]] = ++cnt;
}
for (int i = 1; i <= m; i++) {
for (int j = i + 1; j <= m; j++) {
int u = mp[id[i]], v = mp[id[j]];
if (p[i][j] == 1) addEdge(u, v + cnt, 1);
else if (p[i][j] == 2) addEdge(v, u + cnt, 1);
}
}
t = 2 * cnt + 1;
for (int i = 1; i <= cnt; i++) {
addEdge(s, i, 1), addEdge(i + cnt, t, 1);
val[i] = 1;
}
while (bfs()) res += dfs(s, 1e9);
cout << cnt - res << '\n';
for (int i = 1; i <= cnt; i++) {
if (!deg[i]) {
int x = i; k++, val[x] = 1e9;
while (x) {
val[nxt[x]] = val[x] - 1;
col[x] = k, x = nxt[x];
}
}
}
for (int i = 1; i <= m; i++) cout << col[mp[id[i]]] << ' ';
cout << '\n';
for (int i = 1; i <= m; i++) cout << val[mp[id[i]]] << ' ';
cout << '\n';
for (int i = 1; i <= n; i++) {
fill(ans + 1, ans + k + 1, 1);
for (int j = 1; j <= m; j++) {
if (a[i][j]) ans[col[mp[id[j]]]] = max(ans[col[mp[id[j]]]], val[mp[id[j]]]);
}
for (int i = 1; i <= k; i++) cout << ans[i] << ' ';
cout << '\n';
}
return 0;
}
CF1404E
题意
有一个 \(n \times m\) 的网格图,每个格子要么是黑色,要么是白色。
你有很多块砖,砖块被定义为具有整数边长的矩形,宽度为 \(1\) 或高度为 \(1\)。
你需要用砖块覆盖图上的每个黑色格子,但是不能覆盖到白色格子,也不能让某个黑色方格被两块砖块覆盖到。
请你求出最少需要用多少块砖。
思路
我们考虑对于某个黑色方格,如果它的上方也是黑色方格,那么它们俩就是可以被同一块砖所覆盖的,就可以建出一条边;同样的,如果它的右边也是黑色方格,那么它们俩也是可以被同一块砖所覆盖的,也可以建出一条边。
但是,显然的,如果它的上方和右边都有黑色方格,它是无法同时和两个方格被一块砖覆盖的,也就是说,那两条边是无法同时选择的。
因此,我们可以在这两边中建一条边,意味着这两条边无法被同时选择。
我们又希望最终的砖块数最少,也就是说,我们希望被选择的边最多。
因此,答案就是这张图的最大独立集。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 10, M = 210;
const int dx[] = {0, 1};
const int dy[] = {1, 0};
struct Node {
int v, w, id;
};
int T, n, m, d[N], s, t, cnt, k;
ll ans;
char a[M][M];
bool vis[N];
vector<int> p, c[M][M];
vector<Node> g[N];
queue<int> que;
void addEdge(int u, int v, int w) {
int x = g[u].size(), y = g[v].size();
g[u].push_back({v, w, y});
g[v].push_back({u, 0, x});
}
bool bfs() {
fill(d, d + N, -1);
fill(vis, vis + N, 0);
d[s] = 0, que.push(s);
while (!que.empty()) {
int u = que.front(); que.pop();
for (auto [v, w, id] : g[u]) {
if (w > 0 && d[v] == -1) {
d[v] = d[u] + 1, que.push(v);
}
}
}
return d[t] != -1;
}
ll dfs(int u, ll f) {
if (u == t) return f;
if (vis[u]) return 0;
ll ret = 0;
for (auto &[v, w, id] : g[u]) {
if (w <= 0 || d[v] != d[u] + 1) continue;
ll tmp = dfs(v, min(f, 0ll + w));
auto &[_, vw, __] = g[v][id];
ret += tmp, vw += tmp, f -= tmp, w -= tmp;
if (f <= 0) break;
}
vis[u] = f > 0;
return ret;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j], k += a[i][j] == '#';
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == '.') continue;
int nx = i, ny = j + 1;
if (1 <= nx && nx <= n && 1 <= ny && ny <= m && a[nx][ny] == '#') {
c[i][j].push_back(++cnt), c[nx][ny].push_back(cnt), addEdge(s, cnt, 1);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == '.') continue;
int nx = i + 1, ny = j;
if (1 <= nx && nx <= n && 1 <= ny && ny <= m && a[nx][ny] == '#') {
cnt++;
for (int x : c[i][j]) addEdge(x, cnt, 1);
for (int x : c[nx][ny]) addEdge(x, cnt, 1);
p.push_back(cnt);
}
}
}
cnt++, t = cnt;
for (int x : p) addEdge(x, t, 1);
while (bfs()) ans += dfs(s, 1e18);
cout << k - (cnt - 1 - ans) << '\n';
return 0;
}