二分图
匈牙利算法
下面展示的是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;
}
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增广,时间复杂度为 \(n ^ 2\),而每次扩大相等子图只能加入一条边,最多会有 \(n^2\) 条边,也就是说,km + dfs 的时间复杂度会被卡到 \(n ^ 4\) 。
解决这个问题的方法也很简单,我们只要换用bfs来实现,在每次扩大完子图后,在 bfs 外将所有可增广点入队,这样我们就将 n 次 增广操作与 bfs 分开了,时间复杂度也被降到了 \(n^3\) 。
//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;
}
下面是一道安徽大学校赛题的二分图最小权匹配。
显然,对于寻找最小权,我们只要将所有边权取负,再找对大权,此时的最大权的绝对值就是原图的二分图最小权。
此外,根据本题题意,所有棋子都要落到边上,可转化为二分图匹配问题,所有棋子对应左部点,而所有边上的格子对应为右部点,最终棋盘整理好即为所有棋子都要有对应的格子,即一个完美匹配,所以本题是一道二分图最小权值完美匹配问题。
不过本题转化为二分图匹配问题有一个前提:在考虑棋子移动时,我们无需考虑格子上已有的棋子,也就是说我们的棋子在移动时可穿过其他棋子,因为其他棋子也可发生移动。也就是说,我们甚至无需考虑棋子的移动,只需要考虑棋子与有效格子的匹配,这样原问题就变成了一个二分图匹配问题。
当格子数量小于棋子数量时,可特判为无解,这样场上的棋子与格子数最多不会超过\(800\),对于用 bfs 实现的 km 算法,时间复杂度为 \(O(m^3)\) ,其中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;
}
标签:二分,pre,const,记录,int,ll,long,匹配
From: https://www.cnblogs.com/you-mu-jv-ruo/p/17347500.html