二分图定义:百度百科
- 二分图 = 不存在奇数环 = 染色法不存在矛盾
- 匈牙利算法:匹配、最大匹配、匹配点、增广路径
增广路径:在二分图中,先从左边非匹配点走,先走非匹配边,然后沿着匹配边,非匹配边、匹配边、非匹配边、匹配边…,最终走到右边一个非匹配点。如果存在这样一条路径,那么可以将所有匹配边删去,留下所有的非匹配边形成匹配边,且原非匹配边的数量比原匹配边的数量多1
。
最大匹配 = 不存在增广路径 - 最小点覆盖、最大独立集、最小路径点覆盖(最小路径重复点覆盖)
二分图中:最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径点覆盖
最小点覆盖:在一个无向图中,选择最小的点集,使得每条边的两个端点至少有一个端点被选择出来。
最大独立集:从一个图中,选择最多的点构成点集,使得点集内部任意两点之间是没有边的。二分图中,求最大独立集等价于求去掉最少的点,将图中所有的边都破坏掉,这个就等价于找最小点覆盖,也就等价于最大匹配数。
最大团:从一个图中,选择最多的点构成点集,使得点集内部任意两点之间都有边的。
最大独立集与最大团之间关系:一个图与其补图(原来有的边就去掉,没有的边加上),那么图的最大独立集就是补图的最大团。
最小路径点覆盖:也称最小路径覆盖。对DAG图,用最少的点、不重复的且互不相交的路径将所有点覆盖住,这样的问题称为最小路径覆盖。
最小路径重复点覆盖:在最小路径点覆盖问题的基础上,取消了点不重复的限制,既两条路径之间可以有公共点、公共边。最小路径点覆盖做法:拆点。对于原图中的每个点,比如 1 、 2 、 3 、 . . . 、 n 1、2、3、...、n 1、2、3、...、n,再建立一个新点 1 ′ 、 2 ′ 、 3 ′ 、 . . . 、 n ′ 1^{'}、2^{'}、3^{'}、...、n^{'} 1′、2′、3′、...、n′。原点称为出点,新点称为入点。如果原来存在边 i → j i → j i→j,那么在新图中必然存在 i → j ′ i → j{'} i→j′,即出点指向入点,所以新图一定是一个二分图(本来是一个有向图,但是实际上可以看成无向图)。最终答案为: n − n- n−最大匹配数量。
最小路径重复点覆盖做法:1、先求原图 G G G的传递闭包得到图 G ′ G^{'} G′;2、原图的最小路径重复点覆盖 = 新图的最小路径点覆盖。 - 最优匹配:KM算法
- 多重匹配
证明去听视频。4、5见进阶课。
0、二分图算法基础模型
0.1 染色法判定二分图
奇数环: 图中环的边数是奇数。
图的重要性:一个图是二分图,等价于 图中不含奇数环。
算法思路:
从图中一个点开始遍历,将每条边的两个点染成不同的颜色(图中任意一条边的两个顶点一定是不同的颜色),依次下去。由于图中不含有奇数环,所以染色过程中一定没有矛盾。
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010, M = 2e5 + 20;
int n, m;
int h[N], e[M], ne[M], idx;
int color[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c) {
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (color[j] && color[j] != 3 - c) return false;
if (!color[j] && !dfs(j, 3 - c)) return false;
}
return true;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
bool flag = true;
for (int i = 1; i <= n; i++)
if (!color[i])
if (!dfs(i, 1)) {
flag = false; break;
}
if (flag) puts("Yes"); else puts("No");
return 0;
}
0.2 二分图的最大匹配——匈牙利算法
二分图的最大匹配:
从二分图中选择n
条边,这n
条边的任意两条边都不会共用一个顶点,当n
取最大时,就称之为最大匹配。
算法思路:
注: 代码中的边是从左边指向右边的有向边!
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N]; // 那个妹子和那个男的在一块
bool st[N]; // 查看这个点是否搜索过
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true;
if (!match[j] || find(match[j])) {
match[j] = x; return true;
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n1, &n2, &m);
memset(h, -1, sizeof h);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
int res = 0;
for (int i = 1; i <= n1; i++) {
memset(st, 0, sizeof st);
if (find(i)) res++;
}
printf("%d\n", res);
return 0;
}
1、关押罪犯
将一个图染色成二分图,使得同一组内部的边的权值之和最小。
我们在 [ 0 , 1 0 9 ] [0,10^9] [0,109]之间枚举最大边权 l i m i t limit limit, 当 l i m i t limit limit固定之后,剩下的问题就是判断能否将所有点分成两组,使得所有权值大于 l i m i t limit limit的边都在组间,而不在组内。也就是判断由所有点以及所有权值大于 l i m i t limit limit的边构成的新图是否是二分图。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20010, M = 200010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int color[N]; // 0表示未染色,1表示染白色,2表示染黑色
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c, int mid) {
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (w[i] <= mid) continue;
if (color[j]) {
if (color[j] == c) return false;
} else if (!dfs(j, 3 - c, mid)) return false;
}
return true;
}
bool check(int mid) {
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i++)
if (!color[i])
if (!dfs(i, 1, mid))
return false;
return true;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
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);
return 0;
}
2、棋盘覆盖
将两个相邻的空格子看成了两个点并且之间有一条边,那么问题就转换为了:最多取多少条边,且选出来的每条边没有公共点(卡片不重叠)—— 最大匹配问题。
那么这种图是否是二分图呢?如下图所示,对图进行如下染色,橙色部分为黑色,空白部分为白色,所以可以发现此图按任意两个相邻格子及其之间建立一条边的规则来建图,所有边两端颜色均不相同,此图是一个二分图。并且还可以发现,对其建立坐标后,所有黑色格子均是偶数,白色格子均是奇数。
#include <cstring>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
PII match[N][N]; // 匹配的点
bool g[N][N], st[N][N]; // g存储哪个格子是坏的,st用于判重
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool find(int x, int y) {
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a && a <= n && b && b <= n && !g[a][b] && !st[a][b]) {
st[a][b] = true;
PII t = match[a][b];
if (t.x == -1 || find(t.x, t.y)) {
match[a][b] = {x, y};
return true;
}
}
}
return false;
}
int main() {
cin >> n >> m;
while (m--) { // 读入坏格
int x, y;
cin >> x >> y;
g[x][y] = true;
}
memset(match, -1, sizeof match);
int res = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if ((i + j) % 2 && !g[i][j]) { // 枚举奇数点(枚举偶数点也可以)
memset(st, 0, sizeof st);
if (find(i, j)) res++;
}
cout << res << endl;
return 0;
}
3、机器任务
因为两台机器 A 、 B A、B A、B最初都是处于状态 0 0 0,那么如果两个数组 a [ i ] 、 b [ i ] a[i]、b[i] a[i]、b[i]中存在数字 0 0 0,就可以直接执行不需要重启,所以可以直接排除掉 a [ i ] 、 b [ i ] a[i]、b[i] a[i]、b[i]中值为 0 0 0的点,剩余 a [ i ] 、 b [ i ] a[i]、b[i] a[i]、b[i]中所有的值均 > 0 >0 >0。问题就转换为了在 A 、 B A、B A、B两台机器的 N + M − 2 N+M-2 N+M−2种模式中,最少选择多少个模式可以将 a [ i ] 、 b [ i ] a[i]、b[i] a[i]、b[i]中值 > 0 >0 >0的任务全部完成。如果将每一个任务看成 a i → b i a_i → b_i ai→bi的一条无向边,做掉一个任务就是选择这条边上的某一个点。那么问题就转换为了在 N + M − 2 N+M-2 N+M−2中最少选择多少个点,可以将所有的边全部覆盖住——最小点覆盖问题。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int n, m, k;
int match[N];
bool g[N][N], st[N];
bool find(int x) {
for (int i = 1; i < m; i++)
if (!st[i] && g[x][i]) {
st[i] = true;
if (match[i] == -1 || find(match[i])) {
match[i] = x;
return true;
}
}
return false;
}
int main() {
while (cin >> n, n) {
cin >> m >> k;
memset(g, 0, sizeof g);
memset(match, -1, sizeof match);
while (k--) {
int t, a, b;
cin >> t >> a >> b;
if (!a || !b) continue;
g[a][b] = true;
}
int res = 0;
for (int i = 1; i < n; i++) {
memset(st, 0, sizeof st);
if (find(i)) res++;
}
cout << res << endl;
}
return 0;
}
4、骑士放置
将棋盘所有格子看成图中一个点,如果两个格子可以相互攻击到,那么在这两个格子之间建立一条无向边,那么问题转换为了:在这个图中最多可以选择多少个点,使得任意两个点之间不能相互攻击——最大独立集。
如果对图进行上面第二题一样的染色,即横纵坐标之和为奇数染黑色,和为偶数然白色,如下图所示,
那么可以发现,将处于位置
(
3
,
3
)
(3,3)
(3,3)的骑士与其可以攻击到的格子之间建立一条边所建立的图是一个二分图。
证明:对处于位置 ( x , y ) (x,y) (x,y)的骑士,其横纵坐标的变化均为 ± 1 \pm1 ±1或者 ± 2 \pm2 ±2,即最后变换完之后的格子上的横纵坐标之和为: x + y + ( x+y+( x+y+(奇数 + + +偶数 ) ) ) = x + y + x+y+ x+y+奇数 。所以当前能够攻击到的格子必然和当前格子的奇偶性不同,所以该图一定是一个二分图。
最后答案为: n × m − T − n \times m - T - n×m−T− 最大匹配数
棋盘问题:当 n n n比较小,使用状态压缩DP,但是状态压缩DP大多求方案数;二分图解多用于求 m a x max max。
#include <cstring>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m, k;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
bool find(int x, int y) {
for (int i = 0; i < 8; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 1 || a > n || b < 1 || b > m) continue;
if (g[a][b]) continue;
if (st[a][b]) continue;
st[a][b] = true;
PII t = match[a][b];
if (t.x == 0 || find(t.x, t.y)) {
match[a][b] = {x, y};
return true;
}
}
return false;
}
int main() {
cin >> n >> m >> k;
for (int i = 0; i < k; i++) {
int x, y;
cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (g[i][j] || (i + j) % 2) continue;
memset(st, 0, sizeof st);
if (find(i, j)) res++;
}
cout << n * m - k - res << endl;
return 0;
}
5、捉迷藏
在一个有向无环图中,选出来尽可能多的点,使得点集中任意两点之间,都不能从一个点出发到达另外一个点。首先求出图中的最小路径重复点覆盖有 c n t cnt cnt条,由于 c n t cnt cnt条路径已经将所有点覆盖,所以结果 k k k只能从这 c n t cnt cnt条路径中选择且每条路径上最多只能选择一个点,因此 k ≤ c n t k \le cnt k≤cnt。最后可知: k = c n t k = cnt k=cnt,证明看视频。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, M = 30010;
int n, m;
bool d[N][N], st[N];
int match[N];
bool find(int x) {
for (int i = 1; i <= n; i++)
if (d[x][i] && !st[i]) {
st[i] = true;
int t = match[i];
if (t == 0 || find(t)) {
match[i] = x;
return true;
}
}
return false;
}
int main() {
scanf("%d%d", &n, &m);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
d[a][b] = true;
}
// 传递闭包
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] |= d[i][k] & d[k][j];
int res = 0;
for (int i = 1; i <= n; i++) {
memset(st, 0, sizeof st);
if (find(i)) res++;
}
printf("%d\n", n - res);
return 0;
}
6、点的赋值
算法思路
因为每个不连通的连通块相互独立,分别求每个连通块的方案数,然后求乘积。
又有要使一条边上两个端点的和为奇数,必须是奇数 + 偶数
。所以对于每一个连通图,可以将所有奇数、偶数分别放在不同的集合中,然后连线可以构成一个二分图。所以如果这个图有方案,那么这个图肯定是二分图;反之,如果这个图是二分图,那么它一定存在解。
对于一个连通块的方案数,若这个连通块构成的图是二分图,那么由二分图的性质,对任意一个点,只要其确定了属于哪一个集合,那么这个图中剩余点属于的集合就确定了。假设有左边集合有x
个元素,右边集合有y
个元素,且由题知所有数仅为1、2、3
中之一:
- 假设左边为奇数,右边为偶数,那么可以知道每个奇数有
2
种取法,每个偶数仅有1
种取法,那么这种方案总共有2^x * 1^y = 2^x
种取法; - 假设左边为偶数,右边为奇数,同理可知这种方案有
2^y
种取法;
综上,若一个图是二分图,边总有2^x + 2^y
种取法,其中x
和y
分别为图中奇数和偶数的个数。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 300010, M = N * 2, MOD = 998244353;
int n, m;
int h[N], e[M], ne[M], idx;
int col[N]; // 染色
int s1, s2; // 集合点数
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int pow2(int k) {
int res = 1;
while (k--) res = res * 2 % MOD;
return res;
}
bool dfs(int u, int c) {
col[u] = c;
if (c == 1) s1++;
else s2++;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (col[j] && col[j] != 3 - c) return false;
if (!col[j] && !dfs(j, 3 - c)) return false;
}
return true;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
/* 这里不能全部初始化,会TLE */
/* 每个字节为4,本题只用到了 n + 1 个点 */
memset(h, -1, (n + 1) * 4);
memset(col, 0, (n + 1) * 4);
idx = 0;
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
int res = 1;
for (int i = 1; i <= n; i++) // 枚举所有连通块
if (!col[i]) // 当前没有没染色,说明是一个新的连通块
{
s1 = s2 = 0;
if (dfs(i, 1)) res = (LL) res * (pow2(s1) + pow2(s2)) % MOD;
else {
res = 0;
break;
}
}
printf("%d\n", res);
}
return 0;
}