二分图(概念、相关算法和题目应用)(全面整理)

TP

  • 二分图的概念:
  • 二分图常用算法:
      • 染色法(判断一个图是否为二分图):
      • 匈牙利算法(求出二分图的最大匹配数):
  • 相应题目应用:
    • 二分图 染色 应用:
      • Acwing:关押罪犯
    • 二分图最大匹配应用:
      • Acwing:棋盘覆盖
      • 洛谷:矩阵游戏
    • 二分图最大匹配的一些推论:
    • 二分图最小点覆盖应用:
      • Acwing:机械任务
      • Acwing:泥地
    • 二分图最大独立集应用:
      • Acwing:骑士放置
    • 二分图 最大路径点覆盖 与 最大路径重复点覆盖 应用:
      • Acwing:捉迷藏

二分图的概念:

二分图通常针对 无向图 问题(有些题目虽然是有向图,但一样有二分图性质)

在一张图中,如果能够把全部的点分到 两个集合 中,保证两个集合内部没有 任何边 ,图中的边 只存在于两个集合之间,这张图就是二分图
——————————————————————————————————————————

二分图常用算法:

染色法(判断一个图是否为二分图):

算法原理就是,用 黑 与 白 这两种颜色对图中点染色(相当于给点归属一个集合),一个点显然不能同时具有两种颜色,若有,此图就不是二分图

二分图(概念、相关算法和题目应用)(全面整理)_第1张图片
二分图(概念、相关算法和题目应用)(全面整理)_第2张图片
代码:

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]) {//如果已经被染过色了
			if (color[j] == c)return false;
			//就需要判断一下,如果两点颜色一样,染色就冲突了
		}
		else if (!dfs(j, 3 - c))return false;
		//否则dfs去染下一个结点,赋予的颜色肯定要跟 c 不一样
		//3 - 1 == 2,3 - 2 == 1
		//同时传回染色成功与否的信息
	}
	return true;
}

bool check() {
	memset(color, 0, sizeof color);//0 —— 未染色,1 —— 黑色,2 —— 白色
	for (int i = 1; i <= n; i++)
		if (color[i] == 0)//一旦某个点没染过色,dfs去染色
			if (!dfs(i, 1))return false;//如果传回false显然失败,此图不是二分图
	return true;
	//否则true
}

遍历了这张图的点和边,时间复杂度 O ( n + m ) O(n + m) O(n+)

——————————————————————————————————————————

匈牙利算法(求出二分图的最大匹配数):

满足 是二分图 这个前提,才能使用匈牙利算法

所谓 最大匹配数 的意思就是:

两个集合分别选一个点,这两个点之间有边就确认一段关系(一个集合中的两点 占有 另一集合中同一个点 是不合法的 一夫一妻(确信) ),最多的关系数量就是这张二分图的最大匹配
二分图(概念、相关算法和题目应用)(全面整理)_第3张图片
代码:

bool find(int x) { //标准匈牙利
	for (int j = 1; j <= n; j++)
		if (!st[j] && g[x][j]) { 
		//x 点连向的所有点(因为是二分图,所以这些点都在右集合),如果存在边且没标记过
			st[j] = true;
			//标记一下,防止多次遍历
			int t = match[j];
			//右集合中该点的匹配对象

			if (!t || find(t)) { 
			//没对象就可以和 x 匹配,有的话就让 t 尝试更改对象,能更改就和 x 匹配,不能就false
				match[j] = x;
				return true;
			}
		}
	return false;
}

int main() {
	cin >> n;
	int ans = 0;
	for (int i = 1; i <= n; i++) { //遍历左集合
		memset(st, 0, sizeof st);//每次都要重置标记
		if (find(i))ans++;//一旦有一个匹配,数量就++
	}
	cout << ans;

	return 0;
}

最坏情况会每个点遍历全部边一次,所以时间复杂度是 O ( n m ) O(nm) O(nm)

但匈牙利算法还是很优秀的,大部分情况时间都比较小

