二分图
什么是二分图
若无向图 G=(V,E)G=(V,E) 的顶点集 VV 可以分割为两个互不相交的子集,且图中每条边的两个顶点分别属于不同的子集,则称图 GG 为一个二分图。
简而言之,就是顶点集V可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。
二分图的一个等价定义是:不含有「含奇数条边的环」的图
也就是将图中的节点分到两个集合中,满足:只存在由一个集合中的点指向另一个集合中的点的边。(也就是说两个集合中点不互通)
染色法判定二分图
我们找到无向二分图本质上就是一种不存在奇数环的图,使用染色法就可以进行快速判定。
方法步骤:
首先随意选取一个未染色的点进行染色,然后尝试将其相邻的点染成相反的颜色。如果邻接点已经被染色并且现有的染色与它应该被染的颜色不同,那么就说明不是二分图。而如果全部顺利染色完毕,则说明是二分图。染色结束后的情况(记录在数组中)便将图中的所有节点分为了两个部分,即为二分图的两个点集。
1.随意选取点1未染色的点进行染色
2.扫描与他相邻的点2,3,染成相反对颜色
3.枚举每一条边
4.邻接点已经被染色并且现有的染色与它应该被染的颜色不同,那么就说明不是二分图。而如果全部顺利染色完毕,则说明是二分图
dfs版本
代码思路
染色可以使用1和2区分不同颜色,用0表示未染色
遍历所有点,每次将未染色的点进行dfs, 默认染成1或者2
由于某个点染色成功不代表整个图就是二分图,因此只有某个点染色失败才能立刻break/return
染色失败相当于至少存在2个点染了相同的颜色
题目链接: leetcode_ 785. 判断二分图
代码实现
bool dfs(vector<vector<int>>& graph,vector<int>& color, int u, int c) // 将u号点 染成c 颜色 (c = 1 或 2 )
{
color[u] = c;
for(auto e : graph[u])
{
if(color[e] == color[u])
return false;
if(!color[e] && !dfs(graph, color, e, 3 - c))
return false;
}
return true;
}
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
// 定义 color 数组,初始值为 0 表示未被访问,赋值为 1 或者 2 表示两种不同的颜色。
vector<int> color(n, 0);
bool flag = true;
for(int i = 0; i < n; i++)
{
if(!color[i])
{
flag = dfs(graph, color, i, 1);
if(flag == false)
break;
}
}
return flag;
}
};
bfs版本
代码思路
颜色 1 和 2 表示不同颜色, 0 表示 未染色
定义queue是存PII,表示 <点编号, 颜色>,
同理,遍历所有点, 将未染色的点都进行bfs
队列初始化将第i个点入队, 默认颜色可以是1或2
while (队列不空)
每次获取队头t, 并遍历队头t的所有邻边
若邻边的点未染色则染上与队头t相反的颜色,并添加到队列
若邻边的点已经染色且与队头t的颜色相同, 则返回false
代码实现
bool isBipartite(vector<vector<int>>& graph) {
int n = graph.size();
// 定义 visited 数组,初始值为 0 表示未被访问,赋值为 1 或者 2 表示两种不同的颜色。
vector<int> visited(n, 0);
queue<int> q ;
// 因为图中可能含有多个连通域,所以我们需要判断是否存在顶点未被访问,若存在则从它开始再进行一轮 bfs 染色。
for (int i = 0; i < graph.size(); i++) {
if (visited[i] != 0) {
continue;
}
// 入队一个点, 判断它的连通域,求出是否存在互斥 关系
q.push(i);
visited[i] = 1;
while (!q.empty()) {
int v = q.front();
q.pop();
for (auto e : graph[v]) {
// 如果当前顶点的某个邻接点已经被染过色了,且颜色和当前顶点相同,说明此无向图无法被正确染色,返回 false。
if (visited[e] == visited[v]) {
return false;
}
if (visited[e] == 0) {
visited[e] = 3 - visited[v];
q.push(e);
}
}
}
}
return true;
}
};
二分图的最大匹配(匈牙利算法)
什么是最大匹配
要了解匈牙利算法必须先理解下面的概念:
匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
下面是一些补充概念:(看不懂 不要紧)
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。
匈牙利算法:
一个简单的男女处对象的例子,就可以弄懂匈牙利算法了 — 其实匈牙利就是一个恋爱高手,通过一个形象的例子 完全理解匈牙利算法。
初始状态下:
第一步:从男生A开始匹配:
第二步:男生B开始匹配:
第三步:男生C开始匹配: 很重要的一点
也就是说 女生B 把男生A绿了,(图中绿色线表示的就是被绿的关系)。 之后男生A 和 女生C 又凑成一对。 现实中,这种现象也有不少.
大家慢慢的 也就 无师自通了 。
第四步:男生D开始匹配 , 只能匹配女生D
最终 就会有四组无向图的最大匹配 。 所以该二分图的最大匹配是4
时间复杂度分析:远小于O(n*m)
男生的人数也就是点的个数时n, 边的个数是m, 那么我们只需要遍历每一个男生,看男生与哪一个女生匹配就可以了,也就是再遍历这些边, 所以复杂度为O(nm) , 但是我们遍历边的时候,往往不需要将所有的边都遍历。 所以时间复杂度往往小于O(nm)
题目链接 : P3386 【模板】二分图最大匹配
代码实现
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510 , M = 100010;
int n1,n2,m;
int h[N],ne[M],e[M],idx;
bool st[N]; // 避免重复判断
int match[N]; // match数组的含义是 女生配对的男生 如 match[i] = j 女生i配对的是男生j
void add(int a , int b) // 邻接表建边
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void init()
{
memset(h,-1,sizeof h);
}
int find(int x)
{
//遍历自己喜欢的女孩
for(int i = h[x] ; i != -1 ;i = ne[i])
{
int j = e[i];
if(!st[j])//如果在这一轮模拟匹配中,这个女孩尚未被预定
{
st[j] = true;//那x就预定这个女孩了
//如果女孩j没有男朋友,或者她原来的男朋友能够预定(找到)其它喜欢的女孩。配对成功
if(!match[j]||find(match[j]))
{
match[j] = x; // 将 女生j 与男生 x 配对
return true;
}
}
}
//自己中意的全部都被预定了。配对失败。
return false;
}
int main()
{
init();
cin>>n1>>n2>>m;
while(m--)
{
int a,b;
cin>>a>>b;
add(a,b);
}
int res = 0;
for(int i = 1; i <= n1 ;i ++)
{
//因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
memset(st,false,sizeof st);
if(find(i))
res++;
}
cout<<res<<endl;
}