二分图(Bipartite Graph)
二分图的判定
理论:如果某个图为二分图,那么它至少有两个顶点,且其所有回路的长度均为偶数(偶环)。任何无回路的的图均是二分图。
方法:染色法——》用两种颜色,对所有顶点逐个染色,且相邻顶点染不同的颜色,如果发现相邻顶点染了同一种颜色(即存在奇环),就认为此图不为二分图。
时间复杂度: O ( N ) O(N) O(N)
板子
bfs
//存图
struct Edge{
int to,ne,w;
}e[M<<1];
int head[N],cnt;
void add(int x,int y,int z){
e[++cnt].w=z;
e[cnt].to=y;
e[cnt].ne=head[x];
head[x]=cnt;
}
//数据结构
int color[N];
bool bfs(int mid)
{
queue<int> q;
for(int i=1;i<=n;++i) color[i]=0;
for(int i=1;i<=n;++i){
if(color[i]==0){
color[i]=1;
q.push(i);
while(q.size()){
int u=q.front();
q.pop();
for(int j=head[u];j;j=e[j].ne){
if(e[j].w<=mid) continue;
if(color[e[j].to]==color[u]) return false; //相邻点被染成同样的颜色,不是二分图
if(color[e[j].to]==0) { //未染色的,染成相反颜色
color[e[j].to]=-color[u];
q.push(e[j].to);
}
}
}
}
}
return true;
}
dfs
#include <bits/stdc++.h>
using namespace std;
const int N=1e3; //点数
int n,m;
//数据结构
vector<int> g[N]; //存图
int color[N]; //标记点被染成什么颜色
//核心
bool dfs(int u,int c){ //点,颜色
color[u]=c;
for(auto v:g[u]){
if(color[v]==c) return false; //相邻点被染成同样的颜色,不是二分图
if(color[v]==0 && !dfs(v,-c)) return false; //如果v还未染色,dfs将v染成相反的颜色
}
return true;
}
void solve(){
for(int i=1;i<=n;++i){
if(!color[i] && !dfs(i,1)) printf("NOT Bipartite Graph\n");
}
printf("A Bipartite Graph\n");
}
int main(){
cin>>n>>m; //点数,边数
memset(color,0,sizeof color);
int u,v;
for(int i=1;i<=m;++i) {
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
solve();
return 0;
}
例题:
洛谷 P1525 [NOIP2010 提高组] 关押罪犯
题意:有2的集合,n个人,m对矛盾关系,每对关系分别涉及到x,y两人,矛盾值为w。当x,y被分到一个集合里,会产生w的矛盾,集合的影响力定义为集合中产生的矛盾的最大值。问如何分配n个人,使得集合的影响力最小。
思路:二分答案+二分图判断。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e4+4;
const int M=1e5+5;
int n,m;
struct Edge{
int to,ne,w;
}e[M<<1];
int head[N],cnt;
void add(int x,int y,int z){
e[++cnt].w=z;
e[cnt].to=y;
e[cnt].ne=head[x];
head[x]=cnt;
}
int l,r;
int color[N];
bool bfs(int mid) //bfs判断二分图
{
queue<int> q;
for(int i=1;i<=n;++i) color[i]=0;
for(int i=1;i<=n;++i){
if(color[i]==0){
color[i]=1;
q.push(i);
while(q.size()){
int u=q.front();
q.pop();
for(int j=head[u];j;j=e[j].ne){
if(e[j].w<=mid) continue;
if(color[e[j].to]==color[u]) return false;
if(color[e[j].to]==0) {
color[e[j].to]=-color[u];
q.push(e[j].to);
}
}
}
}
}
return true;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
int u,v,w;
for(int i=1;i<=m;++i){
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
r=max(r,w);
}
while(l<=r){
int mid=(l+r)/2;
if(bfs(mid)) r=mid-1;
else l=mid+1;
}
if(bfs(0)) cout<<0<<'\n';
else{
l++;
while(bfs(l) && l) l--;
l++;
cout<<l<<'\n';
}
}
另一种做法:并查集
#include <bits/stdc++.h>
using namespace std;
const int N=2e4+10;
const int M=1e5+5;
int n,m;
struct Edge{
int u,v,w;
}e[M];
bool cmp(Edge x,Edge y){
return x.w>y.w;
}
int fa[N];
int get(int x){
if(fa[x]==0) return fa[x]=x;
return fa[x]==x?x:fa[x]=get(fa[x]);
}
void merge(int x,int y){
fa[get(y)]=get(x);
}
bool check(int x,int y){
if(get(x)==get(y)) return true;
return false;
}
unordered_map<int,int> mp;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;++i){
cin>>e[i].u>>e[i].v>>e[i].w;
}
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m+1;++i){
if(check(e[i].u,e[i].v)) {
cout<<e[i].w<<'\n';
break;
}
if(mp[e[i].u]==0) mp[e[i].u]=e[i].v;
else merge(mp[e[i].u],e[i].v);
if(mp[e[i].v]==0) mp[e[i].v]=e[i].u;
else merge(mp[e[i].v],e[i].u);
}
}
二分图最大匹配
二分图中的匹配是一组边的选择方式,使得一个端点不会对应两条边。最大匹配是最大边数的匹配。在最大匹配中,如果添加了任何边,则不再是匹配。给定的二部图可以有多个最大匹配。
这有什么用?
现实世界中有许多问题可以作为二分匹配来解决。例如,考虑以下问题:
有m个职位申请者和n个职位。每个申请人都有他/她感兴趣的工作子集。每个职位空缺只能接受一个应聘者,一个职位应聘者只能被指定一个职位。找一份工作分配给申请者,以便尽可能多的申请者得到工作。参考
基本理论
1.在二分图中,最小顶点覆盖和最大匹配在数值上相等。
2.增广路径
匈牙利算法
裸题:
HDU-2963 过山车
题意:有n个女孩,m个男孩,k对男女关系x,y,表示女孩x愿意跟男孩y做partner,求最大组合数。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int k,n,m;
vector<int> g[550]; //v[i]:标记Ai可以连B集合的哪些点
//数据结构
bool vis[550]; //每次标记B集合被占有的点
int match[550]; //标记B集合的点匹配的点
//主要函数
bool dfs(int u){
for(auto v:g[u]) { //A集合中的u能连到B集合中的v
if(vis[v]==0) { //v点还没有被占有
vis[v]=1;
if(match[v]==0 || dfs(match[v])) { //如果B集合的v点没有被占用或者占用v的可以换其他点占用,那么v点可以被u点占用
match[v]=u;
return true;
}
}
}
return false;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
while(cin>>k){
if(k==0) break;
cin>>n>>m;
memset(match,0,sizeof match);
memset(vis,0,sizeof vis);
for(int i=0;i<=500;++i) g[i].clear();
int u,v;
while(k--){
cin>>u>>v;
g[u].push_back(v);
}
//主函数部分
int ans=0;
for(int i=1;i<=n;++i){ //遍历A集合
for(int j=0;j<=m;++j) vis[j]=0; //初始化B集合点的占用情况
if(dfs(i)) ans++;
}
cout<<ans<<'\n';
}
}
好题:Magic Potion
题意:有n个英雄,可以杀若干个怪兽,每个英雄只能杀一次,至多有k个英雄可以有两次杀怪兽的机会,问最多可以杀多少只怪兽。
解法:有限制匹配个数的二分图最大匹配。
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
int k,n,m;
vector<int> g[1003]; //v[i]:标记Ai可以连B集合的哪些点
//数据结构
bool vis[1003]; //每次标记B集合被占有的点
int match[1003]; //标记B集合的点匹配的点
//主要函数
bool dfs(int u){
for(auto v:g[u]) { //A集合中的u能连到B集合中的v
if(vis[v]==0) { //v点还没有被占有
vis[v]=1;
if(match[v]==0 || dfs(match[v])) { //如果B集合的v点没有被占用或者占用v的可以换其他点占用,那么v点可以被u点占用
match[v]=u;
return true;
}
}
}
return false;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m>>k;
int t,tt;
for(int i=1;i<=n;++i){
cin>>t;
for(int j=1;j<=t;++j){
cin>>tt;
g[i].pb(tt);
g[i+n].pb(tt);
}
}
int ans=0,ans1=0;
for(int i=1;i<=2*n;++i){ //遍历A集合
for(int j=0;j<=m;++j) vis[j]=0; //初始化B集合点的占用情况
if(dfs(i)) {
if(i<=n) ans++;
else ans1++;
}
}
cout<<ans+min(ans1,k)<<'\n';
}
HK(hopcroft-karp) 算法
优化点:每次选多条长度相同的增广路径:
复杂度: O ( N M ) O(\sqrt N M) O(NM)
板子:
#include <bits/stdc++.h>
using namespace std;
//存图
const int N=505; //左集合点数
const int M=505; //右集合点数
int bgraph[N][M];
//数据结构
const int INF=0x3f3f3f3f;
int cx[N]; //cx[i]表示左集合i顶点所匹配的右集合的顶点序号
int cy[M]; //cy[i]表示右集合i顶点所匹配的左集合的顶点序号
int nx,ny; //左集合点数,右集合点数
int dx[N]; //dx[i]:记录到左集合i点的距离
int dy[M]; //dy[i]:记录到右集合i点的距离
int dis; //记录增广路的长度
bool vis[M]; //记录右集合点是否被标记,用法同匈牙利算法
bool bfs(){
queue<int> q;
dis=INF;
memset(dx,-1,sizeof dx);
memset(dy,-1,sizeof dy);
for(int i=1;i<=nx;++i){
if(cx[i]==-1){
q.push(i);
dx[i]=0;
}
}
while(q.size()){
int u=q.front();
q.pop();
//该路径长度大于dis,等待下一次BFS扩充
//dis是增广路径的长度,所以dis一定是一个奇数
if(dx[u]>dis) break;
//取右侧节点
for(int v=1;v<=ny;++v){
if(bgraph[u][v]&&dy[v]==-1){
dy[v]=dx[u]+1;
//v是未匹配点,停止延伸(查找)
//得到本次BFS的最大遍历层次
if(cy[v]==-1) dis=dy[v];
else { //v是匹配点,继续延伸
dx[cy[v]]=dy[v]+1;
q.push(cy[v]);
}
}
}
}
return dis!=INF; //若dis为INF说明右集合没有未匹配点,也就是没有增广路径了
}
bool dfs(int u){
for(int v=1;v<=ny;++v) {
if(!vis[v] && bgraph[u][v] && dy[v]==dx[u]+1){
vis[v]=1;
if(cy[v]!=-1 && dy[v]==dis) continue;
if(cy[v]==-1 || dfs(cy[v])) {
cy[v]=u;
cx[u]=v;
return true;
}
}
}
return false;
}
int HK() {
int ans=0;
memset(cx,-1,sizeof cx);
memset(cy,-1,sizeof cy);
while(bfs()){ //找到了增广路
memset(vis,0,sizeof vis);
for(int i=1;i<=nx;++i){
if(cx[i]==-1 && dfs(i)) ans++;
}
}
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int e;
cin>>nx>>ny>>e;
int u,v;
while(e--){
cin>>u>>v;
bgraph[u][v]=1;
}
cout<<HK()<<'\n';
}