如果想要更优秀的算法左转 网络流 吧,匈牙利匹配本质上还是网络流的一种特殊形式,网络流可以更好地解决此类问题。网络流真是太简单了bushi

——————————————————————————————————————————

相应题目应用:

二分图 染色 应用:

Acwing:关押罪犯

二分图(概念、相关算法和题目应用)(全面整理)_第4张图片
题意又臭又长,总结就是:

尽可能将 仇恨值大的 两名罪犯放在不同监狱中

把仇恨值当作罪犯之间的边的边权,两座监狱看作两个集合

这道题就变成了 如何让二分图 两集合之间的边权 尽可能大(使得集合内部边权尽可能小,冲突也就没那么激烈)

观察数据范围,此题可以用 二分 + 染色法 求解

代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 20010, M = 200010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], e[M], ne[M], w[M], idx;
int color[N];//染色,记录该点的状态:0 —— 未染色、1 —— 染白、2 —— 染黑

void add(int a, int b, int x) {
	e[idx] = b, w[idx] = x, 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;
		//mid是答案,是设定的集合内部最大边权,如果该边小于等mid,边两点显然可以随意处理,放在一个集合内也行

		//否则,该边两点就严格要放在两个不同集合,防止冲突

		if (color[j]) { //连向的点若被染过色
			if (color[j] == c)return false;//显然色不能相同
		}
		else if (!dfs(j, 3 - c, mid))return false;//没被染过就染该点
	}
	return true;//成功
}

bool check(int mid) {
	mem(color, 0);//每次重新对点染色
	for (int i = 1; i <= n; i++)
		if (color[i] == 0)//枚举每一个点是因为有可能有多个连通块
			if (!dfs(i, 1, mid))return false;//要保证都为二分图
	return true;
}

int main() {
	cinios;

	cin >> n >> m;
	mem(h, -1);
	for (int i = 0; i < m; i++) {
		int a, b, x;
		cin >> a >> b >> x;
		add(a, b, x);//二分图通常针对的都是无向图
		add(b, a, x);
	}

	int l = 0, r = 1e9;//二分答案
	while (l < r)
	{
		int mid = l + r >> 1;//mid 即是最小的最大影响力
		//比 mid 小的影响显然都可以随意塞到监狱中
		//比 mid 大的影响要让其尽可能不存在(即一边两点分别在图的两个不同子集中)
		//也就是询问能否用比 mid 大的边建立一个二分图

		if (check(mid))r = mid;//能的话显然包含答案
		else l = mid + 1;//不能就要增大,右移往答案靠拢
	}

	cout << l;

	return 0;
}

——————————————————————————————————————————

二分图最大匹配应用:

Acwing:棋盘覆盖

二分图(概念、相关算法和题目应用)(全面整理)_第5张图片
看上去像状压dp,但一看数据范围 2 100 2^{100} 2100状压肯定炸,所以探讨一下是否具有二分图的性质

如果把每个格子看成点,放置骨牌(长2宽1)的操作可以看成 两点之间连一条边

那每个格子能放置骨牌就有四种情况四个方向连四条边(除非另一个格子被禁止放置)

每个格子都这样处理,可以得到一张图

可以发现的是,能放置最多骨牌 == 能不重复尽可能多地选取边 == 最大匹配数量

之前说过,求最大匹配的前提是这张图是二分图,所以我们需要判断下这张图是否具有二分图的性质

纸上作画一下便可发现,奇数格/偶数格 彼此之间是没有边的(相当于二分图中的两个集合)

因此,
代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 110, M = 50010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
bool g[N][N], st[N][N];
PII match[N][N];
int dx[] = { -1,0,1,0 }, dy[] = { 0,1,0,-1 };

//防止长2宽1的骨牌,可以看作是在相邻两格之间建边(要求合法)
//结果要求最多能放多少块骨牌,就相当于 “ 最多有多少条匹配边 ”
//经过观察象棋棋盘上黑白格的分布,我们可以推导出,N行N列的棋盘具有二分图性质
//至此,该问题就变成了一个二分图最大匹配求解问题

