主要是
1)二分图最大匹配匈牙利和HK算法
2)二分图多重匹配
3)二分图最大权匹配KM算法
以及较难的一般图匹配带花树算法
总结:通过做题目对二分图的各种性质有了更深刻的理解
二分图不可能有奇环,所以很多算法的实现都变得简单很多,但到了一般图就很难了,嗯~~~一般图的各种匹配算法还在摸索
二分图最小点覆盖 (这一小部分是找的一篇博客https://blog.youkuaiyun.com/ACTerminate/article/details/78937924里的内容)
定义:点覆盖是图中一些点的集合,且对于图中所有的边,至少有一个端点属于点覆盖,点数最小的覆盖就是最小点覆盖。
定理:最小点覆盖=最大匹配。
简单证明:首先必然有,最小覆盖>=最大匹配。于是只要证明不等式可以取等号,我们可在最大匹配的基础上构造出一组点覆盖。对右边每一个未匹配的点进行dfs找增广路,标记所有dfs过程中访问到的点,左边标记的点+右边未标记的点就是这个图的一个点覆盖。因为对于任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖。再来证明:最大匹配=左边标记的点+右边未标记的点。对于每条匹配边,只有一个点属于点覆盖。如果这条边在dfs过程中被访问了,那么就左端点属于点覆盖,右端点不属于,否则就有左端点不属于点覆盖,右端点属于点覆盖。除此之外,不可能存在其它的点属于最小覆盖了,不然就必然可以找到增广路。所以:左边标记的点+右边未标记的点=最大匹配,对于任意的二分图,我们总能在最大匹配的基础上构造出一组点数等于最大匹配的点覆盖,所以:最小点覆盖=最大匹配。
二分图最小边覆盖
定义:边覆盖是图中一些边的集合,且对于图中所有的点,至少有一条集合中的边与其相关联,边数最小的覆盖就是最小边覆盖。
定理:最小边覆盖=图中点的个数-最大匹配。
简单证明:先贪心的选一组最大匹配的边加入集合,对于剩下的每个未匹配的点,随便选一条与之关联的边加入集合,得到的集合就是最小边覆盖,所以有:最小边覆盖=最大匹配+图中点的个数-2*最大匹配=图中点的个数-最大匹配。
二分图最大独立集
定义:独立集是图中一些点的集合,且图中任意两点之间都不存在边,点数最大的就是最大独立集。
定理:最大独立集=图中点的个数-最大匹配。
简单证明:可以这样来理解,先把所有的点都加入集合,删除最少的点和与其关联的边使得剩下的点相互之间不存在边,我们就得到了最大独立集。所以有:最大独立集=图中点的个数-最小点覆盖=图中点的个数-最大匹配。
有向无环图最小不相交路径覆盖
定义:用最少的不相交路径覆盖所有顶点。
定理:把原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By。这样就得到了一个二分图,最小路径覆盖=原图的节点数-新图最大匹配。
简单证明:一开始每个点都独立的为一条路径,总共有n条不相交路径。我们每次在二分图里加一条边就相当于把两条路径合成了一条路径,因为路径之间不能有公共点,所以加的边之间也不能有公共点,这就是匹配的定义。所以有:最小路径覆盖=原图的节点数-新图最大匹配。
有向无环图最小可相交路径覆盖
定义:用最小的可相交路径覆盖所有顶点。
算法:先用floyd求出原图的传递闭包,即如果a到b有路,那么就加边a->b。然后就转化成了最小不相交路径覆盖问题。
hdu 1045
题目:同一行同一列只能放一个棋子,除非同一行(列)的两个棋子之间有障碍,问最多能放多少个障碍
解:缩点建图,作为第一个匹配题确实建图有点难搞
我是这么理解的:一般的行列建图是说一行对应一列只能放一个,没有障碍的情况下应该这样建图,有障碍的话,障碍把一行(列)分成了几部分,这几部分每一部分都可能可以放一颗棋子进去,所以把最多只能放一颗棋子的一团点缩成一个进行匹配
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1010;
int n ,cntx ,cnty ,cx[maxn] ,cy[maxn] ,tmp[15][15] ;
char s[15][15] ;
bool vis[maxn] ,flag[maxn][maxn];
void build()
{
memset(flag,0,sizeof flag);
memset(tmp,0,sizeof tmp);
cntx = 1;
for(int i = 0;i < n;++i)
{
for(int j = 0;j < n; ++j)
if(s[i][j] == 'X') ++cntx; else tmp[i][j] = cntx;
if(s[i][n - 1]!='X') ++cntx;
}
cnty = 1;
for(int j = 0;j < n; ++j)
{
for(int i = 0;i < n;++i)
if(s[i][j] == 'X') ++cnty; else flag[tmp[i][j]][cnty] = 1;
if(s[n - 1][j] != 'X') ++cnty;
}
}
bool dfs(int u)
{
for(int v = 1;v <= cnty; ++v)
if(!vis[v] && flag[u][v])
{
vis[v] = 1;
if(!cy[v] || dfs(cy[v]))
{
cx[u] = v ,cy[v] = u ;
return 1;
}
}
return 0;
}
void solve()
{
memset(cx,0,sizeof cx);
memset(cy,0,sizeof cy);
int ans = 0;
for(int i = 1;i <= cntx;++i)
if(!cx[i])
{
memset(vis,0,sizeof(vis));
ans += dfs(i);
}
printf("%d\n",ans);
}
int main()
{
while(~scanf("%d",&n) && n)
{
for(int i = 0;i < n;++i) scanf("%s",s[i]);
build();
solve();
}
return 0;
}
hdu 2444
题目:有一些同学和一些认识关系(不具有传递性),问能否把他们分成两部分使得每一部分的人内部彼此都不认识,如果可以从两组中各找一个人安排双人间住,要求这两个人彼此认识,问最多能安排几个双人间
解:判断能否分成两组实际上就是判断图是否是二分图,常用的是染色法 DFS/BFS两种姿势,然后求最大匹配即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
///二分图判定+匹配
bool g[300][300],used[300];
int ans,n,m,x,y,linker[700],f[300],vist[300];
bool dfs(int u)
{
for(int v = 0;v < n; ++v)
{
if(g[u][v] && !used[v])
{
used[v] = 1;
if(linker[v] == -1 || dfs(linker[v]))
{
linker[v] = u;
return 1;
}
}
}
return 0;
}
int hungary()
{
int res = 0;
memset(linker,-1,sizeof(linker));
for(int u = 0;u < n; u++)
{
memset(used,0,sizeof(used));
if(dfs(u)) res++;
}
return res;
}
int isTwo()
{
queue<int>q;
memset(vist,0,sizeof(vist));
q.push(0); vist[0] = 1;
while(!q.empty())
{
int p = q.front(); q.pop();
for(int j = 0;j < n;j++)
if(g[p][j]) ///ab间有边
{
if(!vist[j]) vist[j] = -vist[p],q.push(j);
else if(vist[j] == vist[p]) return 0;
}
}
return 1;
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
memset(g,0,sizeof(g));
for(int i = 0;i < m; ++i)
{
scanf("%d%d",&x,&y);
g[x - 1][y - 1] = g[y - 1][x - 1] = 1;
}
if(!isTwo()) printf("No\n");
else printf("%d\n",hungary() / 2);
}
return 0;
}
hdu 2389
题目就是一个裸二分匹配问题,关键再于ngeng的范围有3000,一般的匈牙利算法只能处理差不多1000左右的数据,所以我们只能用更快一点的HK算法来解决了
#include<iostream>
#include<string.h>
#include<vector>
#include<math.h>
#include<queue>
#define ll long long
using namespace std;
typedef pair<int,int> P;
const int maxn = 6050;//存的点最多是n+m
int vis[maxn];
struct node{
int v,next;
}e[maxn*1500];
int cnt,n,m,p,ca = 0,T,t;
int head[maxn],con[maxn],dep[maxn];;
int x[maxn],y[maxn],s[maxn];
void add(int u,int v)
{
e[cnt].v = v;
e[cnt].next = head[u];
head[u] = cnt++;
}
void init()
{
cnt = 0;
memset(head,-1,sizeof head);
memset(vis,0,sizeof vis);
memset(con,-1,sizeof con);
cin >> t >> n;
for(int i = 1;i <= n;++i) scanf("%d%d%d",&x[i],&y[i],&s[i]);
cin >> m;
for(int i = 1;i <= m;++i) scanf("%d%d",&x[i + n],&y[i + n]);
for(int i = 1;i <= n;++i)
for(int j = 1;j <= m;++j)
{
int w = (x[i] - x[j + n]) * (x[i] - x[j + n]) + (y[i] - y[j + n])*(y[i] - y[j + n]);
int temp = t * s[i] * t * s[i];
if(temp >= w) add(i,j + n);
}
}
bool bfs()
{
memset(dep,0,sizeof dep);
queue<int> q;
while(q.size()) q.pop();
for(int i = 1;i <= n;++i)
if(con[i] == -1) q.push(i);
bool flag = 0;
while(q.size())
{
int u = q.front();q.pop();
for(int i = head[u];i != -1; i = e[i].next)
{
if(!dep[e[i].v])
{
dep[e[i].v] = dep[u] + 1;
if(con[e[i].v] == -1) flag = 1;
else dep[con[e[i].v]] = dep[e[i].v] + 1,q.push(con[e[i].v]);
}
}
}
return flag;
}
bool dfs(int u)
{
for(int i = head[u];i != -1; i = e[i].next)
{
int v = e[i].v;
if(dep[v] != dep[u] + 1) continue;
dep[v] = 0;
if(con[v] == -1 || dfs(con[v]))
{
con[u] = v;
con[v] = u;
return 1;
}
}
return 0;
}
void sol()
{
int ans = 0;
while(bfs())
for(int i = 1;i <= n;++i) if(con[i] == -1 && dfs(i)) ans++;
printf("Scenario #%d:\n%d\n\n",++ca,ans);
}
int main()
{
cin>>T;
while(T--)
{
init();
sol();
}
}
hdu 3829
题目:动物园有n只猫m条狗,小朋友喜欢猫就一定讨厌狗,如果能把这个小朋友喜欢的动物留下并移走他不喜欢的动物,他就会很开心,问最多能让多少个小朋友很开心?
解:这个题的建图很有意思,
开始想的是这个小朋友喜欢猫a讨厌狗b就从猫a连一条边向狗b
但是样例给出了提示,不同小朋友喜欢和讨厌的可能都相同,换句话说如果把喜欢的和不喜欢的连边,可能会有重边(例如两个小朋友都喜欢猫a讨厌狗b,就会重复从猫a连一条边向狗b,所以不可行
怎么办呢,我们把喜欢猫A的小朋友和讨厌猫A的小朋友连边,表示构成矛盾,他们中只能选一个,然后就是最大独立集了
#include<bits/stdc++.h>
using namespace std;
bool used[510];
int n,m,x[510],y[510],linker[510],T,k,tot;
vector<int>g[510];
bool dfs(int u)
{
for(int v = 0;v < g[u].size(); ++v)
{
if(!used[g[u][v]])
{
used[g[u][v]] = 1;
if(linker[g[u][v]] == -1 || dfs(linker[g[u][v]]))
{
linker[g[u][v]] = u;
return 1;
}
}
}
return 0;
}
int hungary()
{
int res = 0;
memset(linker,-1,sizeof(linker));
for(int u = 1;u <= k; u++)
{
memset(used,0,sizeof(used));
if(dfs(u)) res++;
}
return res;
}
int main()
{
while(~scanf("%d%d%d",&n,&m,&k))
{
for(int i = 1;i <= k; ++i) g[i].clear();
char ch1,ch2;
for(int i = 1;i <= k; ++i)
{
getchar();
scanf("%c%d %c%d",&ch1,&x[i],&ch2,&y[i]);
if(ch1 == 'D') x[i] += n;
if(ch2 == 'D') y[i] += n;
}
for(int i = 1;i <= k; ++i)
for(int j = 1;j <= k; ++j)
{
if(x[i] == y[j] || x[j] == y[i])
{
g[i].push_back(j);
g[j].push_back(i);
}
}
printf("%d\n",k - hungary() / 2);
}
return 0;
}
M-O 的三个多重匹配都是结合二分答案做的qaq
N题POJ 2112
有k个挤奶机和c头奶牛,给出他们之间的距离,每个挤奶机最多容纳m头奶牛,问如何分配能使走的最远的那头奶牛的路径长尽可能短
首先二分路径长度,check的时候,把小于等于这个路径的点连边,跑一边多重匹配看是否合法
需要注意的是题目说奶牛在去挤奶机的路上可以走过好几条路
所以要跑一遍floyed找最短的路走,所以floyed跑完后最长的路应该作为二分答案的上界,而不是200,呜呜呜,不如直接用inf省的麻烦
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn = 1010;
int mapp[maxn][maxn],a[maxn][maxn];
int n,m,k,c,ans;
bool used[maxn];
struct node
{
int cnt; ///y节点的匹配的个数
int k[maxn];///和y匹配的x的集合数
}linker[maxn];
bool dfs(int u)
{
for(int i = 1;i <= k;i++)
if(!used[i] && mapp[i][u])
{
used[i] = 1;
if(linker[i].cnt < m) ///如果当前y节点还有空间可以匹配
{
linker[i].k[linker[i].cnt++] = u;
return 1;
}
for(int j = 0;j < linker[i].cnt;j ++)///如果y节点的容量已满,尝试换掉y节点的某个对象
if(dfs(linker[i].k[j]))
{
linker[i].k[j] = u;///y节点的第j个位置让给x
return 1;
}
}
return 0;
}
bool hungary()
{
memset(linker,0,sizeof(linker));
for(int i = k + 1;i <= k + c;i ++)///为所有的x节点匹配对象
{
memset(used,0,sizeof(used));
if(!dfs(i)) return 0;
}
return 1;
}
bool check(int x)
{
memset(mapp,0,sizeof(mapp));
for(int i = 1;i <= k; ++i)
for(int j = k + 1;j <= k + c; ++j)
if(a[i][j] <= x && a[i][j])
mapp[i][j] = 1;
return hungary();
}
int main()
{
scanf("%d%d%d",&k,&c,&m);
int l = 0,r = 0x3f3f3f3f;
for(int i = 1;i <= k + c; ++i)
for(int j = 1;j <= k + c; ++j)
scanf("%d",&a[i][j]);
for(int l = 1;l <= k + c; l++)
for(int i = 1;i <= k + c; i++)
for(int j = 1;j <= k + c; j++)
if(a[i][l] && a[l][j])
{
if(!a[i][j] || a[i][j] > a[i][l] + a[l][j])
a[i][j] = a[i][l] + a[l][j];
}
ans = r;
while(r >= l)
{
int mid = (l + r) / 2;
if(check(mid))
{
ans = min(ans,mid);
r = mid - 1;
}
else l = mid + 1;
}
printf("%d\n",ans);
return 0;
}
O题POJ 3189
理解错题意好难受,有n个奶牛,b个谷仓,每个奶牛对谷仓都有一个排名,问如何分配谷仓能使排名最高的和排名最低的之间的差值尽可能小
解:虽说数据比较小,但是单纯枚举上下界还是会超时的,所以就二分差值,然后从小到大枚举下界就OK,很多人说EK超时,没有的事,毕竟二分是王道嘛
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn = 1010;
int mapp[maxn][maxn],a[maxn][maxn],num[maxn];
int n,m,k,c,ans;
bool used[maxn];
struct node
{
int cnt; ///y节点的匹配的个数
int k[maxn];///和y匹配的x的集合数
}linker[maxn];
bool dfs(int u)
{
for(int i = 1;i <= m;i++)
if(!used[i] && mapp[u][i])
{
used[i] = 1;
if(linker[i].cnt < num[i]) ///如果当前y节点还有空间可以匹配
{
linker[i].k[linker[i].cnt++] = u;
return 1;
}
for(int j = 0;j < linker[i].cnt;j++)///如果y节点的容量已满,尝试换掉y节点的某个对象
if(dfs(linker[i].k[j]))
{
linker[i].k[j] = u;///y节点的第j个位置让给x
return 1;
}
}
return 0;
}
bool hungary()
{
memset(linker,0,sizeof(linker));
for(int i = 1;i <= n;i ++)///为所有的x节点匹配对象
{
memset(used,0,sizeof(used));
if(!dfs(i)) return 0;
}
return 1;
}
bool check(int x,int y)
{
memset(mapp,0,sizeof(mapp));
for(int i = 1;i <= n; ++i)
for(int j = x;j <= min(y,m);++j)
mapp[i][a[i][j]] = 1;
return hungary();
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= n; ++i)
for(int j = 1;j <= m; ++j)
scanf("%d",&a[i][j]);
for(int i = 1;i <= m; ++i) scanf("%d",&num[i]);
int l = 0,r = m,ans = m;
while(r >= l)
{
int mid = (l + r) / 2;
bool flag = 0;
for(int i = 1;i <= m; ++i)
if(check(i,mid + i - 1))
{
ans = min(ans,mid);
flag = 1;
break;
}
if(flag) r = mid - 1;
else l = mid + 1;
}
printf("%d\n",ans);
return 0;
}
floyed传递闭包+匈牙利最大匹配
n个点,m条单行道,机器人不能往回走,问最少几个机器人能把所有的点都走完
注意:两个机器人可能经过同一条边
区分最小相交路径覆盖和最小不相交路径覆盖,解决方案前面已经提到过就不多说了,直接上代码
#include<cstdio>
#include<cstring>
using namespace std;
int T,tot,n,m,linker[1010];
bool used[1010],g[1010][1010];
bool dfs(int u)
{
for(int v = 1;v <= n; ++v)
{
if(g[u][v] && !used[v])
{
used[v] = 1;
if(linker[v] == -1 || dfs(linker[v]))
{
linker[v] = u;
return 1;
}
}
}
return 0;
}
int hungary()
{
int res = 0;
memset(linker,-1,sizeof(linker));
for(int u = 1;u <= n; u++)
{
memset(used,0,sizeof(used));
if(dfs(u)) res++;
}
return res;
}
int main()
{
while(~scanf("%d%d",&n,&m) && n + m)
{
for(int i = 1;i <= n; ++i)
for(int j = 1;j <= n; ++j)
g[i][j] = 0;
for(int i = 0;i < m; ++i)
{
int x,y;
scanf("%d%d",&x,&y);
g[x][y] = 1;
}
for(int k = 1;k <= n; ++k)
for(int i = 1;i <= n; ++i)
for(int j = 1;j <= n; ++j)
if(g[i][k] && g[k][j])
g[i][j] = 1;
printf("%d\n",n - hungary());
}
return 0;
}