二分图及二分图的最大匹配详解
二分图及其最大匹配
概念
二分图定义:二分图,就是顶点集V可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。如下图就是一个二分图:
注意:二分图不一定是连通图。
判定定理:一个图是二分图当且仅当没有长度为奇数的圈。
染色法判定二分图
从上面的判定定理可知, 我们可以通过染色法判断有没有奇圈。模板题点我,代码如下:
//DFS求解
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
struct edge{
int nex, to;
}Edge[maxn<<1];
int n, m, head[maxn], co[maxn], tot;
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
bool dfs(int u, int x){
co[u] = x;
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(co[v] == 0){
if(!dfs(v, 3 - co[u])) return 0;
}
else if(co[v] == co[u]) return 0;
else if(co[v] + co[u] == 3) continue;
}
return 1;
}
int main()
{
scanf("%d %d", &n, &m);
memset(head, -1, sizeof(head));
for(int i = 1; i <= m; ++i){
int a, b;
scanf("%d %d", &a, &b);
add(a, b); add(b, a);
}
int ok = 1;
for(int i = 1; i <= n; ++i){
if(co[i]) continue;
if(dfs(i, 1)) continue;
ok = 0;
break;
}
ok ? puts("Yes") : puts("No");
system("pause");
}
//BFS求解
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
queue<int> q;
struct edge{
int nex, to;
}Edge[maxn<<1];
int n, m, head[maxn], co[maxn], tot;
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
bool bfs(int x){
co[x] = 1;
q.push(x);
while(!q.empty()){
int u = q.front(); q.pop();
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(co[v] == co[u]) return 0;
else if(co[v] == 0){
co[v] = 3 - co[u];
q.push(v);
}
else if(co[u] + co[v] == 3) continue;
}
}
return 1;
}
int main()
{
scanf("%d %d", &n, &m);
memset(head, -1, sizeof(head));
for(int i = 1; i <= m; ++i){
int a, b;
scanf("%d %d", &a, &b);
add(a, b); add(b, a);
}
int ok = 1;
for(int i = 1; i <= n; ++i){
if(co[i]) continue;
if(bfs(i)) continue;
ok = 0;
break;
}
ok ? puts("Yes") : puts("No");
system("pause");
}
二分图最大匹配
基本概念:
匹配:“任意两条边都没有公共端点”的边的集合称为匹配。
最大匹配:包含边的个数最多的一组匹配。
增广路:一个连接两个非匹配点的路径path, 使得非匹配边和匹配边在path上交替出现(第一条边和最后一条边都是未匹配边),称path是一个增广路。
求解二分图的最大匹配问题可用匈牙利算法(增广路算法)。下面给出模板代码:
模板题
【题意】:求解最大匹配的边数。时间复杂度为\(O(nm)\)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
struct edge{
int nex, to;
}Edge[maxn * maxn];
int n, m, e;
int head[maxn], tot;
int match[maxn], vis[maxn], ans;
//vis表示每次搜索有没有被访问到, match表示点是不是匹配点
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
bool dfs(int u)
{
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(vis[v]) continue;
vis[v] = 1;
if(!match[v] || dfs(match[v])){
match[v] = u;
return 1;
}
}
return 0;
}
int main()
{
scanf("%d %d %d", &n, &m, &e);
memset(head, -1, sizeof(head));
for(int i = 1; i <=e; ++i){
int u, v;
scanf("%d %d", &u, &v);
if(u > n || v > m) continue;
add(u, v);
}
for(int i = 1; i <= n; ++i){
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
system("pause");
}
在将一个问题转化为二分图匹配模型时, 要注意两个要素:
0要素:节点能分成两个独立的集合, 每个集合内部有0条边
1要素:每个节点只能与1条匹配边相连
棋盘覆盖
【描述】:有n * n的棋盘, 输入一些坐标, 表示这些坐标中不能有1 * 2 的方块覆盖它。问这个棋盘上最多能放多少个1 * 2 的棋盘。
【思路】:由于是1*2的方块, 即一个方块连接另一个相邻的方块,这里可以看做是一个二分图模型。由于坐标和全为奇数的集合中任意两点不可能被同一个方块覆盖。所以我们可以将所有坐标和为奇数的点的集合去匹配坐标和为偶数的集合,求一下最大匹配即可。时间复杂度\(O(n^2m^2)\)。
0要素:坐标和为奇数的点和坐标和为偶数的点两个集合,且集合中没有边。
1要素:相邻坐标的坐标和奇偶性不同, 且只能连接一条边。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 1e4 + 10;
const int inf = 0x3f3f3f3f;
int gcd(int a,int b) { return b == 0 ? a : gcd(b, a % b); }
struct edge{
int nex, to;
}Edge[maxn<<2];
int n, t, m[110][110];
int head[maxn], tot;
int vis[maxn], match[maxn];
int ans;
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
inline int id(int x, int y) { return y + (x - 1) * n; }
bool dfs(int u)
{
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(vis[v]) continue;
vis[v] = 1;
if(!match[v] || dfs(match[v])){
match[v] = u;
return 1;
}
}
return 0;
}
int main()
{
scanf("%d %d", &n, &t);
memset(head, -1, sizeof(head));
while(t--){
int a, b;
scanf("%d %d", &a, &b);
m[a][b] = 1;
}
//奇数点连偶数点
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
if((i + j) % 2 == 0) continue;
if(m[i][j]) continue;
if(i > 1 && !m[i-1][j]) add(id(i, j), id(i-1, j));
if(i < n && !m[i+1][j]) add(id(i, j), id(i+1, j));
if(j > 1 && !m[i][j-1]) add(id(i, j), id(i, j-1));
if(j < n && !m[i][j+1]) add(id(i, j), id(i, j+1));
}
}
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
if((i + j) % 2 == 0) continue;
for(int k = 1; k <= n*n; ++k) vis[k] = 0;
if(dfs(id(i, j))) ans++;
}
}
printf("%d\n", ans);
system("pause");
}
车的放置
【描述】:一个n * m的棋盘, 有的地方不能放车,问棋盘上最多能放多少个车,使得不相互攻击。
【思路】:0要素:1车不可能同时属于两行或两列。
1要素:一行与一列只能通过一个车匹配。
所以可以将所有的行当成1个集合, 所有的列当成一个集合,求一下最大匹配就好了。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 4e4 + 10;
const int inf = 0x3f3f3f3f;
int gcd(int a,int b) { return b == 0 ? a : gcd(b, a % b); }
struct edge{
int nex, to;
}Edge[maxn];
int head[maxn], tot;
int n, m, t, arr[210][210];
int vis[maxn], match[maxn];
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
bool dfs(int u){
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(vis[v]) continue;
vis[v] = 1;
if(!match[v] || dfs(match[v])){
match[v] = u;
return 1;
}
}
return 0;
}
int main()
{
scanf("%d %d %d", &n, &m, &t);
memset(head, -1, sizeof(head));
while(t--){
int u, v;
scanf("%d %d", &u, &v);
arr[u][v] = 1;
}
//行匹配列
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= m; ++j){
if(arr[i][j]) continue;
add(i, j);
}
}
int ans = 0;
for(int i = 1; i <= n; ++i){
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
printf("%d\n", ans);
system("pause");
}
二分图的完美匹配
完美匹配:和最大匹配相似, 在二分图中,左右定点都为n, 如果能匹配到n条边的话, 我们称这个匹配是完美匹配。由此看出, 完美匹配一定是最大匹配, 最大匹配不一定是完美匹配。
二分图多重匹配
多重匹配:即1个人左节点或右节点可以与多个右节点或左节点相连。这样的匹配叫做多重匹配。
解法1:可以拆点, 把每一个左节点拆成\(kl_i\)个左节点, 将每一个右节点拆成\(kr_i\)个右结点,然后将它们分边相连。很明显, 当\(kl_i = kr_i = 1\)时, 就简化成一个二分图的最大匹配。
解法2:网络流。
【题意】:n座防御塔, m个侵略者,一个防御塔发射炮弹需要\(t1 + dis(a[i], b[j]) / v\)个时间, 其中 t1 是炮弹发射时间, dis(a[i], b[j])是导弹从防御塔到怪兽之间飞的时间,一个防御塔发射第二枚炮弹时还需等待防御塔冷却时间 t2,问最少要多少时间能把所有怪兽全部消灭。
【思路】:首先这个答案在一个区间内肯定存在, 所以我们可以二分答案,对于每个答案,我们用二分图匹配, 看能不能匹配到 m 个边,如果可以,那么对于小于等于它的答案也可以,否则答案大于它。在判定时, 由于怪兽只能被消灭一次,所以将怪兽作为二分图左结点,对于防御塔,我们可以将一个防御塔看做发射 m 枚导弹的 m 个点, 将这些点分别于怪兽的点相连,注意判断怪兽与第k枚防御塔之间消耗的时间, 不大于才能练边。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> pii;
const int maxn = 55, maxm = 2e5 + 10;
const int inf = 0x3f3f3f3f;
const double eps = 1e-8, pi = acos(-1);
int gcd(int a,int b) { return b == 0 ? a : gcd(b, a % b); }
struct node{
int x, y;
}a[110], b[110];
struct edge{
int nex, to;
}Edge[maxm];
int head[maxm], tot;
int n, m, v;
double t1, t2;
int vis[maxn*maxn], match[maxn*maxn];
inline double dis(node a, node b){
return sqrt(1.0 * (a.x - b.x) * (a.x - b.x) + 1.0 * (a.y - b.y) * (a.y - b.y));
}
inline int id(int x, int y) { return y + m * (x - 1); }
inline void add(int from, int to){
Edge[++tot].to = to;
Edge[tot].nex = head[from];
head[from] = tot;
}
bool dfs(int u)
{
for(int i = head[u]; i != -1; i = Edge[i].nex){
int v = Edge[i].to;
if(vis[v]) continue;
vis[v] = 1;
if(!match[v] || dfs(match[v])){
match[v] = u;
return 1;
}
}
return 0;
}
bool check(double x)
{
memset(head, -1, sizeof(head));
memset(match, 0, sizeof(match));
tot = 0;
for(int i = 1; i <= m; ++i){
for(int j = 1; j <= n; ++j){ //第j座防御塔的第k发炮弹
for(int k = 1; k <= m; ++k){
if(t1 * k + t2 * (k - 1) + dis(a[i], b[j]) / v > x) continue;
add(i, id(j, k));
}
}
}
int ans = 0;
for(int i = 1; i <= m; ++i){
memset(vis, 0, sizeof(vis));
if(dfs(i)) ans++;
}
return ans == m;
}
int main()
{
scanf("%d %d %lf %lf %d", &n, &m, &t1, &t2, &v);
t1 /= 60;
for(int i = 1; i <= m; ++i){
scanf("%d %d", &a[i].x, &a[i].y);
}
for(int i = 1; i <= n; ++i){
scanf("%d %d", &b[i].x, &b[i].y);
}
double l = 0, r = 2000000;
while(r - l > eps)
{
double mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid;
}
printf("%.6lf\n", l);
system("pause");
}