int find(int x, int y) { //匈牙利算法
	for (int i = 0; i < 4; i++) { //每个点的边指向都确定了,所以可以不建边
		int a = x + dx[i], b = y + dy[i];

		if (a <= 0 || b <= 0 || a > n || b > n || g[a][b])continue;
		//越界或者触碰禁止的格子就不处理

		PII t = match[a][b];
		if (st[t.fx][t.fy])continue;//防止多次遍历
		st[t.fx][t.fy] = true;

		if (t.fx == 0 || find(t.fx, t.fy)) { //一旦该点没被标记过,或者点曾经的对象可以找到下家
			match[a][b] = { x,y };
			return true;//该点就能匹配

			//搜索时是遍历左子集往右子集的边,但match记录的是右子集点的对象
		}
	}
	return false;
}

int main() {
	cinios;

	cin >> n >> m;

	while (m--)
	{
		int a, b;
		cin >> a >> b;
		g[a][b] = true;//点数较小,用邻接矩阵存储方便
	}

	int ans = 0;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			if (i + j & 1 && !g[i][j]) { //只对集合一半的点处理匹配,同时不能是障碍
				mem(st, 0);//每次要重置标记
				if (find(i, j))ans++;//匹配成功++
			}

	cout << ans;

	return 0;
}

同类型题

洛谷:矩阵游戏

二分图(概念、相关算法和题目应用)(全面整理)_第6张图片
洛谷题解就挺好

目的是使得最终(1,1)(2,2)…(n,n)都有一个点

可以看作为,最终状态需要每 i 行和 i 列都存在一个匹配

建图方式:对于 i 行 j 列的1点,建一条 i 连向 j 的边即可,最后跑一个二分图匹配,只有匹配数为 n 才能说明有解

可以证明交换行、交换列的操作不会影响匹配数

代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 210, M = 40010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int h[N], e[M], ne[M], idx;
int match[N << 1];
bool st[N << 1];

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; i = ne[i]) {
		int j = e[i];
		if (st[j])continue;
		st[j] = true;

		if (!match[j] || find(match[j])) {
			match[j] = x;
			return true;
		}
	}
	return false;
}

int main() {
	cinios;

	cin >> T;
	while (T--)
	{
		cin >> n;
		mem(h, -1);
		idx = 0;

		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++) {
				cin >> k;
				if (k)add(i, n + j);//行对列建边
				//虽然二分图是无向图问题,但只用建单向边即可,枚举其中一个集合
			}

		mem(match, 0);
		int mat = 0;
		for (int i = 1; i <= n; i++) {
			mem(st, 0);
			if (find(i))mat++;
		}

		if (mat == n)cout << "Yes";//匹配数必须为 n
		else cout << "No";
		cout << '\n';
	}

	return 0;
}

——————————————————————————————————————————

二分图最大匹配的一些推论:

