二分图匹配
相关概念
无向二分图\(G(U\bigcup V,E)\):U是一个顶点集合,V是另一个顶点集合,对于一个集合内的点无边直接相连,而对于不同集合的点可以连边,即\((u,v)\in E\)。
匹配:两两不含公共端点的边的集合M称为匹配(就是两个集合之间连的边,只不过不同边的端点不能重合)
最大匹配:元素最多的M,即而G中两两不含公共端点的边的集合\(M\subseteq E\)的基数\(|M|\)的最大值就是最大匹配。
完美匹配:当最大匹配的匹配数满足\(2*|M|=V\)时,称为完美匹配。形象的解释就是一各集合的所有点到另一个集合都有互不相同且唯一对应的点。(类似于函数的双射),想象一下图
增广路:设M为二分图G已匹配边的集合,若P是图G中一条连通两个未匹配顶点的路径(P的起点在X部,终点在Y部,反之亦可),并且属M的边和不属M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。 (就是连了两个还没有配对的顶点的路径,路径有一个配对边,一个非配对边交替组成)
更多详细概念解释见匈牙利部分的参考文章
最大流的方法
二分图的匹配可以看成是一种最大流问题(这谁想得出来啊)。具体过程如下:
现在有两个点集U和V,之间已经有了写连边,我们需要引入一个源,一个汇,把源跟集合U的所有点有向地连起来,把V的所有点和汇有向地连起来,就构成了一个流网络。现在由于二分图匹配的限制,一个点不能连自己内部的点(在原来的二分图中这关系已经成立),不能连两个或多个边。那么就把每个边的权值赋为一。这样边的流量只有零壹两种,要么有边,要么不连边。在上面跑最大流算法即可(具体讲解参见上篇博客)
下面是代码:
代码:
#include <iostream> #include <memory.h> #include <vector> #include <queue> #define max_n 10005 #define INF 0x3f3f3f3f using namespace std; //邻接表 struct edge { int v;//到达顶点 int cap;//最大流量 int rev;//对应反向边的编号 }; vector<edge> G[max_n]; int level[max_n];//Dinic算法用到的层次图 int iter[max_n];//当前弧优化 void add_edge(int u,int v,int cap) { G[u].push_back((edge){v,cap,G[v].size()});//最后一个表示uv这条边的反向边在G[v]里的标号 G[v].push_back((edge){u,0,G[u].size()-1}); } void bfs(int s)//处理层次图 { memset(level,-1,sizeof(level)); queue<int> que; level[s] = 0; que.push(s); while(!que.empty()) { int v = que.front(); que.pop(); for(int i = 0;i<G[v].size();i++) { edge& e = G[v][i]; if(e.cap>0&&level[e.v]<0) { level[e.v] = level[v]+1; que.push(v); } } } } int dfs(int v,int t,int f)//Dinic的dfs { if(v==t) return f; for(int& i = iter[v];i<G[v].size();i++) { edge& e = G[v][i]; if(e.cap>0&&level[e.v]==level[v]+1) { int d = dfs(v,t,min(f,e.cap)); if(d>0) { e.cap-=d; G[e.v][e.rev].cap+=d; return d; } } } return 0; } int max_flow(int s,int t)//Dinic算法 { int flow = 0; for(;;) { bfs(s); if(level[t]<0) { return flow; } memset(iter,0,sizeof(iter)); int f; while(f=dfs(s,t,INF)>0) { flow += f; } } } int N,K;//N,K为两个集合的点数 bool can[max_n][max_n];//二分图中原有的边 void solve() { //0~N-1是U中的点 //N~N+K-1是V中的点 int s = N+K; int t = s+1; for(int i = 0;i<N;i++)//s与U连边 { add_edge(s,i,1); } for(int i = 0;i<K;i++)//t与V连边 { add_edge(i+N,t,1); } for(int i = 0;i<N;i++) { for(int j = 0;j<K;j++) { if(can[i][j]) { add_edge(i,j,1);//二分图原有边的链接 } } } cout << max_flow(s,t) << endl;//求最大流即得最大匹配 } int main() { cin >> N >> K; solve(); return 0; }
匈牙利算法
这个算法是专门处理二分图的最大匹配问题的,有很好的博客讲解,下面是推荐阅读方式:
我上面的概念可能不太全,那就先来看看二分图的相关概念:
C20180630_zjf 二分图匹配——匈牙利算法和KM算法 https://blog.csdn.net/C20180630/article/details/70175814
可能还对增广路径不是很理解,什么是增广路,非配对配对交替什么的很混乱,那不妨先看看这个:
土豆钊,匈牙利算法-看这篇绝对就够了!,https://blog.csdn.net/u013384984/article/details/90718287,重点看增广路那部分
现在到了算法流程了,在正式介绍前,先有个有趣而深刻的认识,下面是十分清晰的讲解:
Dark_Scope,趣写算法系列之--匈牙利算法,https://blog.csdn.net/dark_scope/article/details/8880547
好了,该正式一点了,相信你也有了一定的了解:
二分图匹配(Bipartite Matching)问题与匈牙利算法(Hungary Algorithm),https://skywt.cn/posts/bipartite-matching/
上面的代码其实已经够清晰了,如果还想看一下,就这篇吧:
https://blog.csdn.net/C20180630/article/details/70175814
代码:
#include <iostream> #include <memory.h> #define max_n 200005 using namespace std; int n,m; int con_x[max_n]; int con_y[max_n]; int visit[max_n]; int head[max_n]; struct edge { int v; int nxt; }e[max_n<<1]; int cnt = 0; void add(int u,int v) { ++cnt; e[cnt].v = v; e[cnt].nxt = head[u]; head[u] = cnt; } int dfs(int u) { for(int i = head[u];i;i=e[i].nxt) { int v = e[i].v; if(visit[v]==0) { if(con_y[v]==-1||dfs(v))//结合上面算法流程部分的有趣博客再理解了一下这里的递归,好奇妙 { con_x[u] = v; con_y[v] = u; return 1; } } } return 0; } int Hungary() { memset(con_x,-1,sizeof(con_x)); memset(con_y,-1,sizeof(con_y)); int ans = 0; for(int i = 1;i<=n;i++) { memset(visit,0,sizeof(visit)); ans += dfs(i); } return ans; } int main() { cin >> n >> m; for(int i = 1;i<=m;i++) { int a,b,c; cin >> a >> b; add(a,b); } cout << Hungary() << endl; return 0; }
参考文章
以上