二分图概念
以下概念转自其他博客,不是原创。
二分图:简单来说,如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。准确地说:把一个图的顶点划分为两个不相交集A和B ,使得每一条边都分别连接 A、B中的顶点。如果存在这样的划分,则此图为一个二分图。
二分图的一个等价定义是:不含有「含奇数条边的环」的图。例如图1不存在环所以为二分图,而图4中的1、4、5构成了一个环,而这个环中有三个边所以它不是一个二分图
匹配:在图论中,一个「匹配」(matching)是一个边的集合,其中任意两条0边都没有公共顶点。例如,图 3、图 2 中红色的边就是图 1 的匹配
我们定义匹配点、匹配边、未匹配点、非匹配边,它们的含义非常显然。例如图 2 中 1、4、5、7 为匹配点,其他顶点为未匹配点;1-5、4-7为匹配边,其他边为非匹配边。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 3是一个最大匹配,它包含 4 条匹配边。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 3是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。
二分图染色的概念:
二分图染色是一种用来判断给定图(有向图或无向图)是否是二分图的算法。
在图上不断进行BFS或DFS,并在运行过程中不断对结点进行"染色","染色"保证相邻结点的颜色必然不同。如果无法保证,则这个图就是二分图.
二分图染色时的注意事项:
二分图染色的题常会结合DP进行考察,因此往往要注意推理状态转移方程
二分图染色类型的题目也有可能结合类似DAG上的推论这种图上定理进行考察,这就要求对图论有一定的掌握。特别需要注意非联通图的情况.
二分图染色的例题(难度由低到高):
P1330封锁阳光大学
HDU2444
关押罪犯
二分图染色的适用范围:
只要是图即可.
判断一个图是二分图的充分条件:
图上没有边数为奇数的环.
基本上二分图染色也是模板
/*
二分图染色匹配数据分析
4 4
1 2
1 3
1 4
2 3
先把1染成1然后与之相连的2、3、4都变成了2
然后对与2相邻的染色 当循环遍历到3的时候 2 3颜色相同所以这个图不是二分图
6 5
1 2
1 3
1 4
2 5
3 6
先对1染色成1与之相邻的 2、3、4、都变成2
然后对与2相邻的染色 5被染成了1
然后对与3相邻的染色 6被染成了1
然后对4 5 6染色无与之相邻的返回1 所以这个图是二分图
*/
#include<cstdio>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<string>
#include<queue>
#include<iostream>
using namespace std;
const int maxn=505;
int num[maxn][maxn];
int vis[maxn];
int tu[maxn];
int n,m;
int bfs() {
queue<int>q;
//从图中的第一个点开始图颜色
//如果下标是从0开始的那么应该把0放进队列里面
q.push(1);
vis[1]=1;
tu[1]=1;
int top;
//bfs染色
while(q.size()) {
top=q.front();
q.pop();
for(int i=1;i<=n;i++) {
if(num[top][i]) { //如果这两个点相连
if(!vis[i]) {//如果没有被涂色
vis[i]=1;
//与top涂上不同的颜色
//也可以把tu数组初始化为0用tu[i]=tu[top]^1进行涂色;
if(tu[top]==1) tu[i]=2;
else tu[i]=1;
q.push(i);
//把这个点加入队列中继续涂色
}
//如果存在两个连接的的点颜色相同就返回0
else if(tu[i]==tu[top]) return 0;
}
}
}
return 1;
}
int main()
{
while(cin>>n>>m) {
memset(tu,0,sizeof(tu));
memset(num,0,sizeof(num));
memset(vis,0,sizeof(vis));
while(m--) {
int u,v;
cin>>u>>v;
num[u][v]=1;
num[v][u]=1;
}
if(bfs()) cout<<"YES\n";
else cout<<"NO\n";
}
}
上面的例题HDU2444就是先判断它是否为二分图,然后再计算它的最大匹配。下面介绍如何计算最大匹配
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 3是一个最大匹配,它包含 4 条匹配边。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 3是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。例如,图 5 中的一条增广路如图 6 所示(图中的匹配点均用红色标出):
PS:我自己的理解是如果存在增广路那么边数将会增加1,看图5中的4-8这是一条边,而与4相连的有未匹配点9,然后1-8之间有一条未匹配边,接着1-6是匹配边,2-6是未匹配边,那么这条路完全可以变成4-9 8-1 6-2 这样与之前的4-8 1-6相比就多了一条边,这也是匈牙利算法的核心,找到增广路,将匹配边和未匹配边互换直到没有增广路从而找到最大匹配
/*
//
// cx代表一种 一种集合 cy代表另外一个集合
// 例
// 集合A 集合B
// 1 1
// 1 2
// 1 3
// 2 1
// 2 2
// 3 1
//对应关系如下
// 1---------1、2、3
// 2---------1、2
// 3---------1、
//那么最大匹配就是 1-3 2-2 3-1
*/
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
const int maxn=1e4+5;
const int mod=1e9+7;
typedef long long ll;
int cx[maxn];//记录集合A中的数例cx[i]=j代表集合A中的i与集合B中的j匹配
int cy[maxn];//记录集合B中的数
int G[maxn][maxn];//记录两个数是否相连
int vis[maxn];//用来标记
int nx,ny,m;
int pime(int v){//匈牙利算法
for(int u=1;u<=ny;u++) {//遍历第二集合中的所有点
if(G[v][u]&&!vis[u]) {//如果v-u之间存在匹配关系并且u未被访问
vis[u]=1;//标记
if(cy[u]==-1||pime(cy[u])) {
//条件一:右边的v顶点没有左边对应的匹配的点,
//条件二:以v顶点在左边的匹配点为起点能够找到一条增广路
//(如果能够到达条件二,说明v顶点在左边一定有对应的匹配点)
//其实就是想递归找增广路
//更新节点信息
cx[v]=u;
cy[u]=v;
return 1;
}
}
}
return 0;
}
int main()
{
cin>>m>>nx>>ny;
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
int u,v;
while(m--) {
cin>>u>>v;
//记录
G[u][v]=1;
}
int ans=0;
for(int i=1;i<=nx;i++) {
if(cx[i]==-1) {//如果当前这个点是非匹配点 增广路就是从一个非匹配点出发的
memset(vis,0,sizeof(vis));
ans+=pime(i);
}
}
cout<<ans<<endl;
}
那么上面的HDU2444的题也很容易写出来了,你们也可以先把算法的思路写出来然后再自己敲一遍
#include<cstdio>
#include<cstring>
#include<iostream>
#include<string>
#include<queue>
using namespace std;
const int maxn=305;
int G[maxn][maxn];
int cx[maxn],cy[maxn];
int vis[maxn];
int x,y,n;
int ranse[maxn];
int bfs(){
queue<int>q;
q.push(1);
ranse[1]=1;
while(q.size()) {
int v=q.front();
q.pop();
for(int i=1;i<=n;i++) {
if(G[v][i]) {
if(ranse[i]==0) {
if(ranse[v]==1) ranse[i]=2;
else ranse[i]=1;
q.push(i);
}
else if(ranse[i]==ranse[v]) {
return 0;
}
}
}
}
return 1;
}
int prim(int u) {
for(int v=1;v<=x;v++) {
if(G[u][v]&&!vis[v]) {
vis[v]=1;
if(cy[v]==-1||prim(cy[v])){
cx[u]=v;
cy[v]=u;
return 1;
}
}
}
return 0;
}
int main()
{
while(cin>>x>>y) {
memset(G,0,sizeof(G));
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
memset(ranse,0,sizeof(ranse));
n=x;
while(y--) {
int u,v;
cin>>u>>v;
G[u][v]=1;
G[v][u]=1;
}
if(!bfs()||n==1) {//特判n等于1或者不是二分图
cout<<"No\n";
continue;
}
else cout<<"YES\n";
int ans=0;
for(int i=1;i<=x;i++) {//寻找最大匹配
if(cx[i]==-1) {
memset(vis,0,sizeof(vis));
ans+=prim(i);
}
}
cout<<ans/2<<endl;
}
}
接下来讲一个例子,当我们自己跑一组数据的时候就很会发现算法实现的过程了,没自己做出图就拿网上一位大佬的例子来讲了,标注大佬的博客:大佬博客
分析一下poiint函数是如何运行的,通过上图分析,第一次以1顶点为起点进入point(1)函数,然后5号顶点满足条件,更新cx[1],cy[5],返回1。此时1–>5这条边为匹配边,1号,5号顶点为匹配顶点
然后以2顶点为其实点进入point(1)函数,然后访问到5号节点,然后以5号节点的匹配点1号节点为起始点再次进入point(2)函数,找到了7号点,这时说明找到了增广路,然后更新cx[1],cy[7],然后返回1,更新cx[2],cy[5],返回一,函数结束。
然后以3号顶点为起始点进入point(1)函数,然后访问到5号节点,然后以5号节点的匹配点2号节点为起始点再次进入point(2)函数,找不到点了,返回0,到达point(1)函数,继续访问其他的节点。然后访问到6号顶点,满足条件,更新cx[3],cy[6],返回一,函数结束。
然后以4号顶点为起始点进入point(1)函数,然后访问到7号节点,然后以7号节点的匹配点1号节点为起始点再次进入point(2)函数,然后找到5号节点,然后以5号节点的匹配点2号节点为起始点再次进入point(3)函数,找不到点了,返回0,到达point(2)函数,继续访问其他的节点,没有点了,返回0,到达point(1)函数,继续访问其他的节点,到达了8号节点,满足,然后更新cx[4],cy[8],返回一,函数结束。
上图就是找到的所有的匹配边。
下面介绍下KM算法
KM算法用来求二分图最大权完美匹配。
KM算法讲解1
KM算法讲解2
我学这个算法也是从这两个博客上学的当然还有上面那位大佬的博客,建议大家还是先把算法的思路学会了,然后试着自己写一遍
附上一道题目连接,供大家来联系
HDU2255奔小康赚大钱
读完题就会发现这就是让求最大权完美匹配的
代码
/*
看了上面那个大佬的博客之后我们对这个算法都比较清楚了,大概就是每个男生,女生都有一个期望值
那么我们怎么算最大权呢 ?
如果把男生的期望值都给女生,男生期望值都初始化为0就变成了女生找男生的状态了
至于怎么找我也讲下算法的思路
开一个love[][] love[i][j]代表i女生对j男生的好感度
ex_girl[]代表每个妹子的期望值
ex_boy[]代表每个男生的期望值
vis_girl[]用来标记每一轮匹配过的女生
vis_boy[]用来标记每一轮匹配过的男生
match[maxn];//记录每个男生匹配过的女生
slack[]代表某男生要被妹子看上还需要多少期望值
love[MAXN][MAXN];//记录每个妹子和男生的好感度
大概的流程图就是
初始化妹子的好感度,是与之相连的男生中最大的好感度,双for实现
遍历每个女生1-n
初始化slack数组为INF
dfs开始
标记这个妹子
遍历与之相连的男生
计算双方期望值相加与最初输入的love[girl][boy]之间的差值
差值为0 男生是否已经被标记如果没有或者与她已经牵手的女生可以找到其他男生就拆散他们并且返回1
上面的也就是找增广路
若差值不为0 就更新slack记录这个男生当备胎还需要多少期望值
dfs结束
所以可以看出dfs就是在找增广路的基础上增加了权值的计算
返回KM函数 如果找到了就跳出继续找下一位妹子
没有找到就要从本轮标记过的男生中找出当备胎期望值最小的
就是说如果这个女生找不到最大期望值的那就只能让她降低点要求和男生在一起了
因为是递归找的所以所有标记过的女生期望值都减少,相应的男生的期望值增加并且更新slack
这个也就是让没有成功牵手的女生统一降低下标准然后再为这个女生找男朋友直到找到为止
全部匹配成功了
遍历所有女生求出好感度之和
上面的思路过程仔细想想就容易理解了,大概就是女生找男生找到了最适合的就牵手如果说这个男生已经
牵手了那让跟他牵手的那个妹子再去找其他男生,好残忍哈哈, 其实就是找增广路,从而求出全部匹配
后的最大的期望之和
*/
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring >
#include<string>
using namespace std;
const int maxn=1e3+5;
const int INF=1e9+7;
int ex_girl[maxn];//ex_girl[]代表每个妹子的期望值
int ex_boy[maxn];//ex_boy[]代表每个男生的期望值
int vis_girl[maxn];//vis_girl[]用来标记每一轮匹配过的女生
int vis_boy[maxn];//vis_boy[]用来标记每一轮匹配过的男生
int love[maxn][maxn];//记录每个妹子和男生的好感度
int match[maxn];//记录每个男生匹配的女生
int slack[maxn];//slack[]代表某男生要被妹子看上还需要多少期望值
int n,m,t;
int dfs(int girl)//dfs函数求的是编号为gir的女孩能否匹配到男生,如果能,返回true,否则,返回false
{
vis_girl[girl]=1;//标记
for(int i=0;i<n;i++) {
if(!vis_boy[i]) {//未访问过
int pos=ex_girl[girl]+ex_boy[i]-love[girl][i];//计算差值
if(pos==0) {//如果这个条件满足,说明编号为gir女孩和编号为i的男孩可能能够匹配成功
vis_boy[i]=1;
if(match[i]==-1||dfs(match[i])) {//满足其中一个说明妹子可以和男生匹配成功
//*
match[i]=girl;//更新并且返回1
return 1;
}
}
else {
slack[i]=min(slack[i],pos);//没有匹配成功就找差值最小的 更新slack
}
}
}
return 0;
}
int KP()
{
memset(match,-1,sizeof(match));
memset(ex_boy,0,sizeof(ex_boy));
for(int i=0;i<n;i++) {
ex_girl[i]=love[i][0];
for(int j=1;j<n;j++) {
ex_girl[i]=max(ex_girl[i],love[i][j]);
}
}
for(int i=0;i<n;i++) {
// for(int j=0;j<n;j++) slack[j]=INF;
fill(slack,slack+n,INF);
while(1) {//直到当前这个女生找到了男生
memset(vis_girl,0,sizeof(vis_girl));
memset(vis_boy,0,sizeof(vis_boy));
if(dfs(i)) {//如果能直接找到就跳出循环
break;
}
int d=INF;
for(int k=0;k<n;k++) {//寻找差值最小的男生
if(!vis_boy[k]) {
d=min(d,slack[k]);
}
}
for(int p=0;p<n;p++) {//让匹配过的女生期望值减少,匹配过的男生期望值增加
if(vis_girl[p]) {
ex_girl[p]-=d;
}
//
if(vis_boy[p]) {
ex_boy[p]+=d;
}
else slack[p]-=d;//更新slack信息
}
}
}
int ans=0;
for(int i=0;i<n;i++) {
ans+=love[match[i]][i];
}
return ans;
}
int main()
{
while(~scanf("%d",&n)) {
memset(love,0,sizeof(love));
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
scanf("%d",&love[i][j]);
}
}
printf("%d\n",KP());
}
}
能力有限,附上部分题及题解
建图很关键为大家提供了一篇文章可以参考下二分图如何建图
HDU1150
PS:题意有A、B台机器,每个机器有许多种工作模式,再某次开启使用中只能处于一种模式,模式只有重启后才能调节,给出n个工作 包括i xi yi表示i只能在A机器的xi模式或者B机器的yi的模式下完成,问最少需要多少次重启机器。
思路:其实我最先想到的是把任务当做一个集合而机器当做另外一个集合,但是后来WA了才发现同一个机器可以处理好几个相同模式下的任务,所以好吧又没读懂题意,只能乖乖地将机器A和机器B建立二分图了,求出他们之间最大匹配数 就是结果了
代码
/*
*/
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#include<vector>
#include<set>
#include<stdlib.h>
using namespace std;
typedef long long ll;
const int maxn=1e3+1;
int num[maxn][maxn],num1[maxn][maxn];
int cx[maxn];
int cy[maxn];
int vis[maxn];
int nx,ny;
int prie(int x) {//匈牙利算法
for(int y=1;y<=ny;y++) {
if(num[x][y]&&!vis[y]) {
vis[y]=1;
if(cy[y]==-1||prie(cy[y])) {//找增广路
cx[x]=y;
cy[y]=x;
return 1;
}
}
}
return 0;
}
int main()
{
int n,m,k;
while(cin>>n) {
if(n==0) break;
cin>>m>>k;
nx=n;
ny=m;
//设立两个集合
memset(num,0,sizeof(num));
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
int j;
for(int i=0;i<k;i++) {
int a,b;
cin>>j>>a>>b;
num[a][b]=1;
}
int ans=0;
for(int i=1;i<=ny;i++) {
if(cx[i]==-1) {
memset(vis,0,sizeof(vis));
ans+=prie(i);
}
}
cout<<ans<<endl;
}
}
HDU2063水题
PS:女生和男生坐过山车,每个女生都有理想的男生,只有匹配成功了才能坐过山车,问最多有多少个组合可以坐过山车
代码
/*
*/
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
const int maxn=1e3+5;
int G[maxn][maxn],x[maxn],y[maxn];
int k,m,n;
int vis[maxn];
int prim(int u) {
for(int v=1;v<=n;v++) {
if(G[u][v]&&!vis[v]) {
vis[v]=1;
if(y[v]==-1||prim(y[v])) {
x[u]=v;
y[v]=u;
return 1;
}
}
}
return 0;
}
int main()
{
while(cin>>k) {
if(k==0)break;
cin>>m>>n;
memset(G,0,sizeof(G));
memset(x,-1,sizeof(x));
memset(y,-1,sizeof(y));
while(k--) {
int u,v;
cin>>u>>v;
G[u][v]=1;
}
int cnt=0;
for(int i=1;i<=m;i++) {
if(x[i]==-1) {
memset(vis,0,sizeof(vis));
cnt+=prim(i);
}
}
cout<<cnt<<endl;
}
}
HDU1083
PS 题意:
有p门的课,每门课都有若干学生,现在要为每个课程分配一名课代表,每个学生只能担任一门课的课代表,如果每个课都能找到课代表,则输出"YES",否则"NO"。
课堂是一个集合 学生是另外一个结合 建图套匈牙利算法求最大匹配即可。
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
const int maxn=405;
const int mod=1e9+7;
typedef long long ll;
int cx[maxn];
int cy[maxn];
int G[maxn][maxn];
int vis[maxn];
int nx,ny,m;
int pime(int x){
for(int y=1;y<=ny;y++) {
if(G[x][y]&&!vis[y]) {
vis[y]=1;
if(cy[y]==-1||pime(cy[y])) {
cx[x]=y;
cy[y]=x;
return 1;
}
}
}
return 0;
}
int main()
{
int t;
cin>>t;
while(t--){
cin>>nx>>ny;
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
memset(G,0,sizeof(G));
int u,v;
for(int i=1;i<=nx;i++) {
cin>>m;
for(int j=1;j<=m;j++) {
cin>>v;
G[i][v]=1;
}
}
int ans=0;
for(int i=1;i<=nx;i++) {
if(cx[i]==-1) {
memset(vis,0,sizeof(vis));
ans+=pime(i);
}
}
if(ans==nx) cout<<"YES\n";
else cout<<"NO\n";
}
}
接下来点覆盖、最小点覆盖
下面部分参考了某位大佬大家可以去学习下
点覆盖集 即一个点集,使得所有边至少有一个端点在集合里。或者说是“点” 覆盖了所有“边”。。极小点覆盖(minimal vertex covering):本身为点覆盖,其真子集都不是。最小点覆盖(minimum vertex covering):点最少的点覆盖。点覆盖数(vertex covering number):最小点覆盖的点数。
边覆盖、极小边覆盖
边覆盖集即一个边集,使得所有点都与集合里的边邻接。或者说是“边” 覆盖了所有“点”。极小边覆盖(minimal edge covering):本身是边覆盖,其真子集都不是。最小边覆盖(minimum edge covering):边最少的边覆盖。边覆盖数(edge covering number):最小边覆盖的边数。
独立集、极大独立集
独立集即一个点集,集合中任两个结点不相邻,则称V为独立集。或者说是导出的子图是零图(没有边)的点集。极大独立集(maximal independent set):本身为独立集,再加入任何点都不是。最大独立集(maximum independent set):点最多的独立集。独立数(independent number):最大独立集的点。
团
团即一个点集,集合中任两个结点相邻。或者说是导出的子图是完全图的点集。极大团(maximal clique):本身为团,再加入任何点都不是。最大团(maximum clique):点最多的团。团数(clique number):最大团的点数。
边独立集、极大边独立集
边独立集即一个边集,满足边集中的任两边不邻接。极大边独立集(maximal edge independent set):本身为边独立集,再加入任何边都不是。最大边独立集(maximum edge independent set):边最多的边独立集。边独立数(edge independent number):最大边独立集的边数。
边独立集又称匹配(matching),相应的有极大匹配(maximal matching),最大匹配(maximum matching),匹配数(matching number)。
支配集、极小支配集
支配集即一个点集,使得所有其他点至少有一个相邻点在集合里。或者说是一部分的“点”支配了所有“点”。极小支配集(minimal dominating set):本身为支配集,其真子集都不是。最小支配集(minimum dominating set):点最少的支配集。支配数(dominating number):最小支配集的点数。
边支配集、极小边支配集
边支配集即一个边集,使得所有边至少有一条邻接边在集合里。或者说是一部分的“边”支配了所有“边”。极小边支配集(minimal edge dominating set):本身是边支配集,其真子集都不是。最小边支配集(minimum edge dominating set):边最少的边支配集。边支配数(edge dominating number):最小边支配集的边数。
最小路径覆盖
最小路径覆盖(path covering):是“路径” 覆盖“点”,即用尽量少的不相交简单路径覆盖有向无环图G的所有顶点,即每个顶点严格属于一条路径。路径的长度可能为0(单个点)。
最小路径覆盖数=G的点数-最小路径覆盖中的边数。应该使得最小路径覆盖中的边数尽量多,但是又不能让两条边在同一个顶点相交。拆点:将每一个顶点i拆成两个顶点Xi和Yi。然后根据原图中边的信息,从X部往Y部引边。所有边的方向都是由X部到Y部。因此,所转化出的二分图的最大匹配数则是原图G中最小路径覆盖上的边数。因此由最小路径覆盖数=原图G的顶点数-二分图的最大匹配数便可以得解。
二分图的性质
二分图中,点覆盖数是匹配数。
(1) 二分图的最大匹配数等于最小覆盖数,即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可。
(2) 二分图的独立数等于顶点数减去最大匹配数,很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质。
(3) DAG的最小路径覆盖,将每个点拆点后作最大匹配,结果为n-m,求具体路径的时候顺着匹配边走就可以,匹配边i→j’,j→k’,k→l’…构成一条有向路径。
(4)最大匹配数=左边匹配点+右边未匹配点。因为在最大匹配集中的任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖。
(5)最小边覆盖=图中点的个数-最大匹配数=最大独立集
PS :二分图的求解大部分都是用找增广路去求得,而找增广路无疑要用到匈牙利算法,所以二分图的求解基本上就是套模板了而如何建立二分图将是十分关键的,
例题
POJ3020
PS 题意:
一个矩形中,有N个城市’*’,现在这n个城市都要覆盖无线,若放置一个基站,那么它至多可以覆盖相邻的两个城市。
问至少放置多少个基站才能使得所有的城市都覆盖无线?
思路:构造无向图,将给出的点拆分为两个集合得到无向图 然后求无向二分图的最小路径覆盖 = 顶点数 – 最大二分匹配数/2
PS:可以看下大佬写的题解
代码
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
const int maxn=505;
int G[maxn][maxn],cx[maxn],cy[maxn],vis[maxn];
bool temp[maxn][maxn];
int dr[]={-1,1,0,0,};
int dc[]={0,0,-1,1};
int xn,yn;
int m,n;
int prim(int x) {//匈牙利
for(int y=1;y<=yn;y++) {
if(temp[x][y]&&!vis[y]) {
vis[y]=1;
if(cy[y]==-1||prim(cy[y])) {
cy[y]=x;
cx[x]=y;
return 1;
}
}
}
return 0;
}
int main()
{
int t;
cin>>t;
while(t--) {
memset(G,0,sizeof(G));
memset(temp,0,sizeof(temp));
memset(cx,-1,sizeof(cx));
memset(cy,-1,sizeof(cy));
cin>>m>>n;
char s;
int ind=0;
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
cin>>s;
if(s=='*') {
G[i][j]=++ind;//记录所有为*的点
}
}
}
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
if(G[i][j] ) {
for(int k=0;k<4;k++) {//搜索寻找这个点四周有没有相联通的点更新temp
int x=i+dr[k];
int y=j+dc[k];
if(G[x][y]) {//以这两个点分别为行和列构造二分图
temp[G[i][j]][G[x][y]]=1;
}
}
}
}
}
int cnt=0;
xn=yn=ind;
for(int i=1;i<=xn;i++) {
if(cx[i]==-1) {
memset(vis,0,sizeof(vis));
cnt+=prim(i);
}
}
cout<<ind-cnt/2<<endl;
}
}
PS:最后说下二分图的学习过程中遇到的问题,要熟悉掌握匈牙利算法和KM算法的过程,然后就是学会怎么构建二分图,最后还是要吐槽下自己的代码水平,还是多刷题吧,学长也说过不要只背模板而是要学会其中的思想,那我写的代码怕是失去了灵魂,哈哈不过收获还是挺多的。继续努力吧