由最大匹配推导而来,本文不细节探讨证明(我也不会啊
二分图(概念、相关算法和题目应用)(全面整理)_第7张图片
——————————————————————————————————————————

二分图最小点覆盖应用:

证明可得:二分图的最大匹配数 == 最小点覆盖(证明理解不能,寄了)

什么是最小点覆盖?

最小点覆盖并不只在二分图中才存在

在一个图中任意选取 最少 多少个点(两个集合中都可以选!),可以保证图中 所有的边都与选取的点相连

这个数量就是最小点覆盖

Acwing:机械任务

二分图(概念、相关算法和题目应用)(全面整理)_第8张图片
代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 110, M = 400010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int match[N];
bool st[N], g[N][N];

//证明可得:二分图的最大匹配数 == 最小点覆盖(证明理解不能,寄了)

//什么是最小点覆盖?
//最小点覆盖并不只在二分图中才存在,任意一个图中
//选取 最少 多少个点,可以保证图中 所有的边都与选取的点相连
//这个数量就是最小点覆盖

bool find(int x) { //标准匈牙利
	for (int i = 1; i < m; i++)
		if (!st[i] && g[x][i]) { //确保有边
			st[i] = true;
			int t = match[i];
			if (t == 0 || find(t)) {
				match[i] = x;
				return true;
			}
		}
	return false;
}

int main() {
	cinios;

	//该题就可以转化成一个最小点覆盖问题,然后用二分图最大匹配求解

	//对于每一个任务 i,可以选择(A 的 a[i] 模式)或(B 的 b[i] 模式)解决
	//
	//所以在 a[i] 和 b[i] 之间建一条边,代表任务 i(这样的图显然是二分图)
	//要完成所有任务显然表示要 选取所有的边
	// 
	//这个问题就可以转化成:最少标记多少个点,即在 a[i] 与 b[i] 中选择其中一个,可以保证图中所有的边都与选取的点相连

	while (cin >> n, n)
	{
		cin >> m >> k;
		mem(g, 0);
		mem(match, 0);

		while (k--)
		{
			int t, a, b;
			cin >> t >> a >> b;
			if (!a || !b)continue;//0模式初始就能解决
			g[a][b] = true;//无向图,但建单向边就可以find了
		}

		int ans = 0;
		for (int i = 1; i < n; i++) {
			mem(st, 0);
			if (find(i))ans++;
		}
			
		cout << ans << '\n';
	}

	return 0;
}

同类型题

思维上更巧妙的最小点覆盖应用

Acwing:泥地

二分图(概念、相关算法和题目应用)(全面整理)_第9张图片
代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 2010, M = 500010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int match[N], r[N][N], c[N][N], rcnt = 1, ccnt = 1;
//记录每个点所在的泥地连续行、连续列,行对列连边,集合自然分为行集合、列集合,满足二分图性质
//最后求的就是,最少的点覆盖所有的边,即最小点覆盖
bool st[N], g[N][N];
string s[60];
//注意一下数组大小

bool find(int x) { //标准匈牙利
	for (int j = 1; j <= ccnt; j++)
		if (!st[j] && g[x][j]) {
			st[j] = true;
			int t = match[j];

			if (!t || find(t)) {
				match[j] = x;
				return true;
			}
		}
	return false;
}

int main() {
	cinios;

	cin >> n >> m;
	for (int i = 0; i < n; i++)
		cin >> s[i];

	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			if (j && s[i][j] == '*' && s[i][j - 1] == '.')rcnt++;
			//一旦遇到干净地面,泥地行编号++
			if (s[i][j] == '*')r[i][j] = rcnt;//赋予该格行编号
		}
		rcnt++;//换行也要++
	}

	//对列同样操作,小心bug
	for (int i = 0; i < m; i++) { //bug —— i < n, j < m
		for (int j = 0; j < n; j++) {
			if (j && s[j][i] == '*' && s[j - 1][i] == '.')ccnt++;
			//bug —— s[j][i - 1]
			if (s[j][i] == '*')c[j][i] = ccnt;//bug —— c[i][j]
		}
		ccnt++;
	}

	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			if (s[i][j] == '*') //对于泥地格,建边
				g[r[i][j]][c[i][j]] = true;

	int ans = 0;
	for (int i = 1; i <= rcnt; i++) { //行集合匹配列集合
		mem(st, 0);
		if (find(i))ans++;
	}
	cout << ans;

	return 0;
}

——————————————————————————————————————————

二分图最大独立集应用:

最大独立集 是一个点数,指在一个图中选取最多多少个点,可以使得这些点所组成的集合 内部任意两点间没有边

最大独立集 ==(总点数 - 最小点覆盖)

Acwing:骑士放置

二分图(概念、相关算法和题目应用)(全面整理)_第10张图片
代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 110, M = 10010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
PII match[N][N];
bool st[N][N], g[N][N];
int dx[] = { -1,1,2,2,1,-1,-2,-2 }, dy[] = { 2,2,1,-1,-2,-2,-1,1 };

//最大匹配数 == 最小点覆盖 == 总点数 - 最大独立集
// 
//什么是最大独立集?最大独立集 是一个点数,指在一个图中选取最多多少个点,可以使得这些点所组成的集合 内部任意两点间没有边
//
//因此显然,当你把一个图中 最小点覆盖集合 中的点删去(最少的点覆盖所有的边),剩下的点组成的集合就是 最大独立集(总点数 - 最小点覆盖)

