POJ3041 二分图的最小覆盖点问题——匈牙利算法
光是完全理解算法就花了几天时间。。基础还是太差了。。后悔大一没好好听课
算法背景
https://blog.csdn.net/jeryjeryjery/article/details/79596922
https://blog.csdn.net/qq_25379821/article/details/83721379
https://cloud.tencent.com/developer/article/1346205
最小覆盖数==最大匹配数,因此先从最大匹配问题开始分析:
朴素的递归思想比较容易理解:一句话,能让别人让就让。
如何从增广路径的角度去考虑,不好理解。在寻找增广路径时,“能让别人让就让”反映为被匹配的那个节点没有和其它人已匹配或者和该节点匹配的其他人能找到人匹配(按照递归的写法若该节点被判断为可以匹配,事实上增广路径此时已经找到,这种写法等价于深度优先地搜索一条交替路,而跳出递归的条件恰恰是这条交替路成为了增广路,在回溯的过程中会完成一系列“反色”操作,匹配数加一,同时起始的未匹配点也完成了匹配),下面要做的就是遍历所有的增广路,每找到一条新的增广路匹配数就会加一。因为一个未匹配点为起点对应的增广路只有一条(结合“能让别人让就让”来思考,一个未匹配点作为起点一次的话,“反色”后就会变成匹配点,而根据增广路的寻找规则,即使接下来再有其他增广路途经这个点,这个点也不会变成未匹配点,自然也不会再作为起点),再加上二分图的匹配实际上是一一对应的,所以只需对数量较少的那个部的所有点进行一次寻找增广路操作,此时就已经找到了最大匹配。
在增广路的寻找过程中,深搜和广搜的效率是接近的,但是深搜更好理解一些。邻接矩阵的复杂度要比邻接表高。
核心代码
bool dfs(int u) {
for (int v = 0; v < vN; v++) {
if (g[u][v] && !used[v]) {//匹配到第一个空闲的节点,其实就是深搜的标准操作
used[v] = true;
if (linker[v] == -1 || dfs(linker[v])) {//被匹配的那个节点没有和其它人已匹配或者和该节点匹配的其他人能找到人匹配
linker[v] = u;
return true;
}
}
}
return false;
}
对vis数组的理解
跳出递归有两种可能:return false 或者是 return true。return false的话意味着这条尝试找到的增广路失败了,但是一路走来没有改变任何匹配关系(要改变的话一定要return true,因此这个vis实际上完成了两个功能:保证当前尝试的路径上的所有点下次再寻找时都不会再次找到,一是防止死循环,二是避免了重复的无谓劳动);return true的话找到了增广路,可以直接下班了。(之前老是觉得有可能中途某个节点的可匹配性可以改变,因此不能理解,其实只要每个节点遍历一次就好了
题目分析
http://poj.org/problem?id=3041
https://www.jianshu.com/p/b4c5fb3439d4
本题将行与列视为点,每个障碍物点视为边,问题转化为求一个最小覆盖。
代码
#include<iostream>
#include<string.h>
using namespace std;
const int MAXN = 550;
int N,K,R,C;
int mat[MAXN][MAXN],vis[MAXN],match[MAXN];
int dfs(int x){
for(int i = 1;i <= N;i++){
if(mat[x][i] && !vis[i]){
vis[i] = 1;
if(match[i]==-1||dfs(match[i])){
match[i] = x;
return 1;
}
}
}
return 0;
}
int main(){
cin>>N>>K;
memset(mat,0,sizeof(mat));
memset(match,-1,sizeof(match));
for(int i = 0;i < K;i++){
cin>>R>>C;
mat[R][C] = 1;
}
int cnt = 0;
for(int i = 1;i <= N;i++){
memset(vis,0,sizeof(vis));
if(dfs(i)) cnt++;
}
cout<<cnt;
}
1840K 63MS