【算法学习笔记】二分图
二分图
图论的证明是真的好复杂。。。我也就是听一听,大致理解一下,苯人真的不会证qaq
二分图的板子还是很简单的,难就难在把问题抽象成二分图中对应的问题(图论之难大多在此)
所以多做题吧...
染色法判二分图
看是否存在奇环
板子:
bool dfs (int u, int c) {
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (color[j] == -1) {
if (!dfs (j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check () {
memset (color, -1, sizeof color);
for (int i = 1; i <= n; i++) {
if (color[i] == -1) {
if (!dfs (i, 0)) return false;
}
}
return true;
}
典中典题目:P1525 [NOIP2010 提高组] 关押罪犯
#include <bits/stdc++.h>
using namespace std;
const int N = 20005, M = 200005;
int h[N], e[M], ne[M], w[M], idx;
int color[N], n, m;
void add (int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool dfs (int u, int c, int maxn) {
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (w[i] <= maxn) continue; //w[i]
if (color[j] == -1) {
if (!dfs (j, !c, maxn)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check (int x) {
memset (color, -1, sizeof color);
for (int i = 1; i <= n; i++) {
if (color[i] == -1) {
if (!dfs (i, 0, x)) return false;
}
}
return true;
}
int main () {
memset (h, -1, sizeof h);
scanf ("%d%d", &n, &m);
while (m --) {
int a, b, c;
scanf ("%d%d%d", &a, &b, &c);
add (a, b, c), add (b, a, c);
}
int l = 0, r = 1e9;
while (l < r) {
int mid = l + r >> 1;
if (check (mid)) r = mid;
else l = mid + 1;
}
printf ("%d\n", r);
}
//二分边长
二分图的最大匹配
增广路径:从左匹配点出发,先沿非匹配边走,再沿匹配边走...依次交替,最后落在右非匹配点的一条路。
最大匹配 \(\leftrightarrow\) 不存在增广路径
匈牙利算法
板子:
bool find (int u) {
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (vis[j]) continue;
vis[j] = true;
if (!match[j] || find (match[j])) {
match[j] = u;
return true; //匹配上一对
}
}
return false;
}
例题:棋盘覆盖
抽象成二分图:
把每个格子视为点,向能放骨牌的点连一条边,所求转化为所选边无公共点的情况下,最多能选多少边(转化为匹配问题)
隔一个染色又可以转化为二分图(相邻格子颜色不同)
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 105;
int n, k;
bool vis[N][N], stk[N][N];
pii match[N][N];
int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0};
bool Range (int x, int y) {
if (x < 1 || x > n || y < 1 || y > n) return false;
return true;
}
bool find (pii p) {
int x = p.first, y = p.second;
for (int i = 0; i < 4; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (!Range (xx, yy)) continue;
if (vis[xx][yy] || stk[xx][yy]) continue;
//if ((xx + yy) & 1) continue;
vis[xx][yy] = true;
if (!match[xx][yy].first || find (match[xx][yy])) {
match[xx][yy] = p;
return true;
}
}
return false;
}
int main () {
cin >> n >> k;
while (k --) {
int x, y;
cin >> x >> y;
stk[x][y] = true;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (stk[i][j]) continue;
memset (vis, false, sizeof vis);
if (((i + j) & 1) && !vis[i][j]) {
if (find ({i, j})) ans ++;
}
}
}
cout << ans;
}
二分图的最小点覆盖
最小点覆盖:从图中选出最少的点,使得每一条边的两个端点至少有一个点被选出来
二分图中,最小点覆盖=最大匹配数
例题:机器任务
直接利用 二分图中,最小点覆盖=最大匹配数 这个结论,套板子即可
注意多组数据之间的清空问题
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int n, m, k, x;
int match[N];
bool vis[N], g[N][N];
bool find (int u) {
for (int i = 0; i < m; i++) {
if (vis[i] || !g[u][i]) continue;
vis[i] = true;
if (!match[i] || find (match[i])) {
match[i] = u;
return true;
}
}
return false;
}
int main () {
while (cin >> n, n) {
cin >> m >> k;
memset (g, false, sizeof g);
memset (match, 0, sizeof match);
while (k --) {
int a, b;
cin >> x >> a >> b;
if (a == 0 || b == 0) continue;
g[a][b] = true;
}
int ans = 0;
for (int i = 0; i < n; i++) {
memset (vis, false, sizeof vis);
if (find (i)) ans ++;
}
cout << ans << endl;
}
}
最大独立集
选出最多的点,使得任意两点之间没有边
二分图中,求最大独立集等价于 \(\leftrightarrow\) 去掉最少的点,使得所有边被破坏 \(\leftrightarrow\) 找最小点覆盖
即 最大独立集 = \(n-m\),\(n\) 是总点数,\(m\) 是最小点覆盖的最小点数
例题:骑士放置
把能攻击到的两点连一条边,就转化为求最大独立集问题了,然后证明他是二分图的过程与棋盘覆盖那道题类似。
即 所求为 \(n-k-m\)
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 105;
bool vis[N][N], stk[N][N];
int n, m, k;
int dx[] = {1, 2, -1, 2, -2, 1, -1, -2};
int dy[] = {2, 1, 2, -1, 1, -2, -2, -1};
pii match[N][N];
bool Range (int x, int y) {
if (x < 1 || x > n || y < 1 || y > m) return false;
return true;
}
bool find (pii p) {
int x = p.first, y = p.second;
for (int i = 0; i < 8; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (!Range (xx, yy)) continue;
if (vis[xx][yy] || stk[xx][yy]) continue;
vis[xx][yy] = true;
if (!match[xx][yy].first || find (match[xx][yy])) {
match[xx][yy] = p;
return true;
}
}
return false;
}
int main () {
cin >> n >> m >> k;
for (int i = 0; i < k; i++) {
int x, y;
cin >> x >> y;
stk[x][y] = true;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if ((i + j) & 1) continue;
if (stk[i][j]) continue;
memset (vis, false, sizeof vis);
if (find ({i, j})) ans ++;
}
}
cout << n * m - ans - k << endl;
}
最小路径点覆盖
最小路径覆盖问题:DAG上,用最少的互不相交的路径将所有点覆盖。
数值上等于最大独立集
拆点,由于是DAG,所以每个点最多覆盖一个边。
原图终点(没有出边)对应新图的左部未匹配点。
则 求原图中互不相交路径数\(\leftrightarrow\)求路径终点数最少\(\leftrightarrow\)求左部非匹配点最少\(\leftrightarrow\)求最大匹配
拓展:最小路径重复点覆盖
- 求传递闭包,得新图G'
- 在G'上求最小路径覆盖
例题:捉迷藏
具体的证明我还是自己写不出来(想不到),所以可以看看题姐:
#include <bits/stdc++.h>
using namespace std;
const int N = 205, M = 30005;
int n, m;
int match[N];
bool vis[N], g[N][N];
bool find (int u) {
for (int i = 1; i <= n; i++) {
if (vis[i] || !g[u][i]) continue;
vis[i] = true;
if (!match[i] || find (match[i])) {
match[i] = u;
return true;
}
}
return false;
}
int main () {
cin >> n >> m;
while (m --) {
int a, b;
cin >> a >> b;
g[a][b] = true;
}
for (int k = 1; k <= n; k++) { //floyd求传递闭包
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
g[i][j] |= g[i][k] & g[k][j];
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
memset (vis, false, sizeof vis);
if (find (i)) ans ++;
}
cout << n - ans;
}
补题
说起来倒回来学二分图是为了补这题:19ICPC南京-J
Reference
AcWing算法提高课
OI-Wiki:最小点覆盖,
洛谷二分图板刷:https://www.luogu.com.cn/problem/list?keyword=&page=1&tag=187&orderBy=difficulty&order=asc