//该题问最多能放多少个不能互相攻击的骑士,即两点之间的边不可选取,两点之间的联系必须断开
//断开联系也就相当于删去 最小点覆盖

bool find(int x, int y) {
	for (int i = 0; i < 8; i++) { //走日八个方向
		int ix = x + dx[i], iy = y + dy[i];
		if (ix <= 0 || iy <= 0 || ix > n || iy > m || g[ix][iy] || st[ix][iy])continue;

		st[ix][iy] = true;
		PII t = match[ix][iy];
		if (t.fx == 0 || find(t.fx, t.fy)) {
			match[ix][iy] = { x,y };
			return true;
		}
	}
	return false;
}

int main() {
	cinios;

	cin >> n >> m >> k;
	for (int i = 0; i < k; i++)
	{
		int a, b;
		cin >> a >> b;
		g[a][b] = true;
	}

	int ans = n * m - k;//点数 == 总点数 减去 不能放骑士的格子
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			if (i + j & 1 && !g[i][j]) {
				mem(st, 0);
				if (find(i, j))ans--;//有一个匹配就相当于有一个点覆盖
				//减去
			}

	cout << ans;//就能得到答案

	return 0;
}

——————————————————————————————————————————

二分图 最大路径点覆盖 与 最大路径重复点覆盖 应用:

最小路径点覆盖含义:
用最少的点,覆盖图中全部的 不相交 路径,这个路径数是多少?
等于总点数 - 最小点覆盖 / 最大匹配数

最小路径点重复覆盖含义:
用最少的点,覆盖图中全部路径(可以有分叉),这个路径数是多少?
等于 对图做一个传递闭包后的 总点数 - 最小点覆盖 / 最大匹配数

Acwing:捉迷藏

二分图(概念、相关算法和题目应用)(全面整理)_第11张图片
代码:

#include
#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define ul (u << 1)
#define ur (u << 1 | 1)
#define fx first
#define fy second
//#pragma GCC optimize(2)
//[博客地址](https://blog.csdn.net/weixin_51797626?t=1) 
using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int N = 210, M = 10010, MM = 3000010;
int INF = 0x3f3f3f3f, mod = 100003;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
int match[N];
bool st[N], g[N][N];

//最小路径点覆盖含义:
//用最少的点,覆盖图中全部的 不相交 路径,这个路径数是多少?
//等于总点数 - 最小点覆盖 / 最大匹配数
// 
//最小路径点重复覆盖含义:
//用最少的点,覆盖图中全部路径(可以有分叉),这个路径数是多少?
//等于 对图做一个传递闭包后的 总点数 - 最小点覆盖 / 最大匹配数

//此类问题为单向无环图,建二分图比较特殊
//给出的 n 个点作为起点,再虚构 n 个点作为终点,单向边就是左边连右边

//连在一条边上的两点显然在一条路径上,这 n 个点中的孤立点就是某路径的终点
//此题要求:选出最多多少个点,保证这些点相互之间都不在同一路径上

bool find(int x) {
	for (int i = 1; i <= n; i++)
		if (!st[i] && g[x][i]) {
			st[i] = true;
			if (match[i] == 0 || find(match[i])) {
				match[i] = x;
				return true;
			}
		}
	return false;
}

int main() {
	cinios;

	cin >> n >> m;
	while (m--)
	{
		int a, b;
		cin >> a >> b;
		g[a][b] = true;
	}

	for (int k = 1; k <= n; k++)//传递闭包
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				g[i][j] |= g[i][k] & g[k][j];//让所有点都尽可能匹配到

	int res = 0;
	for (int i = 1; i <= n; i++) {
		mem(st, 0);
		if (find(i))res++;
	}
	cout << n - res;

	return 0;
}

——————————————————————————————————————————

笔记做吐了ou

你可能感兴趣的:(知识点笔记,算法,图论,二分图,染色法,c++)