前置知识
戳这
常见问题
- 二分图判定
- 匈牙利算法
- 最小点覆盖、最大独立集、最小路径点覆盖(最小路径重复点覆盖);
名词解释
- 最小点覆盖
注意:是对于任意的无向图来说的,不仅仅针对二分图;
对于图 G = ( V , E ) G=(V,E) G=(V,E)中的一个点覆盖是一个集合 S ⊆ V S⊆V S⊆V使得每一条边至少有一个端点在 S S S中。
最小点覆盖:点个数最少的 S S S集合。
如下图绿色圈着的点集,就是一个最小点覆盖;
- 最大独立集
选出最多的点,使得选出的点之间没有边(可以向集合外部连边); - 最大团
与最大独立集互补的关系,即选出最多的点,使得选出的任意两点之间都有边; - 最小路径点覆盖
在一个DAG(有向无环图)中,用最少且互不相交(点不重复)的路径将所有点覆盖; - 最小路径重复点覆盖
在最小路径点覆盖的基础之上,去掉互不相交这个限制即可,求法如下:- 先求传递闭包(利用传递性新增了很多边)
- 再求最小路径点覆盖
注意
二分图一般是指无向图,后续例题虽然存在有向图,但是思路还是无向图的;
常见性质
- 一个图是二分图等价于图中不存在奇数环等价于黑白染色法不存在矛盾
- 最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径点覆盖
- 在匈牙利算法中,最大匹配等价于不存在增广路径
增广路径指的是从一个非匹配点循环走(非匹配边/匹配边)最后走到一个非匹配点,这样答案就会增加;
例题
关押罪犯
传送门
二分 + 染色法判定二分图
题面
思路
把罪犯看成点,把仇恨关系看成边,仇恨值看成边权;
那么问题就转化为将所有点分成两组,使得各组内边的权重的最大值尽可能小;
因为这是一个最大值最小化的问题,我们考虑用二分来解决;
假设当前二分的限制为mid
;
判断能否将所有点分成两组,使得所有权值大于mid
的边都在组间,而不在组内。也就是判断由所有点以及所有权值大于mid
的边构成的新图是否是二分图;
判断二分图我们使用染色法,时间复杂度为 O ( n + m ) O(n+m) O(n+m), n n n是点数, m m m是边数;
那么当 m i d ∈ [ a n s , 1 0 9 ] mid∈[ans,10^9] mid∈[ans,109]时,所有边权大于 m i d mid mid的边,必然是所有边权大于 a n s ans ans的边的子集,因此由此构成的新图也是二分图。
当 m i d ∈ [ 0 , a n s − 1 ] mid∈[0,ans−1] mid∈[0,ans−1]时,由于 a n s ans ans是新图可以构成二分图的最小值,因此由大于 m i d mid mid的边构成的新图一定不是二分图。
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <utility>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 2e4 + 10;
int n,m;
vector<pii> G[N];
int color[N];//-1未染色,0白色,1黑色
bool dfs(int u,int c,int val){
color[u] = c;
for(auto to : G[u]){
int v = to.first,w = to.second;
if(w <= val) continue;//只考虑边权>val的边
if(color[v] != -1){
if(color[v] == c) return false;
}
else{//v未被染色,我们去染v
if(!dfs(v,c^1,val)) return false;
}
}
return true;
}
bool check(int val){
//染色法判断二分图
for(int i=1;i<=n;++i) color[i] = -1;
for(int i=1;i<=n;++i)
if(color[i] == -1)
if(!dfs(i,0,val)) return false;
return true;
}
void solve(){
cin >> n >> m;
while(m--){
int u,v,w;
cin >> u >> v >> w;
G[u].push_back({v,w});
G[v].push_back({u,w});
}
int L = 0,R = 1e9;
while(L < R){
int mid = (L + R) >> 1;
if(check(mid)){
//只考虑边权大于mid的边,能构成二分图
R = mid;
}
else L = mid + 1;
}
cout << L << '\n';
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
棋盘覆盖
思维 + 匈牙利算法
传送门
题面
思路
将每个格子看成一个点,然后将相邻的格子连一条无向边;
那么这个问题就转化成了我们最多可以取多少条边,且取出的边不存在公共点;
那么这就是一个最大匹配问题;
接着我们考虑这个是不是一个二分图,如果是二分图的话,那么等价于01染色不存在矛盾;
因为这是一个矩形,肯定是染色不存在矛盾的;
那么这个问题就变成了一个二分图上的最大匹配问题,我们将所有白色的点归到一边,将所有绿色的点归到另一边;
不难发现只有集合之间有边,集合内部不存在边;
进一步,我们可以发现绿色格子是偶数格,白色格子都是奇数格;
我们知道二分图匹配只用枚举其中一个集合即可,代码中我们枚举奇数格子;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
struct Node{
int x,y;
};
const int N = 1e2 + 10;
bool g[N][N];//是否禁止使用
Node match[N][N];//匹配(x,y)是哪个点
bool st[N][N];//防止重复遍历
int n,m;
int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};
bool dfs(int x,int y){
//枚举边
for(int i=0;i<4;++i){
int xx = x + dx[i],yy = y + dy[i];
if(xx < 1 || xx > n || yy < 1 || yy > n) continue;
if(st[xx][yy] || g[xx][yy]) continue;
st[xx][yy] = 1;
Node tt = match[xx][yy];
//未匹配 或者能找到新欢
if(tt.x == 0 || dfs(tt.x,tt.y)){
match[xx][yy] = {x,y};
return true;
}
}
return false;
}
void solve(){
cin >> n >> m;
while(m--){
int x,y;
cin >> x >> y;
g[x][y] = 1;
}
int ans = 0;
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
//枚举奇数格子
if(g[i][j]) continue;
if((i+j)&1){
memset(st,0,sizeof st);
if(dfs(i,j)) ++ans;
}
}
}
cout << ans;
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
机器任务
最小点覆盖问题
题面
思路
因为不用按顺序做,我们贪心的先把所有模式 0 0 0能解决的问题全部解决掉;
如果一个任务可以被机器 A A A的 u u u模式解决,也可以被机器 B B B的 v v v模式解决;
那么我们就给 u u u和 v v v连一条无向边;
随后我们将一个任务看成一条边,两种模式看成两个点;
要完成一个任务就要从这两个点中选一个点(任务可以在A上执行也可以在B上执行);
对于所有任务就要从N+M-2个点中(不包含初始0状态)选出最少的点,覆盖所有的边(任务);
问题就变成求最小点覆盖问题(二分图中最小点覆盖等价于最大匹配数)。
因此我们用匈牙利算法跑最大匹配数即可;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e2 + 10;
vector<int> G[N];
bool st[N];
int match[N];
int n,m,k;
bool dfs(int u){
for(auto v : G[u]){
if(st[v]) continue;
st[v] = 1;
if(match[v] == 0 || dfs(match[v])){
match[v] = u;
return true;
}
}
return false;
}
void solve(){
cin >> m >> k;
for(int i=1;i<=100;++i) G[i].clear();
memset(match,0,sizeof match);
while(k--){
int t,u,v;
cin >> t >> u >> v;
if(!u || !v) continue;//模式0直接处理
G[u].push_back(v);
}
int ans = 0;
for(int i=1;i<=n;++i){
memset(st,0,sizeof st);
if(dfs(i)) ++ans;
}
cout << ans << '\n';
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
while(cin >> n,n)
solve();
return 0;
}
骑士放置
最大独立集问题
题面
思路
奇偶染色并把一个格子放置骑士能攻击到的格子连一条边;
因为走"日"的话,横纵坐标的改变值(绝对值)相加必然是
3
3
3,那么每次走必然奇偶性会改变;
不难发现这是一个二分图;
题意是要让我们求最大独立集,那么应用性质求总点数-最大匹配数
即可求解;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e2 + 10;
int n,m,t;
struct Node{
int x,y;
};
Node match[N][N];
bool g[N][N];
bool st[N][N];
int dx[] = {-2,-1,1,2,2,1,-1,-2};
int dy[] = {1,2,2,1,-1,-2,-2,-1};
bool dfs(int x,int y){
for(int i=0;i<8;++i){
int xx = x + dx[i];
int yy = y + dy[i];
if(xx < 1 || xx > n || yy < 1 || yy > m) continue;
if(st[xx][yy] || g[xx][yy]) continue;
st[xx][yy] = 1;
Node p = match[xx][yy];
if(p.x == 0 || dfs(p.x,p.y)){
match[xx][yy].x = x,match[xx][yy].y = y;
return true;
}
}
return false;
}
void solve(){
cin >> n >> m >> t;
for(int i=1;i<=t;++i){
int x,y;
cin >> x >> y;
g[x][y] = 1;
}
int ans = 0;
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
if(g[i][j]) continue;
//枚举其中一个集合即可
if((i+j)&1) continue;
memset(st,0,sizeof st);
if(dfs(i,j)) ++ans;
}
}
//总点数 - 最大匹配数 - 坏掉的格子
cout << (n*m) - t - ans << '\n';
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
捉迷藏
应用性质最小点覆盖 = 总点数 - 最大匹配数
题面
思路
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 3e4 + 10;
bool G[N][N];
int n,m;
int match[N];
bool st[N];
void floyd(){
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];
}
bool dfs(int u){
for(int j=1;j<=n;++j){
if(!G[u][j]) continue;
if(st[j]) continue;
st[j] = 1;
if(match[j] == 0 || dfs(match[j])){
match[j] = u;
return 1;
}
}
return 0;
}
void solve(){
cin >> n >> m;
while(m--){
int x,y;
cin >> x >> y;
G[x][y] = 1;
}
floyd();
int ans = 0;
for(int i=1;i<=n;++i){
memset(st,0,sizeof st);
if(dfs(i)) ++ans;
}
cout << n - ans;
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}