文章目录
二分图初步
一、二分图
1.1 二分图定义
图论中的一种特殊模型,设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且同一集合中不同的两点没有边相连。
上图即是一个明显的二分图。
一个基本性质:二分图不含奇数环,含奇数环的图一定不是二分图。
1.2 染色法判二分图
(1) 图中不存在奇数环<=>一个图是二分图(定义)
(2) 图中不存在奇数环<=>染色过程中不存在矛盾
code:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10,M = 2*N;
int h[N],e[M],ne[M],w[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)
{
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()
{
memset(h,-1,sizeof h);
int n,m;
cin>>n>>m;
for(int i = 1;i<=n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
for(int i = 1;i<=n;i++)
{
if(!color[i]&&!dfs(i,1))
{
puts("No");
return 0;
}
}
puts("Yes");
return 0;
}
例题:关押罪犯
二分+染色法
#include<bits/stdc++.h>
using namespace std;
const int N = 2e4+10,M = 2e5+10;
int h[N],e[M],ne[M],w[M],idx;
int n,m,mid;
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)
{
color[u] = c;
for(int i = h[u];i!=-1;i = ne[i])
{
if(w[i]<mid) continue;
int j = e[i];
if(!color[j])
{
if(!dfs(j,3-c)) return false;
}
else if(color[j]==c) return false;
}
return true;
}
bool check(int x)
{
memset(color,0,sizeof color);
for(int i = 1;i<=n;i++)
{
if(!color[i]&&!dfs(i,1))
{
return false;
}
}
return true;
}
int main()
{
memset(h, -1, sizeof h);
cin>>n>>m;
while (m -- ){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
int l = 0, r = 1e9;
while(l < r){
mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
if(n <= 2) cout << 0;
else cout << l - 1;
return 0;
}
/*
作者:cqust_qilin02811
链接:https://www.acwing.com/activity/content/code/content/2671431/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
*/
二、二分图匹配
2.0 定义
匹配:给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。图示:
最大匹配:边数最多的一组匹配
匹配点:在匹配中的点
增广路径:从任意一个非匹配点走,通过非匹配边走到一个匹配点,然后通过匹配边走到匹配点…,最后一定通过一个非匹配边走到一个非匹配点,这样的一条路径就是增广路径。
重要结论:最大匹配等价于不存在增广路径
2.1 匈牙利算法
用处:可以求二分图的最大匹配数量
模板题
这里给出匈牙利算法的过程:
- 准备一个match数组,保存这个点对应的匹配点是谁。
- 图中所有点被划分为两部分,选择任意一部分作为出发点
- 遍历选择的那部分的所有点,看当前遍历点X所有邻边所能到达的点P,只要点P没有被访问过,就把它访问,然后给它标记匹配点:即match[P] = X;然后return true;如果访问过,就看当前match[P]所对应的那个点能不能找到新的点去匹配,如果可以,则match[P] = X;return true;
code:
#include<bits/stdc++.h>
using namespace std;
const int N = 510,M = 2e5+10;
int n1,n2,m;
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 u)
{
for(int i = h[u];~i;i = ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
if(!match[j]||find(match[j]))
{
match[j] = u;return true;
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin>>n1>>n2>>m;
while (m -- ){
int a,b;
cin>>a>>b;
add(a,b);
}
int cnt = 0;
for(int i = 1;i<=n1;i++)
{
memset(st, 0, sizeof st);
if(find(i))
cnt++;
}
cout<<cnt;
return 0;
}
例题:棋盘覆盖
并不用建图,二维数组本身就是图,对于一半的点做最大匹配即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
bool g[N][N];
int n,m;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
typedef pair<int, int> PII;
#define x first
#define y second
PII match[N*N];
bool st[N*N];
bool find(int x,int y)
{
for(int i = 0;i<4;i++)
{
int nx = dx[i]+x,ny = dy[i]+y;
int id = (nx)*n+ny;
if(nx<=0||ny<=0||nx>n||ny>n||st[id]||g[nx][ny]) continue;
st[id] = true;
PII t = match[id];
if(t.x==-1||find(t.x,t.y))
{
match[id] = {x,y};
return true;
}
}
return false;
}
int main()
{
memset(match,-1,sizeof match);
cin>>n>>m;
while (m -- ){
int x,y;
cin>>x>>y;
g[x][y] = true;
}
int cnt = 0;
for(int i = 1;i<=n;i++)
{
for(int j = 1;j<=n;j++)
{
if(g[i][j]) continue;
memset(st, 0, sizeof st);
if((i+j)%2==0&&find(i,j))
cnt++;
}
}
cout<<cnt;
return 0;
}
三、最小覆盖问题
最大匹配数 = 最小点覆盖 = 总点数-最大独立集 = 总点数-最小路径覆盖
3.1 最小点覆盖
给定一个图(不一定是二分图),选出最少的点,使得所有的边能够被这些点访问到
最小点覆盖 = 最大匹配数(1)
例题:
机器任务
注意到让机器重启次数最小,实际上就是寻找最小点覆盖,也就是最大匹配数。
另外注意小细节,机器状态为0的时候不用重启,所以只需要建立状态大于等于1的图即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 110,K = 1010,M = K*2;
int match[N];
int h[N],e[M],ne[M],idx;
int n,m,k;
bool st[N];
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
bool find(int u)
{
for(int i = h[u];~i;i = ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
if(!match[j]||find(match[j]))
{
match[j] = u;
return true;
}
}
}
return false;
}
int main()
{
while(cin>>n&&n)
{
memset(h,-1,sizeof h);
memset(match,0,sizeof match);
cin>>m>>k;
for(int i = 0;i<k;i++)
{
int id,a,b;
cin>>id>>a>>b;
if(!a||!b) continue;
add(a,b);
}
int cnt = 0;
for(int i = 1;i<=n;i++)
{
memset(st,0,sizeof st);
if(find(i)) cnt++;
}
cout<<cnt<<endl;
}
return 0;
}
3.2 最大独立集与最大团
最大独立集定义:从一个图中选出最多的点,使得任意两个选出的点之间不存在边。
最大团定义:从一个图中选出最多的点,使得任意两个选出的点之间都存在边。
容易知道:最大团和最大独立集是互补关系。
原图的最大独立集就是补图的最大团。
在二分图中,求最大独立集:
等价于去掉最少的点使剩下的点都没有边。
等价于找最小点覆盖
等价于求最大匹配
总点数为n,最大匹配数量记为m
那么最大独立集 = n-m
例题:骑士放置
和棋盘覆盖那道题几乎一样。
code:
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int h[N],e[N*N],ne[N*N],idx,n,m,t;
bool st[N][N];
int g[N][N];
PII match[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
#define x first
#define y second
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
bool find(int x,int y)
{
for(int k = 0;k<8;k++)
{
int nx = x+dx[k],ny = y+dy[k];
if(nx<=0||ny<=0||nx>n||ny>m||g[nx][ny]) continue;
if(st[nx][ny]) continue;
st[nx][ny] = true;
PII &t = match[nx][ny];
if(t.x==0||find(t.x,t.y))
{
match[nx][ny] = {x,y};
return true;
}
}
return false;
}
int main()
{
memset(g,0,sizeof g);
cin>>n>>m>>t;
for(int i = 0;i<t;i++)
{
int x,y;
cin>>x>>y;
g[x][y] = 1;
}
int cnt = 0;
for(int i = 1;i<=n;i++)
{
for(int j = 1;j<=m;j++)
{
if(g[i][j]||(i+j)%2==1) continue;
memset(st, 0, sizeof st);
if(find(i,j)) cnt++;
}
}
cout<<n*m-cnt-t;
return 0;
}
3.3 最小路径点覆盖与最小路径重复点覆盖
最小路径点覆盖:
简称为最小路径覆盖。
针对有向无环图(DAG),最少用多少条互不相交的路径,可以将所有点覆盖。
互不相交:(点不重复,边也不重复)
技巧:拆点建图,把一个点分成两个点,分别为出点和入点
容易发现,拆点过后,得到的图一定是一个二分图
最小路径重复点覆盖:
即用多少条路径可以把图的所有点覆盖(不需要互不相交)
- 求传递闭包G’
- 在G’上求最小路径覆盖
求原图中互不相交路径数等价于求最大匹配。
详细证明见:大佬链接
参考资料
acwing算法基础课、提高课
大佬博客