原理
显然(哈哈),如果一个图是二分图,则其去掉一些边之后还是二分图。
下面证明一个重要的定理:一个图是二分图 ⟺ \iff ⟺图中不存在奇数环 ⟺ \iff ⟺染色过程中不存在矛盾
首先证明充分性:图中不存在奇数环 ⇒ \Rightarrow ⇒染色过程中不存在矛盾。可以证明其逆否命题成立,即染色过程中存在矛盾,则图中存在奇数环,因为染色法存在矛盾,说明一定存在相邻的两个点颜色相同,(白、黑、白、黑、…、白)因此一定存在奇数环;
再证明必要性:图中不存在奇数环 ⇐ \Leftarrow ⇐染色过程中不存在矛盾。可以使用反证法,染色法不存在矛盾,但存在奇数环,对于某个环而言,如果是奇数环的话,根据染色过程,一定有(白、黑、白、黑、…、白),最后一个一定是白,否则不能构成奇数环,此时发现染色法矛盾了,因为白色的点和白色的点相邻了。
首先证明必要性:一个图是二分图 ⇐ \Leftarrow ⇐染色过程中不存在矛盾。染色法不存在矛盾,则可以将白点和黑点分成两个部分,此时可以构造出一个二分图,成立。
再证明充分性:一个图是二分图 ⇒ \Rightarrow ⇒图中不存在奇数环。反证法,假设最后推出图中存在奇数环,则最终图中一定存在两个相邻的点颜色相同,矛盾,因此结论成立。
代码模板
#include
#include
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M], ne[M], idx;
int color[N]; // 0代表当前节点还未染色
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 当前给u染成颜色c, 返回是否能染色成功
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 (!dfs(j, 3 - c)) return false;
} else if (color[j] == 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 success = true; // 是否染色成功
for (int i = 1; i <= n; i++)
if (!color[i]) {
if (!dfs(i, 1)) {
success = false;
break;
}
}
if (success) puts("Yes");
else puts("No");
return 0;
}
(1)我们可以随便考察两个集合中的某个集合,比如我们考察男生这个集合,按照编号递增的顺序给每个男生找女朋友;
(2)考察男1号,因为女6号还没有确定恋爱关系,所以可以与女6号成功牵手,连上红线;
(3)接着考察男2号,因为女5号还没有确定恋爱关系,所以可以与女5号成功牵手,连上红线;此时确定的恋爱关系如下图:
(4)接着考察男3号,但发现女6号已经和男1号确定恋爱关系了,此时就是匈牙利算法的关键了,此时女6号就会找到她现在的男友(1号),看看他能不能换个女友,如果不能换的话,男3号接着看下一个他喜欢的女生,但是结果发现男1号可以换个女友,此时1、6分手,1号和8号确定恋爱关系,然后3、6号成为情侣,如下图(绿色代表分手了):
(5)考察男4号,因为女7号还没有确定恋爱关系,所以可以与女7号成功牵手,连上红线;如下图:
最终有四条红线,代表有四对情侣,因此最大匹配是4。
下面这个网友的评论很精彩:
#include
#include
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
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; // 那x就预定这个女孩了
// 如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功
if (match[j] == 0 || 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, false, sizeof st);
if (find(i)) res++;
}
printf("%d\n", res);
return 0;
}
(1)首先证明 n ≥ m n \ge m n≥m。因为对于最大匹配来说,每两条边之间没有公共点,要想把所有用点覆盖所有的边,则必须每条边都选一个端点,所以 n ≥ m n \ge m n≥m。
(2)证明等号可以成立。这里使用构造性证明。
(2.1)求二分图的最大匹配;
(2.2)从左部每个非匹配点出发,做一遍增广(一定不会成功,成功的话就可以换成增广了),标记所有经过的点(左右都需要标记)
做完上面的步骤之后,就可以构造出一个最小覆盖的方案,方案中包含的点=左部所有未被标记的点+右部所有被标记的点。如下图:
因为:方案中包含的点 = 左部所有未被标记的点 + 右部所有被标记的点。又左部所有未被标记的点一定是匹配点(逆否命题),右部所有被标记的点一定是匹配点(逆否命题),因为又有性质③,对于每条匹配边,我们必然只选其中的一个点,被标记的选择右边的点,未被标记的选择左边的点,所以n可以取到m。
根据在左部右部,以及匹配不匹配的情况,我们可以构造出四种边:
① 左边匹配,右边匹配;②左边非匹配,右边匹配;③ 左边匹配,右边非匹配;④ 左边非匹配,右边非匹配;
对于第①种情况我们已经讨论过;对于第④种情况不可能出现,否则的话我们求的不是最大匹配。
对于第②种情况,左边非匹配的点一定是被标记的点,因为走的是增广路径,走到右部的点一定是被标记的,这样的边一定被选;
对于第③种情况,这种情况不可能存在,右边非匹配一定没有被标记,因此左部的点不可能是匹配点;
(1)每条路径转化到新图中一定对应新图的一个匹配,即每个点只会在一条边中。反之也成立。
(2)我们可以看一下原图中每条路径的终点,对应到新图中的出点是没有出边的,即左部的非匹配点,例如上图中的点3。同理左部的每个非匹配点一定也对应着原图中的路径。即使孤立点也可以看成一个终点,符合要求。
最小路径点覆盖 ⟺ \iff ⟺让左部的非匹配点最少 ⟺ \iff ⟺让左部的匹配点最多 ⟺ \iff ⟺找最大匹配。
(1)求原图的传递闭包得到新图;
(2)则原图的最小路径重复点覆盖 ⟺ \iff ⟺新图的最小路径点覆盖。
下面对上述等价性进行证明:
① 充分性:依次考虑原图的每条符合条件的路径,当我们考察到第i条路径时,如果路径上的点和前i-1条边上的点重复,则直接跳过即可,新图中加了很多边,可以跳过。另外第i条路径上的点不可能全部和前i-1条边上的点重复,否则的话第i条路径就没有存在的必要了。
② 必要性:将新图中间接转移过去的边展开成原来的边即可得到原图中的路径。
问题描述
分析
代码
#include
#include
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];
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 (!dfs(j, 3 - c, mid)) return false;
} else if (color[j] == c) return false;
}
return true;
}
bool check(int mid) {
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i++)
if (!color[i] && !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;
}
问题描述
分析
代码
#include
#include
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m; // 边长、不能放置的点的数量
bool g[N][N]; // 为true代表有障碍物
PII match[N][N]; // 存储第二个集合中的每个点 当前 匹配的第一个集合中的点是哪个
bool st[N][N]; // 表示第二个集合中的每个点是否已经被遍历过
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 < 1 || a > n || b < 1 || b > n) continue;
if (st[a][b] || g[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;
while (m--) {
int a, b;
cin >> a >> b;
g[a][b] = true;
}
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;
}
问题描述
分析
代码
#include
#include
using namespace std;
const int N = 110;
int n, m, k;
bool g[N][N]; // 表示两点之间是否有任务
int match[N];
bool 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;
}
问题描述
分析
代码
#include
#include
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m, k;
bool g[N][N]; // 代表是否有障碍物
PII match[N][N];
bool 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] || 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;
}
问题描述
分析
(1)首先一定有 K ≤ c n t K \le cnt K≤cnt,否则一条路径上会选出两个点,会相互看见。接着证明K可以取到cnt。
(2)我们找出这cnt条路径的终点,这些终点肯定两两都不相同,否则的话某条路径上可以删除重复的终点,仍然符合要求。这些记为集合V,然后我们将这些终点所有能反向到达的点的集合记为 n e x t ( V ) next(V) next(V)。
(2.1)如果有 V ⋂ n e x t ( V ) = ∅ V \bigcap next(V) = \emptyset V⋂next(V)=∅,意味着我们从E出发是不可能到达V内部的点的,此时选择E中的cnt个点是符合要求的。
(2.2)如果有 V ⋂ n e x t ( V ) ≠ ∅ V \bigcap next(V) \neq \emptyset V⋂next(V)=∅,则我们可以从V中选择出某一个终点 v i v_i vi,我们让 v i v_i vi沿着边反向向前走(向前走到的点仍然记为 v i v_i vi),直到走到满足条件 v i ∉ n e x t ( V − { v i } ) v_i \not \in next(V -\{v_i\}) vi∈next(V−{vi}),一定是可以找到这样点 v i v_i vi的,否则这条路径就是多余的。如果仍然不为空,继续选择另一个终点进行这样的操作,直到交集为空集为止。
代码
#include
#include
using namespace std;
const int N = 210, M = 30010;
int n, m;
bool d[N][N]; // 邻接矩阵
int match[N];
bool st[N];
bool find(int x) {
for (int i = 1; i <= n; i++)
if (d[x][i] && !st[i]) {
st[i] = true;
if (match[i] == 0 || find(match[i])) {
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;
}
问题描述
分析
代码
/**
* 执行用时:32 ms, 在所有 C++ 提交中击败了40.35%的用户
* 内存消耗:14.2 MB, 在所有 C++ 提交中击败了5.03%的用户
*/
class Solution {
public:
vector<int> color;
vector<vector<int>> g;
bool isBipartite(vector<vector<int>> &graph) {
g = graph;
int n = g.size();
color.resize(n, 0);
for (int i = 0; i < n; i++)
if (!color[i])
if (!dfs(i, 1))
return false;
return true;
}
bool dfs(int u, int c) {
color[u] = c;
for (auto w : g[u]) {
if (!color[w]) {
if (!dfs(w, 3 - c)) return false;
} else if (color[w] == c) return false;
}
return true;
}
};
/**
* Date: 2021/4/13 8:59
* 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
* 内存消耗:38.9 MB, 在所有 Java 提交中击败了65.81%的用户
*/
public class Solution {
int[] color; // 0代表当前节点还未染色, 1、2分别表示两种颜色
int[][] g;
public boolean isBipartite(int[][] graph) {
g = graph;
int n = g.length; // 图中顶点数
color = new int[n];
for (int i = 0; i < n; i++)
if (color[i] == 0)
if (!dfs(i, 1))
return false;
return true;
}
private boolean dfs(int u, int c) {
color[u] = c;
for (int w : g[u]) {
if (color[w] == 0) {
if (!dfs(w, 3 - c)) return false;
} else if (color[w] == c) return false;
}
return true;
}
}
问题描述
分析
代码
/**
* 执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
* 内存消耗:8.2 MB, 在所有 C++ 提交中击败了80.81%的用户
*/
class Solution {
public:
typedef pair<int, int> PII;
int n, m;
vector<vector<int>> g;
PII match[10][10];
bool st[10][10];
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 < 0 || a >= n || b < 0 || b >= m) continue;
if (st[a][b] || g[a][b]) continue;
st[a][b] = true;
PII t = match[a][b];
if (t.first == -1 || find(t.first, t.second)) {
match[a][b] = {x, y};
return true;
}
}
return false;
}
int domino(int _n, int _m, vector<vector<int>> &broken) {
n = _n, m = _m;
g = vector<vector<int>>(n, vector<int>(m, 0));
for (int i = 0; i < broken.size(); i++) {
int a = broken[i][0], b = broken[i][1];
g[a][b] = 1;
}
memset(match, -1, sizeof match);
int res = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if ((i + j) % 2 && !g[i][j]) {
memset(st, 0, sizeof st);
if (find(i, j)) res++;
}
return res;
}
};