二分图
一. 定义
二分图是节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。
比如下图就是一个二分图,两个集合的元素可以用两种颜色表示,每条边上连接的点属于不同的集合,相同集合的两个点上没有边
注意:二分图中不存在元素为奇数的环
二 . 二分图的判定(染色法)
我们可以枚举每个点,如果点还没被染色,那么将其染为1并用dfs遍历这个点所在的连通块的每个点,染1染2这样交替进行,一旦发现冲突(两个相邻的点是同一个颜色)那么该图就不是一个二分图
例题 二分图判定板子
给你一张简单无向图,你需要判断这张图是否为二分图。
图用以下形式给出:
第一行输入两个整数 n,m,表示图的顶点数和边数,顶点编号从 1 到 n。
接下来 m 行,每行两个整数 x,y,表示 x 和 y 之间有一条边。
输出一个字符串 Yes 或者 No,Yes 表示是二分图。
输入格式
第一行两个整数 n,m。接下来 m 行,每行有两个整数,代表一条边。
输出格式
输出一个字符串表示答案。样例输入
4 4
1 2
2 3
3 4
1 4
样例输出
Yes
数据规模
对于所有数据,保证 2≤n≤1000,0≤m≤10000,1≤x,y≤n,x≠y
代码
# include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n,m;
int st[N]; //染色,没染为0,染色为1或2
vector<int> edge[N];
bool dfs(int x) //返回值为染色过程中是否会发生冲突
{
for(auto t: edge[x])
{
if(!st[t])
{
st[t] = 3-st[x]; //将1变成2,2变成1
if(!dfs(t)) return false; //如果染色过程中发生冲突,返回false
}
else if(st[t] == st[x]) return false; //如果两个临点颜色一样,返回false
}
return true;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int x,y;scanf("%d%d",&x,&y);
edge[x].push_back(y);
edge[y].push_back(x);
}
for(int i=1;i<=n;i++)
{
if(!st[i]) //遍历每个点,只要这个点没被染色,标1
{
st[i] = 1;
if(!dfs(i)) //这里的dfs是将该点所在连通块的所有点染色
{
cout<<"No\n";
return 0;
}
}
}
cout<<"Yes\n";
return 0;
}
三 . 二分图的最大匹配(匈牙利算法)
求二分图的最大匹配用到的是匈牙利算法
匹配的意思是每一个点都只对应着另一个集合的一个点,不存在一对多,多对一的现象
匈牙利算法的流程是 从二分图的两个集合中选择一个集合遍历集合中每一个点 如果这个点与之连线的点还没有被匹配,则与之匹配。或者即使有匹配但与之匹配的点还能在找到与之匹配的,那么贪心的将能匹配的都匹配上
例题1 匈牙利算法板子题(acwing 861)
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2 和 m。接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。数据范围
1≤n1,n2≤500
1≤u≤n1
1≤v≤n2
1≤m≤105输入样例:
2 2 4
1 1
1 2
2 1
2 2
输出样例:
2
代码
# include<bits/stdc++.h>
using namespace std;
int n1,n2,m;
const int N = 510;
vector<int> edge[N]; //存边
int st[N],match[N]; //st存这轮是否被访问过,match存每个点的匹配是谁
bool find(int x) //返回这个点是否能找到匹配
{
for(auto i:edge[x]) //遍历这个点所有与之相邻的点
{
if(!st[i]) //如果st[i]则证明这轮已经找过这个点了,这个点已经进行过调整了,再进行调整不会有任何变化
{
st[i] = true;
if(match[i] == 0 || find(match[i]) ) //如果还没有被匹配或者匹配的点还能找到新的匹配 则x点与i匹配成功
{
match[i] = x;
return true;
}
}
}
return false;
}
int main()
{
cin>>n1>>n2>>m;
for(int i=0;i<m;i++)
{
int u,v;
cin>>u>>v;
edge[u].push_back(v); //因为每次都是从n1这个集合里的点访问边,所以需要设成有向图,由n1的点指向n2的点
}
int ans = 0;
for(int i=1;i<=n1;i++)
{
memset(st,0,sizeof st); //这个st只代表找与i匹配的点的时候每个数字有没有被遍历过,每轮st都要置0
if(find(i)) ans++; //如果这个点找到一个匹配,ans++
}
cout<<ans<<endl;
return 0;
}
例题2 代码源oj793 棋盘覆盖问题
现在有一个 n 行 n 列的棋盘,坐标范围从 (1,1)−(n,n),你需要用 1×2 的多米诺骨牌去覆盖这张棋盘。
这张棋盘上有 m 个格子已经放置了棋子,即这些格子不能被覆盖,现在要你求出骨牌最多可以覆盖多少个格子。
第一行输入 n,m。
接下来 m 行,每行两个数字 xi,yi 表示棋子的坐标。
输出一个数表示最多覆盖的格子数。
输入格式
第一行两个整数 n,m。接下来 m 行,每行有两个整数。
输出格式
输出一个数字表示答案。样例输入
3 1
2 2
样例输出
8
数据规模
对于所有数据,保证 2≤n≤40,0≤m≤n2,1≤xi,yi≤n,棋子所在格子各不相同。
我们可以发现除了已经给出的 不可被覆盖的点外,每个多米诺骨牌需要占两个点,
那么我们其实可以将每相邻的两个点标记为不同的颜色,且一共只用两种颜色标记点。那么这个图的点就会形成颜色不同的两个集合,也就转化成了二分图
且每个相邻的两个点之间都连有一条边。选取最大的覆盖面积也就从选取最多的相邻两点转变成了二分图的最大匹配。
代码
# include<bits/stdc++.h>
using namespace std;
const int N = 2500;
int n,m;
int st[N][N]; //每个点的颜色
int n1,n2; //左边集合有几个点,右边集合有几个点
int c[N][N]; //这个点是左边或者右边的第几个点(是左是右由st判断)
int match[N] ; //右边的这个点匹配的是左边的哪个点
vector<int> edge[N];
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
bool d[N]; //匈牙利算法中判断这个点是否被遍历过
bool find(int x)
{
for(auto c : edge[x])
{
if(!d[c])
{
d[c] = true;
if(match[c] ==0 || find(match[c]))
{
match[c] = x;
return true;
}
}
}
return false;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int x,y;scanf("%d%d",&x,&y);
st[x][y] = 1;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(st[i][j] == 1) continue; //如果这个点是不能被覆盖的点那么直接continue
if((i+j)%2 == 0) //我们可以定义染1号颜色的与原点的曼哈顿距离为偶数那么2号颜色就为奇数
{
st[i][j] = 2; n1++;
c[i][j] = n1;
}
else
{
n2++;
c[i][j] = n2;
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(st[i][j] == 2)
{
for(int k=0;k<4;k++)
{
int x = i+dx[k],y = j+dy[k];
if(x<1 || x>n || y<1 || y>n || st[x][y] == 1) continue;
edge[c[i][j]].push_back(c[x][y]); //将每个右边集合的元素加入到与之相邻左边集合的元素中
}
}
}
}
int ans = 0;
for(int i=1;i<=n1;i++) //然后就是匈牙利算法的板子
{
memset(d,0,sizeof d);
if(find(i)) ans++;
}
cout<<ans*2<<endl;
return 0;
}
例题3 代码源oj799 最大独立集
给你一张二分图,图中没有重边,你需要求出这张图中最大独立集包含的顶点个数。
最大独立集是指:在图中选出最多的点,满足他们两两之间没有边相连。
图用以下形式给出:
第一行输入两个整数 n,m,表示图的顶点数和边数,顶点编号从 1 到 n。
接下来 m 行,每行两个整数 x,y,表示 𝑥 和 之间有一条边。
输出一个数为最大独立集大小。
输入格式
第一行两个整数 n,m。接下来 m 行,每行有两个整数,代表一条边。
输出格式
输出一个数表示答案。样例输入
4 3
1 2
1 4
3 4
样例输出
2
数据规模
对于所有数据,保证 2≤n≤1000,0≤m≤10000,1≤x,y≤n,x≠y
对于每个匹配,都最少有一个元素不可以被放入独立集中,那么独立集中最多的点数就是n-匹配数
代码
# include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n,m;
vector<int> edge1[N]; //存二分图中无向边,为之后染色用
vector<int> edge[N]; //二分图匹配时用的有向边
int st[N]; //点的颜色
int n1,n2; //n1集合和n2集合
int c[N]; //在各自集合中的编号
bool d[N];
int match[N];
bool find(int x)
{
for(auto i:edge[x])
{
if(!d[i])
{
d[i] = true;
if(match[i] == 0 || find(match[i]))
{
match[i] = x;
return true;
}
}
}
return false;
}
void dfs(int x) //染色
{
for(auto i:edge1[x])
{
if(!st[i])
{
st[i] = 3-st[x];
dfs(i);
}
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int x,y;scanf("%d%d",&x,&y);
edge1[x].push_back(y);edge1[y].push_back(x);
}
for(int i=1;i<=n;i++)
{
if(!st[i])
{
st[i] = 1;
dfs(i);
}
}
for(int i=1;i<=n;i++)
{
if(st[i] == 1)
{
n1++;c[i] = n1;
}
else
{
n2++;c[i] = n2;
}
}
for(int i=1;i<=n;i++)
{
if(st[i] == 1)
{
for(auto x:edge1[i])
{
edge[c[i]].push_back(c[x]); //给图重新建边,让n1集合指向n2集合
}
}
}
int ans = 0;
for(int i=1;i<=n1;i++)
{
memset(d,0,sizeof d);
if(find(i)) ans++;
}
cout<<n-ans;
return 0;
}
例题4
给你一张有向无环图,你需要求出最少用多少条互不相交的路径可以覆盖图中所有顶点。
两条路径不相交是指两条路径不经过同一个点。
路径可以不包含边。
图用以下形式给出:
第一行输入两个整数 n,m,表示图的顶点数和边数,顶点编号从 1 到 n。
接下来 m 行,每行两个整数 x,y,表示 x 和 y 之间有一条边。
输出一个数为最少的路径条数。
输入格式
第一行两个整数 n,m。接下来 m 行,每行有两个整数,代表一条边。
输出格式
输出一个数表示答案。样例输入
5 4
1 3
2 3
3 4
3 5
样例输出
3
数据规模
对于所有数据,保证 2≤n≤1000,0≤m≤10000,1≤x,y≤n,x≠y
这道题涉及到了一个网络流很常见的思想 : 拆点
将一个点n拆成两个点分别位于左右两个集和,左集合为出边,右集合为入边。
那么就可以将这个图转化为一个二分图。
最后的答案就是n-最大匹配数
代码
# include<bits/stdc++.h>
using namespace std;
const int N = 1e3+10;
int n,m;
vector<int> edge[N];
int d[N];
int match[N];
bool find(int x)
{
for(auto i:edge[x])
{
if(!d[i])
{
d[i] = true;
if(match[i] == 0 || find(match[i]))
{
match[i] = x;
return true;
}
}
}
return false;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int x,y;scanf("%d%d",&x,&y);
edge[x].push_back(y);
}
int ans = 0;
for(int i=1;i<=n;i++) //点n既是左边集合的第n个点又是右边集合的第n个点
{
memset(d,0,sizeof d);
if(find(i)) ans++;
}
cout<<n-ans<<endl;
return 0;
}