个人对于二分图匹配的学习记录
二分图
二分图相关概念
前言 : 二分图问题的难点并不是他的相关算法有多难理解与实现,而在于如何将一个问题转化为二分图问题,所以本篇文章主要的思考方向是如何将一个问题转化为二分图问题。
在二分图意义下
匹配
一组没有公共点的,使两边点相连的一组边的集合称为匹配。
最大匹配
即二分图中边数最多的匹配。
最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖
增广路径
从一个非匹配点出发(图中的白点),交替地经过非匹配边(下图绿边)和匹配边(下图黑边),最后能到达一个非匹配点的路径,称为增广路径。
显然,增广路径取反后,匹配数量 + 1。
最大匹配的状态下图中不存在增广路径。
最大团
一个图的子图中,在保证每个点之间都有边的情况下,拥有点的个数最多的图称为最大团。
最小路径点覆盖
在一个有向无环图中,用最少的互不相交的路径,将所有的点覆盖。
最小路径点覆盖 = 总点数 - 最大匹配数
若路径可相交,则称为最小路径重复点覆盖。
最大独立集
一个图的子图中,在保证每个点之间没有边相连的情况下,拥有点的个数最多的图称为最大独立集。
要得到一个最大独立集,我们可以想象为在一个图中删除最少的点,将所有边破坏掉,那么剩下的点构成的图即为最大独立集。也就是说,最大独立集 = 总点数 - 最大匹配。
染色法判断二分图
染色法是将所有点染成两种颜色,对于每个点,我们根据需求,将与他有关系的点都染成与他本身不同的颜色,显然当全部点染色成功后,图里的点就能分为两部分,形成一个二分图,而如果染色不成功(中途点本身的颜色与要染的颜色冲突),该图就无法形成一个二分图。
在染色时,如果遇到冲突,便将前面所有颜色取反,若取反后还是冲突,则该图无法形成二分图。
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
const int M = 100005;
struct Edge {
int v, w, next;
} e[M << 1];
int head[N], eid;
int cl[N];
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
int n, m;
bool dfs(int u, int c) {
cl[u] = c;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (cl[v]) {
if (cl[v] == cl[u]) return false;
}else if (!dfs(v, 3-c)) return false;
}
return true;
}
bool check() {
memset(cl, 0, sizeof cl);
for (int i = 1; i <= n; i++) {
if (!cl[i]) {
if (!dfs(i, 1)) return false;
}
}
return true;
}
int main () {
memset(head, -1, sizeof head);
cin >> n >> m;
bool fg = 1;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
if (u == v) {
fg = 0;
continue;
}
addEdge(u, v, 1);
addEdge(v, u ,1);
}
if (!fg || !check()) puts("No");
else puts("Yes");
return 0;
}
本题题意是将所有罪犯关押到两个监狱中,以使得事件影响力最小,很明显是一道二分图问题。
而解决这道题的关键是我们如何划分这个二分图。
由于一个事件的影响力对结果有影响当且仅当两个罪犯关押在一个监狱,所以我们很容易能想到,要使整体影响力最小,我们就要把影响力大的事件的两个罪犯关押到两个不同的监狱,但因为罪犯之间的仇恨关系错综复杂,我们不可能使每个事件都不发生。所以一定有一些事件会发生,而我们所要做的就是让这些事件的最大值尽量小。
很明显,利用二分答案可以确定这个尽量小的最大值,我们通过二分答案找到一个 mid ,如果仇恨关系大于 mid 的罪犯们都能被关押到两个监狱,就说明 mid 是当前尽量小的最大值,而大于 mid 的值都不会是答案,由此可以看出二分条件的成立。
对于判断仇恨关系大于 mid 的罪犯们能否关押到两个监狱,只需用染色法即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 20005;
const int M = 100005;
struct Edge {
int v, w ,next;
} e[M<<1];
int head[N],eid;
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
int cl[N];
int n, m;
bool dfs(int u, int c, int x) {
cl[u] = c;
for (int i = head[u]; ~i; i = e[i].next) {
// if (x == 15257)cout << u << " " << e[i].v << " " << e[i].w <<endl;
if (e[i].w <= x) continue;
int v = e[i].v;
if (cl[v] && cl[v] == cl[u]) return false;
if (cl[v] && cl[v] != cl[u]) continue;
if (!dfs(v, 3 - c, x)) return false;
}
return true;
}
bool check(int x) {
memset(cl, 0, sizeof cl);
for (int i = 1; i <= n; i++) {
// if (x == 2) cout << i << endl;
if (!cl[i]) {
if (!dfs(i, 1, x)) return false;
}
}
return true;
}
int main () {
memset(head, -1, sizeof head);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, w);
}
int l = -1, r = 1e9 + 1;
while(r - l > 1) {
int mid = l + r >> 1;
// cout << ":::::" << l << " " << r << " " << mid << endl;
if (check(mid)) r = mid;
else l = mid;
}
cout << r << endl;
return 0;
}
匈牙利算法
下面展示的是dfs实现的写法。
//洛谷P3386 二分图最大匹配 匈牙利算法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1505;
const int M = 50005;
int head[N], eid;
struct Edge {
int v, w, next;
} e[M << 1];
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
int match[N], vis[N];// match 内下标为右部点,表示与右部点匹配的是第几个左部点
// vis 用于标记当前增广操作有没有遇到过该点,所以每次增广时都要清空。
bool dfs(int u) {
// cout << endl;
// cout << u << ": ";
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (!vis[v]) {
vis[v] = 1;//注意标记
// cout << v <<" ";
if (!match[v] || dfs(match[v])) { // 右部点无匹配或右部点原配存在其他匹配
match[v] = u; //该右部点匹配为当前点
// cout << "end " << u << endl;
return true;
}
}
}
// cout << "end " << u << endl;
return false;
}
//以上注释输出代码可用于打印增广路。
int main() {
ios::sync_with_stdio(false);
cin.tie();
memset(head, -1, sizeof head);
int n, m, e;
cin >> n >> m >> e;
for (int i = 1; i <= e; i++) {
int u, v;
cin >> u >> v;
addEdge(u, v, 1);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
memset(vis, 0, sizeof vis);
if (dfs(i)) {
ans++;
}
}
cout << ans << endl;
return 0;
}
//匈牙利算法的本质是优先考虑当前左部点的匹配,如果对应右部点无匹配则改为当前匹配,
//如果有匹配,则查询该右部点的原配能否与其他右部点匹配,可以则增广路增加,否则查询下一右部点。
//每次一个点成功匹配,则二分图最大匹配边数增加(显然)
//匈牙利算法的时间复杂度为 O(VE),其中 V 为左部点个数, E 为边的个数
//查找二分图的最大匹配,也可以用增广路算法(Augmenting Path Algorithm),时间复杂度为O(NE)
下面展示的是bfs实现的写法
//UOJ#78 二分图最大匹配模板题
#include<bits/stdc++.h>
#include<queue>
using namespace std;
typedef long long ll;
const int N = 505;
const int M = 250005;
struct Edge{
int v, w, next;
}e[M];
int head[N],eid;
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
int nl, nr, m;
int matchx[N],matchy[N], visx[N], visy[N], pre[N];//pre用于记录路径
queue<int> q;
void aug(int v) {
while (v) {
int t = matchx[pre[v]]; // t 为 v 当前所想匹配的左部点的原配
matchx[pre[v]] = v; // 当前左部点匹配当前右部点
matchy[v] = pre[v];//匹配
v = t;//更改原配的匹配
}
}
bool bfs(int s) {
memset(visy, 0, sizeof visy);
memset(visx, 0, sizeof visx);
memset(pre, 0 ,sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if(!matchy[v]) {
aug(v); // 修改匹配
return 1;
} else {
q.push(matchy[v]);
}
}
}
}
return false;
}
int main() {
memset(head, -1, sizeof head);
cin >> nl >> nr >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
addEdge(u, v, 1);
}
int ans = 0;
for (int i = 1; i <= nl; i++) {
if (bfs(i)) {
ans++;
}
}
cout << ans << endl;
for (int i =1; i<= nl; i++) {
cout << matchx[i] << " ";
}
cout << endl;
return 0;
}
匈牙利算法相关例题 : 洛谷P2759 模板题 , 洛谷P1129 矩阵游戏 。
//洛谷 P1129 矩阵游戏
//题目要求我们将 1 移动到 (1,1) (2,2)这种位置上
//对于每一行,由于我们可以进行列交换操作,所以只需要在当前行上找一个存在的列即可
//对于每一列,由于我们可以进行行交换操作,所以只需要在当前列上找一个存在的行即可
//而由于同一个位置上的 1 不能被行交换和列交换到两个位置,这样 1 的个数就增加了
//所以此时,每一行每一列就构成了一一对应的关系,即匹配
//至此,题目便转化成了二分图匹配问题,只需要最终行与列的匹配数等于 n 即可。
//证明:由于行与列都有 n 个,所以只有n 个行都找到对应的列,也就是 n 个匹配时,才能得到 n 个(i,i) 的位置
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 505;
const int M =40005;
int head[N],eid;
struct Edge {
int v, w, next;
} e[M<<1];
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
int match[N], vis[N];
bool dfs(int u) {
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (!vis[v]) {
vis[v] = 1;
if (!match[v] || dfs(match[v])) {
match[v] = u;
return true;
}
}
}
return false;
}
int main () {
ios::sync_with_stdio(false);
cin.tie();
int T;
cin >> T;
while(T--) {
memset(head, -1, sizeof head);
eid = 0;
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
match[i] = 0;
for (int j = 1; j <= n; j++) {
int x;
cin >> x;
if (x == 1) {
addEdge(i, j, 1);
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
memset(vis, 0, sizeof vis);
if (dfs(i)) {
ans++;
}
}
if (ans == n) {
puts("Yes");
}else puts("No");
}
return 0;
}
这道题第一眼看起来和图论没有任何关系,像是一道dp题,但是这道题的 ,数据范围偏大,状压dp显然不适用。
现在我们来看一下如何将这道题转化为二分图匹配问题。
由于骨牌是 的,如果我们把每个相邻的点之间连上一条边,那么很显然,最多的骨牌个数即我们能取到的最多的没有公共点的边的个数,即最多的匹配。所以现在,问题转化成了一个找最大匹配的问题。
我们可以再进一步,将该问题转化为二分图匹配问题。
我们考虑每个点的横纵坐标之和,如果这个和值为偶数,将该点分到一边,为奇数则分到另一边。(其实就是将相邻的点染上不同的颜色)
显然,偶数点内部无法构成一张骨牌,奇数点内部也无法构成一张骨牌,但偶数点和奇数点之间可以构成骨牌。
再按照上面所说的,我们将能构成骨牌的点之间连上一条边,可以发现所有的边都在偶数点和奇数点之间,而奇偶点内部无边,至此,该图已转化为一张二分图,而该二分图的最大匹配即为最多骨牌数量。
另外,由于最多有一万个点,如果用邻接矩阵或其他来存储边会造成空间上的冗余,但我们发现每两个点之间有边的情况只可能是两点相邻时,所以此题不需要存边,对于每个点我们找他上下左右的四个点即可。
//AcWing372 棋盘覆盖
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 20005;
int n, t;
int a[N];
int dx[4] = {-1, 0, 0 ,1};
int dy[4] = {0, -1, 1, 0};
int nl, nr, m;
int matchx[N], matchy[N], visx[N], visy[N], pre[N];
queue<int> q;
int cl[N];
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
int x = (u-1) / n + 1, y = (u - 1) % n + 1;
for (int k = 0; k < 4; k++) {
int tx = x + dx[k], ty = y + dy[k];
if (tx < 1 || tx > n || ty < 1 || ty > n) continue;
int v = (tx - 1) * n + ty;
if (a[v] == -1) continue;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return true;
} else q.push(matchy[v]);
}
}
}
return false;
}
int main () {
cin >> n >> t;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
int now = i + j;
int id1 = (i - 1) * n + j;
if (now % 2 == 1) cl[id1] = 1;
else cl[id1] = 2;
}
}
for (int i = 1; i <= t; i++) {
int x, y;
cin >> x >> y;
int u = (x - 1) * n + y;
a[u] = -1;
}
int ans = 0;
for (int i = 1; i <= n * n; i++) {
if (a[i] != -1 && cl[i] == 1 && bfs(i)) {
ans++;
}
}
cout << ans << endl;
return 0;
}
所以对于图论问题,解决问题的关键,在于如何将一个问题转化为一个图论问题。
该题上面那道矩阵游戏的升级版也是一个求最大匹配的问题。
如果我们不考虑题中硬石头的存在,那么这道题就变成了 :“在一张 n * m 的网格地图中放置炸弹,炸弹可以破坏一整行一整列,有些位置不允许放置炸弹,要求炸弹不能互相攻击,求最多能放置多少炸弹?”
是不是很眼熟?这就变成了上面那道矩阵游戏了。
然而这道题加入了硬石子,硬石子会将一行(一列)中的攻击隔开,导致一行(一列)能够放置多个炸弹。
而由于这一行中被硬石子隔开的几部分互不影响,很显然,我们可以将一行视为多行,我们可以从行号方面来
观察这个现象。
例如:
*x#*
我们给他标记行号,原本是这样的:
1x#1
但由于中间被硬石子隔开,我们可以将一行看作是两行:
1x#2
列号也用相同的方法处理,处理完后,会发现这就是一个简单的求二分图最大匹配的问题。
#include<bits/stdc++.h>
#include<queue>
using namespace std;
const int N =105;
const int M =3005;
int n, m;
char c[N][N];
int h[M], l[M];
int a[M][M];
int matchx[M], matchy[M], visx[M], visy[M], pre[M];
queue<int> q;
int cntx, cnty;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
for (int v = 1; v <= cnty; v++) {
if (a[u][v] == 0) continue;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return true;
} else q.push(matchy[v]);
}
}
}
return false;
}
int main () {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> c[i][j];
}
}
for (int i = 1; i <= n; i++) {
cntx++;
for (int j = 1 ; j <= m; j++) {
if (c[i][j] == '#') {
cntx++;
continue;
}
if (c[i][j] == 'x') continue;
int id = (i - 1) * m + j;
h[id] = cntx;
}
}
for (int i = 1; i <= m; i++) {
cnty++;
for (int j = 1; j <= n; j++) {
if (c[j][i] == '#') {
cnty++;
continue;
}
if (c[j][i] == 'x') continue;
int id = (j - 1) * m + i;
l[id] = cnty;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int id = (i - 1) * m + j;
a[h[id]][l[id]] = 1;
}
}
int ans = 0;
for (int i = 1; i <= cntx; i++) {
if (bfs(i)) ans++;
}
//打印行号,列号,以及最终匹配结果。
// for (int i = 1; i <= n; i++) {
// for (int j = 1; j <= m; j++) {
// int id = (i - 1) * m + j;
// cout << h[id];
// }
// cout << endl;
// }
// cout << endl;
//
// for (int i = 1; i <= n; i++) {
// for (int j = 1; j <= m; j++) {
// int id = (i - 1) * m + j;
// cout << l[id];
// }
// cout << endl;
// }
//
// for (int i = 1; i <= cntx; i++) {
// if (matchx[i]) cout << i << ": " << matchx[i] << endl;
// }
cout << ans << endl;
return 0;
}
匈牙利算法解决最大团问题
首先明确朋友圈的定义 : 一个朋友圈中,每两个人都互相满足朋友关系。现在我们将朋友关系看作是两个人之间连上了一条边。
然后单独考虑 A 国 :我们发现 a 国 中的两人要满足朋友关系,需要两人的友善值满足 (x xor y) % 2 = 1,即
两人的友善值需要为一奇一偶,也就是说在 A 国中我们最多能取 2 人。那么对于 A 国,我们只要暴力地查询两人的朋友关系即可,时间复杂度为
然后我们再看 B 国 :
B 国的第一个互为朋友的条件为 (x xor y) mod 2 = 0 ,也就是说只要两个人的奇偶性相同,两人就互为朋友。
B 国的另一个条件 (a or b) 的二进制中有奇数个 1。
现在我们将 B 国内部的人按友善值的奇偶性分成两类,可以发现构成的图是一个两类点集内部互相有边相连,点集之间有边相连的一个图,我们将此图取反(建补图),即可得到一个二分图。
所以 B 国内部的最大朋友圈就是 B 国原图的最大点覆盖,而显然,最大点覆盖 = 原图点数 - 补图最大匹配。
所以此时对于 B 国,我们找到了他的最大朋友圈,也就是 A 国取 0 人时的最大朋友圈。
现在考虑 A 国取 1 人和 2 人时的最大朋友圈:
由于 A 国方案较少,我们直接对 A 暴力 ,然后查找 B 中与已选中的 A 国人有关系的人,在这些有关系的人中寻找最大点覆盖,最后得到最大朋友圈。
此外,这道题中,我们可以利用时间戳的特性来判断 B 国中的点是否与当前选中的点 A 有关,事实上,在图论问题与算法中,有着大量的对时间戳的运用,例如图的连通性算法等等。
下面程序中的时间戳即为 ok 数组。
#include<bits/stdc++.h>
#include<queue>
using namespace std;
typedef long long ll;
const int N =3005;
typedef long long ll;
ll a[N], b[N];
int eb[N][N];
int eab[N][N];
int nl, nr;
int ok[N];
int id;
int l[N], r[N];
int matchx[N], matchy[N], visx[N], visy[N], pre[N];
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int i = 1; i <= nr; i++) {
int v = r[i];
if (eb[u][v] && !visy[v] && ok[v] == id) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return 1;
} else q.push(matchy[v]);
}
}
}
return false;
}
void init() {
nl = 0;
nr = 0;
id = 0;
for (int i = 1; i <= 3000; i++) {
matchx[i] = matchy[i] = 0;
l[i] = 0;
r[i] = 0;
ok[i] = 0;
for (int j = 1; j <= 3000; j++) {
eb[i][j] = 0;
eab[i][j] = 0;
}
}
}
int main () {
int T;
cin >> T;
while (T--) {
init();
int na, nb, m;
cin >> na >> nb >> m;
for (int i = 1; i <= na; i++) {
cin >> a[i];
}
for (int i = 1; i <= nb; i++) {
cin >> b[i];
}
for (int i = 1; i <= nb; i++) {
if (b[i] & 1) {
nl++;
l[nl] = i;
for (int j = 1; j <= nb; j++) { // i 和 i 不成边, 但每个点建边独立,所以从 1 开始循环
// cout << i << ": " << b[i] << ":::" << j << ": " << b[j] << endl;
if ((b[j] & 1) == 0) {
// cout << ":::" << i << " " << j << " " << b[i] << " " << b[j] << endl;
if ((b[i] ^ b[j]) % 2 != 0) {
eb[i][j] = 1;
}
ll now = b[i] | b[j];
int cnt = 0;
while(now) {
if (now & 1) cnt++;
now >>= 1;
}
if (now % 2 == 0) eb[i][j] = 1;
}
// cout << i << " " << j << ": " << eb[i][j] << endl;
}
} else nr++, r[nr] = i;
}
int ans = 0;
// 只有b国
for (int i = 1; i <= nl; i++) {
// cout << i << ": " << l[i] << endl;
if (bfs(l[i])) ans++;
}
ans = nb - ans;
// cout << "::::" << ans << endl;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
eab[x][y] = 1;
}
// A国选一人
for (int i =1; i <= na; i++) {
int now = 0;
int sum = 0;
id++;
for (int j = 1; j <= nb; j++) {
if (eab[i][j]) sum++;
ok[j] = id;
matchx[j] = matchy[j] = 0;
}
for (int j = 1; j <= nl; j++) {
if (eab[i][l[j]] && ok[j] == id) {
if (bfs(l[j])) now++;
}
}
ans = max(ans, sum - now + 1);
}
// cout << ":::::" << ans << endl;
// A国选两人
for (int i = 1; i <= na; i++) {
for (int j = i + 1; j <= na; j++) {
int now = 0;
int sum = 0;
id ++;
if ((a[i] ^ a[j]) % 2 == 1) {
for (int i = 1; i <= nb; i++) matchx[i] = matchy[i] = 0;
for (int k = 1; k <= nb; k ++) {
if (eab[i][k] && eab[j][k]) sum++, ok[k] = id;
if (b[k] & 1 && ok[k] == id) {
if (bfs(k)) now++;
}
}
}
ans = max(ans, sum - now + 2);
}
}
cout << ans << endl;
}
return 0;
}
匈牙利算法解决最小点覆盖问题
首先根据题目给出的两个机器以及 K 任务, 我们可以猜测该题是一道二分图问题,按照这个猜测,现在我们要确定原题如何构成一个二分图问题。
很简单,我们大胆连边。
如果我们对于每个任务给出的两个值 a[i] 和 b[i] 都连上一条边,这代表着一个任务。
当所有任务的两个值的关系都处理好后,即使目前意义还不是很明确,但我们已经构造出了一张二分图,这张图的左部点是出现过的 a[i] ,右部点是出现过的 b[i],而每条边就是一个任务。
我们要将所有任务做完,很明显,就是将图中的每个边都取到。而每选中一个点,相当于我们转换了一次机器的状态,会导致最终答案加 1 。也就是所,如果我们用最少的点覆盖所有边,那么这些点就是我们的答案,也就是所谓的最小点覆盖,至此,问题已转化为二分图问题。
最后,最小点覆盖数 = 最大匹配数,跑一遍匈牙利即可。
//AcWing376 机器任务,最小点覆盖问题。
#include<bits/stdc++.h>
#include<queue>
using namespace std;
const int N = 205;
int a[N][N];
int nl, nr;
int matchx[N], matchy[N], visx[N], visy[N], pre[N];
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0 ,sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
for (int v = 1; v < nr; v++) {
if (a[u][v] == 0) continue;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return 1;
} else q.push(matchy[v]);
}
}
}
return false;
}
void init() {
memset(a, 0 , sizeof a);
memset(matchx, 0, sizeof matchx);
memset(matchy, 0 ,sizeof matchy);
}
int main() {
int n, m, k;
while(cin >> n) {
init();
if (n == 0) break;
cin >> m >> k;
nl = n,
nr = m;
for (int i = 1; i <= k; i++) {
int id, x, y;
cin >> id >> x >> y;
if (x == 0 || y == 0) continue;
a[x][y] = 1;
}
int ans = 0;
for (int i = 1; i < nl; i++) {
if (bfs(i)) ans++;
}
cout << ans << endl;
}
return 0;
}
匈牙利算法解决最大独立集问题
题目 骑士放置
该题是一道非常经典的求最大独立集的问题。
我们将两个能互相攻击的骑士中间连上边,那么显然,最多的不能互相攻击的骑士的个数即为最多的没有边相连的骑士数量,也就是在图中找出最大独立集。
然后,我们就单个格子考虑,可以发现每个格子的对角的两个点无法互相攻击,显然,每一个格子的相同方向对角的点都无法互相攻击,至此,我们根据两个不同方向的对角,可以将一个格子的四个点分成两部分,以至于所有格子的点都能分成两部分,也就是说,我们能够将所有点分成两部分,而每一部分之中不存在互相攻击关系,也就形成了二分图。
总而言之,就是根据对角关系对点染色即可。
最后在求二分图的最大匹配,最大独立集 = 总点数 - 最大匹配 - 禁止放置数量。
//AcWing378 骑士放置 最大独立集
#include<bits/stdc++.h>
#include<queue>
using namespace std;
const int N =105;
const int M = 10005;
bool bn[N][N];
int a[M][8];
int cl[M];
int n, m, np;
int dx[8] = {-2, -2, 2, 2, -1, 1, -1, 1};
int dy[8] = {-1, 1, -1, 1, -2, -2, 2, 2};
int matchx[M], matchy[M], pre[M], visx[M], visy[M];
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()){
int u =q.front();
q.pop();
visx[u] = 1;
int x = (u-1) / m + 1, y = (u - 1) % m + 1;
for (int k = 0; k < 8; k++) {
int tx = x + dx[k], ty = y + dy[k];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (bn[tx][ty]) continue;
if (a[u][k] == 0) continue;
int v = (tx - 1) * m + ty;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return true;
}else q.push(matchy[v]);
}
}
}
return false;
}
int main () {
cin >> n >> m >> np;
for (int i = 1; i <= np; i++) {
int x, y;
cin >> x >> y;
bn[x][y] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int now = i + j, id = (i - 1) * m + j;
if (now % 2 == 0) cl[id] = 1;
else cl[id] = 2;
for (int k = 0; k < 8; k++) {
int tx = i + dx[k], ty = j + dy[k], tid = (tx - 1) * m + ty;
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
if (bn[tx][ty]) continue;
a[id][k] = 1;
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int id = (i - 1) * m + j;
if (bn[i][j] == 0 && cl[id] == 1 && bfs(id)) {
ans++;
}
}
}
cout << (n * m - ans - np) <<endl;
return 0;
}
一道很显然的求最大独立集问题。
由于给出的线段中,横的与横的,竖的与竖的之间保证没有交点,所以我们可以将横的与竖的边分别看作左部点与右部点,将相交看作是两部点之间存在一条边,至此,二分图以构建完成。
只要跑一遍匈牙利,求出二分图的最大独立集即可。
#include<bits/stdc++.h>
#include<queue>
using namespace std;
const int N = 300;
int n;
struct Node{
int x1, y1, x2, y2, id, ch;
}d[N];
int cntx, cnty;
int px[N], py[N];
int a[N][N];
int matchx[N], matchy[N], pre[N], visx[N], visy[N];
queue<int> q;
void aug(int v) {
while(v){
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
for (int i = 1; i <= cnty; i++) {
int v= py[i];
if (a[u][v] == 0) continue;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return true;
}else q.push(matchy[v]);
}
}
}
return false;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i].x1 >> d[i].y1 >> d[i].x2 >> d[i].y2;
if (d[i].x1 == d[i].x2) {
d[i].ch = 1;
d[i].id = ++cntx;
px[cntx] = i;
if (d[i].y1 > d[i].y2) swap(d[i].y1,d[i].y2);
} else {
d[i].id = ++cnty;
d[i].ch = 2;
py[cnty] = i;
if (d[i].x1 > d[i].x2) swap(d[i].x1,d[i].x2);
}
}
int nans = max(cntx, cnty);
int ans = 0;
for (int i = 1; i <= cntx; i++) {
for (int j = 1; j <= cnty; j++) {
int u = px[i], v = py[j];
if (d[u].x1 >= d[v].x1 && d[u].x1 <= d[v].x2) {
if(d[v].y1 >= d[u].y1 && d[v].y1 <= d[u].y2) {
a[u][v] = 1;
}
}
}
}
for (int i = 1; i <= cntx; i++) {
if (bfs(px[i])) ans++;
}
nans = max(nans,n - ans);
// cout << cntx << " " << cnty << " " << ans << " " << n - ans << endl;
cout << nans << endl;
return 0;
}
匈牙利算法解决最小路径点覆盖问题
AcWing379 捉迷藏
首先,题目要求我们在一个DAG中找出最多的不连通的点。
我们反向思考,如果我们能在个DAG中用最少的路径覆盖最多的点,那么在这些路径上取走一个点,这些点就是DAG中最多的不连通的点。
也就是说,我们对这个DAG,求出他的最小路径点覆盖数即可。
而为了解决这个最小路径点覆盖问题,我们可以采用匈牙利算法,从二分图的角度考虑。
我们将每个点拆分成两个点,分为一个出点和入点,这是图论问题中比较常用的一种方法,名为拆点。
出点与出点之间,入点与入点之间都不存在边,我们将点之间的路径全部置于出点与入点之间,并且规定路径是有向的,只会从出点指向入点,这样,一个DAG就被我们转化成了一个二分图。
而对于这张二分图,我们可以假设,如果 A 能走到 B ,那么就会有一个出点 A 指向一个入点 B 的路径。
此外,对于这道题,如果 A 能走到 B, B 能走到 C, 那么 A 也能走到 C(值得注意的是,C 不一定能走到 A)。
也就是说,我们在建二分图时,还有对二分图跑一次传递闭包。
传递闭包可以理解为是一种传递关系,例如在一个集合中, A 是 B 的父亲, B 是 C 的父亲,那么 A 就是 C 的祖先。
显然,传递闭包可以用 Floyd 算法解决。
最终答案就是该图的最小路径点覆盖。
//AcWing379 捉迷藏 最小路径点覆盖例题
#include<bits/stdc++.h>
using namespace std;
const int N = 205;
const int INF = 0x3f3f3f3f;
int a[N][N];
int b[N][N];
int matchx[N], matchy[N], visx[N], visy[N], pre[N];
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
int n, m;
bool bfs(int s) {
memset(visx, 0, sizeof visx);
memset(visy, 0 ,sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int v = 1; v <= n; v++) {
if (!b[u][v]) continue;
if (!visy[v]) {
visy[v] = 1;
pre[v] = u;
if (!matchy[v]) {
aug(v);
return true;
} else q.push(matchy[v]);
}
}
}
return false;
}
int main () {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
b[x][y] = 1;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
b[i][j] |= b[i][k] & b[k][j];
}
}
}//传递闭包
int ans = 0;
for (int i = 1; i <= n; i++) {
if (bfs(i)) ans++;
}
// for (int i = 1; i <= n; i++) {
// cout << i << ": " << matchx[i] << endl;
// }
// cout << "::::" << ans << endl;
cout << n - ans << endl;
return 0;
}
KM算法
KM算法主要用于带权值的二分图匹配问题,通常用来找总权值最大或最小的完美匹配。
//DFS实现版本,时间复杂度比较大,后续更新BFS实现版本
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 105;
const int M =4005;
const ll INF = 0X7F7F7F7F;
struct Edge{
int v, w, next;
}e[M<<1];
int head[N], eid;
void addEdge(int u, int v, int w) {
e[eid] = {v, w, head[u]};
head[u] = eid++;
}
ll a[N][N];
int match[N];
bool visa[N], visb[N];
ll la[N], lb[N];
int n;
ll delta = INF;
bool dfs(int x) {
visa[x] = 1;
for (int i = head[x]; ~i; i = e[i].next) {
int v = e[i].v;
if (!visb[v]) {
if (la[x] + lb[v] - a[x][v] == 0) {
visb[v] = 1;
if (!match[v] || dfs(match[v])) {
match[v] = x;
return true;
}
}else delta = min (delta, la[x] + lb[v] - a[x][v]);
}
}
return false;
}
ll km() {
for (int i = 1; i <= n; i++) {
la[i] = -INF;
lb[i] = 0;
for (int j = head[i]; ~j; j = e[j].next) {
int v = e[j].v;
la[i] = max(la[i], a[i][v]);
}
}
for (int i = 1; i <= n; i++) {
while (1) {
memset(visa, 0, sizeof visa);
memset(visb, 0 ,sizeof visb);
delta = INF;
if (dfs(i)) break;
for (int j = 1; j <= n; j++) {
if (visa[j]) la[j] -= delta;
if (visb[j]) lb[j] += delta;
}
}
}// 我们最多会在每一个右部点上都有一次找不到匹配的结果,这样最多会有n次修改delta的操作。
//也就是说,km + dfs的时间复杂度会被卡到n的四次方。
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans += a[match[i]][i];
}
return ans;
}
int main () {
memset(head, -1,sizeof head);
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
ll x;
cin >> x;
a[j][i] *= x;
addEdge(j, i, a[j][i]);
}
}
cout << km() << endl;
return 0;
}
从上述代码可以发现 : 每次用dfs增广,时间复杂度为 ,而每次扩大相等子图只能加入一条边,最多会有 条边,也就是说,km + dfs 的时间复杂度会被卡到 。
解决这个问题的方法也很简单,我们只要换用bfs来实现,在每次扩大完子图后,在 bfs 外将所有可增广点入队,这样我们就将 n 次 增广操作与 bfs 分开了,时间复杂度也被降到了 。
//km + bfs 实现
//例题 UOJ#80
#include<bits/stdc++.h>
#include<queue>
using namespace std;
typedef long long ll;
const int N =505;
const int M = 160006;
const ll INF = 1e18+7;
int nl, nr ,m;
int matchx[N],matchy[N];
ll lx[N],ly[N];
ll a[N][N], slack[N], d;
int pre[N];
bool visx[N], visy[N];
queue<int> q;
void aug(int v) {
while (v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
void bfs(int s) {
for (int i =1; i <= nr; i++) {
slack[i] = INF;
}
memset(visx, 0 ,sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while (1) {
while (!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int v = 1; v <= nr; v++) {
if (!visy[v]) {
if (lx[u] + ly[v] - a[u][v] < slack[v]) {
slack[v] = lx[u] + ly[v] - a[u][v];
pre[v] = u;
if (!slack[v]) { // 顶标之和大于边权时可加入
visy[v] = 1;
if (!matchy[v]) {
aug(v);
return;
} else q.push(matchy[v]);
}
}
}
}
}
d = INF;
for (int i = 1; i<= nr; i++) {
if (!visy[i]) d = min(d, slack[i]);
}
if (d == INF) break;
for (int i = 1; i<= nl; i++) if (visx[i]) lx[i] -= d;
for (int i = 1; i<= nr; i++) if (visy[i]) ly[i] += d;
else slack[i] -= d;
// slack[v] -= d ,因为当右部点v没有在路径上时顶标不变,而与之相连的左部点顶标减少
//所以 lx + ly 的值减小了 d
//将可增广点加入队列中
for (int i = 1; i <= nr; i++) {
if (!visy[i] && !slack[i]) {
visy[i] = 1;
if (!matchy[i]) {
aug(i);
return;
} else q.push(matchy[i]);
}
}
}
}
void km() {
for (int u = 1; u <= nl; u++) {
for (int v = 1; v <= nr; v++) {
lx[u] = max(lx[u], a[u][v]);
}
}
for (int i = 1; i <= nl; i++) {
bfs(i);
}
}
int main() {
cin >> nl >> nr >> m;
if (nr < nl) nr = nl; //右部点一定大于等于左部点,不满足则创建虚点。
// for (int i = 1; i <= nl; i++) {
// for (int j = 1; j <= nr; j++) {
// a[i][j] = -INF;
// }
// }
//如果优先满足最大匹配再满足权值最大,则给边赋值负无穷
//如果优先满足权值最大,则给边赋值为0
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
cin >> a[u][v];
}
ll ans = 0;
km();
for (int i = 1; i <= nl; i++) ans += lx[i];
for (int i = 1; i <= nr; i++) ans += ly[i];
//总权值和为顶标之和
cout << ans << endl;
for (int i = 1; i <= nl; i++) {
if (a[i][matchx[i]]) cout << matchx[i] << " "; //边权<=0时,该边为虚边
else cout << 0 << " ";
}
cout << endl;
return 0;
}
下面是优先满足完美匹配的二分图最大权完美匹配问题。
//洛谷P6577 模板题 二分图最大权完美匹配
//与上一题不同,本题要求优先满足完美匹配,再满足权值最大。
#include<bits/stdc++.h>
#include<queue>
using namespace std;
typedef long long ll;
const ll INF = 1e12;
const int N = 1005;
int n, m;
int nl, nr;
ll a[N][N];
int matchx[N], matchy[N], pre[N];
ll lx[N], ly[N];
ll slack[N], d;
bool visx[N], visy[N];
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
void bfs(int s) {
for (int i = 1; i <= nr; i++) {
slack[i] = INF;
}
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(1) {
while (!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int v = 1; v <= nr; v++) {
if (!visy[v]) {
if (lx[u] + ly[v] - a[u][v] < slack[v]) {
slack[v] = lx[u] + ly[v] - a[u][v];
pre[v] = u;
}
if (!slack[v]) {
visy[v] = 1;
if (!matchy[v]) {
aug(v);
return;
} else q.push(matchy[v]);
}
}
}
}
d = INF;
for (int i = 1; i <= nr; i++) {
if (!visy[i]) d = min(d, slack[i]);
}
if (d == INF) break;
for (int i = 1; i <= nl; i++) if (visx[i]) lx[i] -= d;
for (int i = 1; i <= nr; i++) if (visy[i]) ly[i] += d;
else slack[i] -= d;
for (int i = 1; i <= nr; i++) {
if (!visy[i] && !slack[i]) {
visy[i] = 1;
if (!matchy[i]) {
aug(i);
return;
} else q.push(matchy[i]);
}
}
}
}
void km() {
for (int u = 1; u <= nl; u++) {
for (int v = 1; v <= nr; v++) {
lx[u] = max(lx[u], a[u][v]);
}
}
for (int i = 1; i <= nl; i++) {
bfs(i);
}
}
int main () {
freopen("P6577_11.in","r",stdin);
cin >> n >> m;
nl = nr = n;
for (int i = 1; i <= n<<1; i++) {
lx[i] = -INF;
for (int j = 1; j <= n<<1; j++) {
a[i][j] = -INF;
}
}
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
cin >> a[x][y];
}
km();
ll ans = 0;
for (int i = 1; i <= nl; i++) {
ans += lx[i];
// cout << "L: " << i << " " << lx[i] << endl;
}
for (int i = 1; i <= nr; i++) {
ans += ly[i];
// cout << "R: " << i << " " << ly[i] << endl;
}
cout << ans << endl;
for (int i = 1; i <= nr; i++) {
if (a[matchy[i]][i] != -INF) cout << matchy[i] << " ";
}
cout << endl;
return 0;
}
模板题,跑一遍二分图最大权匹配和二分图最小权匹配得到答案。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N =105;
const ll INF = 1e18 + 7;
ll a[N][N];
ll lx[N], ly[N];
int matchx[N],matchy[N];
ll slack[N], d;
int pre[N];
bool visx[N], visy[N];
int n;
int nl, nr;
queue<int> q;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
void bfs(int s) {
for (int i = 1; i <= nr; i++) {
slack[i] = INF;
}
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(1) {
while (!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int v = 1; v <= nr; v++) {
if (!visy[v]) {
if (lx[u] + ly[v] - a[u][v] < slack[v]) {
slack[v] = lx[u] + ly[v] - a[u][v];
pre[v] = u;
if (!slack[v]) {
visy[v] = 1;
if (!matchy[v]) {
aug(v);
return;
} else q.push(matchy[v]);
}
}
}
}
}
d = INF;
for (int i = 1; i <= nr; i++) {
if (!visy[i]) d = min(d, slack[i]);
}
if (d == INF) break;
for (int i = 1; i <= nl; i++) if (visx[i]) lx[i] -= d;
for (int i = 1; i <= nr; i++) if (visy[i]) ly[i] += d;
else slack[i] -= d;
for (int i = 1; i <= nr; i++) {
if (!visy[i] && !slack[i]) {
visy[i] = 1;
if (!matchy[i]) {
aug(i);
return;
}else q.push(matchy[i]);
}
}
}
}
void init() {
for (int i = 0; i <= 100; i++) {
matchy[i] = matchx[i] = 0;
lx[i] = 0;
ly[i] = 0;
}
}
void km() {
init();
for (int u = 1; u <= nl; u++) {
for (int v = 1; v <= nr; v++) {
lx[u] = max(lx[u],a[u][v]);
}
}
for (int i = 1; i <= nl; i++) {
bfs(i);
}
}
int main() {
cin >> n;
nl = n;
nr = n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
a[i][j]*=-1;
}
}
ll ans = 0;
km();
for (int i = 1; i <= nl; i++) ans += lx[i];
for (int i = 1; i <= nr; i++) ans += ly[i];
cout << abs(ans) << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
a[i][j]*=-1;
}
}
ans = 0;
km();
for (int i = 1; i <= nl; i++) ans += lx[i];
for (int i = 1; i <= nr; i++) ans += ly[i];
cout << abs(ans) << endl;
return 0;
}
下面是一道安徽大学校赛题的二分图最小权匹配。
显然,对于寻找最小权,我们只要将所有边权取负,再找最大权,此时的最大权的绝对值就是原图的二分图最小权。
此外,根据本题题意,所有棋子都要落到边上,可转化为二分图匹配问题,所有棋子对应左部点,而所有边上的格子对应为右部点,最终棋盘整理好即为所有棋子都要有对应的格子,即一个完美匹配,所以本题是一道二分图最小权值完美匹配问题。
不过本题转化为二分图匹配问题有一个前提:在考虑棋子移动时,我们无需考虑格子上已有的棋子,也就是说我们的棋子在移动时可穿过其他棋子,因为其他棋子也可发生移动。也就是说,我们甚至无需考虑棋子的移动,只需要考虑棋子与有效格子的匹配,这样原问题就变成了一个二分图匹配问题。
当格子数量小于棋子数量时,可特判为无解,这样场上的棋子与格子数最多不会超过,对于用 bfs 实现的 km 算法,时间复杂度为 ,其中M为格子数。
本题的另一特点是要处理好边权,而边权也很明显,即为每个棋子到格子的曼哈顿距离。
//牛客,整理棋盘,二分图
#include<bits/stdc++.h>
#include<queue>
using namespace std;
typedef long long ll;
const int N = 405;
const int M = 805;
const int INF = 0x3f3f3f3f;
int n , m;
char c[N][N];
int a[M][M];
int posx[M], posy[M], pre[M];
int lx[M], ly[M], matchx[M], matchy[M];
int slack[M], d;
bool visx[M], visy[M];
queue<int> q;
int nl, nr;
void aug(int v) {
while(v) {
int t = matchx[pre[v]];
matchx[pre[v]] = v;
matchy[v] = pre[v];
v = t;
}
}
void bfs(int s) {
for (int i = 1; i <= nr; i++) slack[i] = INF;
memset(visx, 0, sizeof visx);
memset(visy, 0, sizeof visy);
memset(pre, 0, sizeof pre);
while(!q.empty()) q.pop();
q.push(s);
while(1) {
while(!q.empty()) {
int u = q.front();
q.pop();
visx[u] = 1;
for (int v = 1; v <= nr; v++) {
if (!visy[v]) {
if (lx[u] + ly[v] - a[u][v] < slack[v]) {
slack[v] = lx[u] + ly[v] - a[u][v];
pre[v] = u;
}
if (!slack[v]) {
visy[v] = 1;
if (!matchy[v]) {
aug(v);
return;
}else q.push(matchy[v]);
}
}
}
}
d = INF;
for (int i = 1; i <= nr; i++) {
if (!visy[i]) d = min(d, slack[i]);
}
if (d == INF) break;
for (int i = 1; i <= nl; i++) if (visx[i]) lx[i] -= d;
for (int i = 1; i <= nr; i++) if (visy[i]) ly[i] += d;
else slack[i] -= d;
for (int i = 1 ; i <= nr; i++) {
if (!visy[i] && !slack[i]) {
visy[i] = 1;
if (!matchy[i]) {
aug(i);
return;
}else q.push(matchy[i]);
}
}
}
}
void km() {
for (int i = 1; i <= nl; i++) {
for (int j = 1; j <= nr; j++) {
lx[i] = max(lx[i], a[i][j]);
}
}
for (int i = 1; i <= nl; i++) {
bfs(i);
}
}
void init() {
for (int i = 0 ; i <= 800; i++) {
posx[i] = -1;
posy[i] = -1;
matchy[i] = matchx[i] = 0;
lx[i] = 0;
ly[i] = 0;
}
}
int main() {
int T;
cin >> T;
while (T--) {
init();
cin >> n >> m;
int sp = n * 4 - 4;
nl = m;
nr = sp;
int cnt = 1;
for (int i = 1 ; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> c[i][j];
if (c[i][j] == '#') {
posx[cnt++] = (i-1) * n + j; // 记录棋子位置
}
a[i][j] = -INF;
}
}
if (m > sp) {
puts("-1");
continue;
}
int cnty = 0;
for (int i = 1; i <= n; i++) {
posy[++cnty] = i;
}
for (int i = 2; i <= n-1; i++) {
posy[++cnty] = (i - 1) * n + 1;
posy[++cnty] = (i - 1) * n + n;
}
for (int i = 1; i <= n; i++) {
posy[++cnty] = (n - 1) * n + i;
}// 记录有效格子位置
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= cnty; j++) {
int x1 = (posx[i] - 1) / n + 1, x2 = (posy[j] - 1) / n + 1;
int y1 = (posx[i] - 1) % n + 1, y2 = (posy[j] - 1) % n + 1;
int w = abs(x1 - x2) + abs(y1 - y2);//处理边权
// cout << x1 << " " << y1 << " , " << x2 << " " << y2 << ": " << w << endl;
a[i][j] = -1 * w;//边权取负
}
}
km();
int ans = 0;
for (int i = 1; i <= nl; i++) ans += lx[i];
for (int i = 1; i <= nr; i++) ans += ly[i];
cout << abs(ans) << endl;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)