您的位置:首页 > 其它

谈谈匈牙利算法

2015-05-12 22:56 232 查看
最近学习了图论的一个新算法——匈牙利算法(感觉图论真是太有趣了!逃

自己也尝试过了几道POJ的题目,为避免以后忘记,特地写一下总结,顺便加深理解。

什么是二分图?什么是二分图匹配?等等这些问题这里并不会说明,您可以谷歌一下绝对是一大把。因为这篇博文的目的主要是为了加深理解,所以这里假定大家都或多或少知道匈牙利算法的原理或实现。

简单来说,匈牙利算法可以用来求二分图的最大匹配问题,包括最大独立点集、最小边/点覆盖等等都能用此算法来求解。

匈牙利算法的核心便是寻找增广路径。做一下并不严谨的解释:增广路径的起点和终点都是还没匹配过的点,且路径为已匹配边和未匹配边交替出现。这样我们依次一个集合(二分图自然就是两个集合)的每个顶点出发寻找增广路,如果发现此路径存在,很明显匹配数会加一。不断执行此操作,直到找不到这样的路径。

具体实现的大致流程为:从点k出发寻找增广路径 ——> 寻找能连接的点j ——> 若j不在此时的这条增广路上 ——> 把j加入增广路 ——> 若j是未匹配过的点或从j出发有增广路 ——> match[j] =k ——> return true。

刚开始我对这个算法有两个严重不理解的点:

1.为什么是从其中一个集合每一个顶点开始找?不是得从未匹配的开始找吗?

2.“依次”为什么可以保证正确?

后来向巨巨们请教再加上认真思考了做过的一些题才真正理解了这样做的原因。

对于匈牙利算法来说有一个重要的定理:

如果从一个点A出发,没有找到增广路径,那么无论再从别的点出发找到多少增广路径来改变现在的匹配,从A出发都永远找不到增广路径。

这个如果要我证明我也不会,不过如果画下图感觉并不难理解的样子?(或者要不记住就好啦!

这个定理直接解释了第二点疑问,“依次”的确可以保证正确。这里要注意一点,就是匈牙利算法的实现保证了后面的匹配不会影响到前面的匹配(这里指匹配数),因为如果要后面的匹配改变了前面的匹配对象,那说明前面可以找到另一个匹配对象,因为只有这样才算是找到了增广路径。也因此前面的匹配依然存在,只不过对象有可能不同而已。

还有一个就是为什么可以从其中一个集合的每一个顶点开始找,因为事实上后面的点并不会因为前面点增广路径的寻找而从未匹配变成已匹配(画个图就知道了)。所以这样做的确是对的。

这里还要说一个就是,有些人喜欢把图先二分之后再来搜(或者题目已经帮你分好了),而有些人喜欢直接把所有点放在一个集合里搜,这样最后的结果是翻倍了的,要求最大匹配得除以2。反正看个人喜欢吧。这里顺便放几份题代码,透过代码也可以比较深刻地理解这些点(因为我似乎说的有点乱,逃…

Poj 3020 求的是最小边覆盖,而最小边覆盖== n - 最大匹配数。

//Antenna Placement.cpp -- poj 3020
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
const int maxh = 40 + 10;
const int maxw = 10 + 10;
int dx[] = {1, 0, 0, -1};
int dy[] = {0, 1, -1, 0};
char maze[maxh][maxw];
vector<int> G[maxh*maxw+maxw];
int match[maxh*maxw+maxw];
bool used[maxh*maxw+maxw];
int h, w;
bool dfs_match(int x)
{
for( int i=0; i<G[x].size(); i++ )
{
int j = G[x][i];
if( !used[j] )
{
used[j] = true;
if( !match[j] || dfs_match(match[j]) )
{
match[j] = x;
return true;
}
}
}
return false;
}
int hungary()
{
int sum = 0;
memset(match, 0, sizeof(match));
for( int i=0; i<=h*w-1; i++ )
{
memset(used, 0, sizeof(used));
if( dfs_match(i) )
sum++;
}
return sum;
}
int main()
{
int n;
scanf("%d", &n);
while( n-- )
{
scanf("%d %d", &h, &w);
for( int i=0; i<h; i++ )
scanf("%s", maze[i]);
for( int i=0; i<=h*w-1; i++ )
G[i].clear();
int qx = 0;
for( int i=0; i<h; i++ )
{
for( int j=0; j<w; j++ )
{
if( maze[i][j]=='*' )
{
qx++;
for( int k=0; k<4; k++ )
{
int x = i + dx[k];
int y = j + dy[k];
if( 0<=x && x<h && 0<=y && y<w && maze[x][y]=='*' )
G[i*w+j].push_back(x*w+y);
}
}
}
}
int H = hungary() / 2;	// 得除以2。
printf("%d\n", qx - H);
}
return 0;
}


Poj 3041 求的是最小点覆盖,而最小点覆盖==最大匹配数。注意建图的时候是i和j连边,可以想一下为什么,感觉这样连边非常巧妙!

//Asteroids.cpp -- poj 3041
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <queue>
#include <map>
typedef long long ll;
using namespace std;
const int maxn = 500 + 10;
int n, k;
bool used[maxn];
int match[maxn];
vector<int> G[maxn];
bool dfs(int v)
{
for( int i=0; i<G[v].size(); i++ )
{
int u = G[v][i];
int w = match[u];
if( !used[u] )
{
used[u] = 1;
if( w<0 ||  dfs(w) )
{
match[u] = v;
return true;
}
}
}
return false;
}
int bipartite_matching()
{
int stal = 0;
memset(match, -1, sizeof(match));
for( int i=1; i<=n; i++ )
{
memset(used, 0, sizeof(used));
if( dfs(i) )
stal++;
}
return stal;
}
int main()
{
int T;
scanf("%d", &T);
while( T-- )
{
scanf("%d %d", &n, &k);
int a, b;
for( int i=1; i<=n; i++ )
G[i].clear();
for( int i=0; i<k; i++ )
{
scanf("%d %d", &a, &b);
G[a].push_back(b);
}
int kry = bipartite_matching();
printf("%d\n", kry);
}

return 0;
}


Acdream 1729 先判断是否为二分图,再求最大匹配。感觉这题关键是读懂题意,然而我虽然AC,但还是不能理解题目在说什么…

//Crime.cpp -- Acdream 1729
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <cmath>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
const int maxn = 300 + 10;
const int maxm = 10 + 10;
char a[maxm], b[maxm];
map<string, int> mp;
int colour[maxn], match[maxn];
//vector<int> G[maxn];
int G[maxn][maxn];
bool used[maxn];
//bool fuck[maxn][maxn];
int n, m;
bool dfs_graph(int x, int y)
{
colour[x] = y;
for( int i=1; i<=n; i++ )
{
if( G[x][i] )
{
if( !colour[i] && !dfs_graph(i, -y)) return false;
else if( colour[i]==y ) return false;
}
}
return true;
}
bool dfs_match(int x)
{
for( int i=1; i<=n; i++ )
{
if( G[x][i] )
{
if( !used[i] )
{
used[i] = true;
if( !match[i] || dfs_match(match[i]) ) // 若k已有匹配,则从匹配k的点出发搜增广路
{
match[i] = x;
return true;
}
}
}
}
return false;
}
int hungary()
{
int ans = 0;
memset(match, 0, sizeof(match));
for( int i=1; i<=n; i++ )
{
if( colour[i]==1 )	// 每次都是从x集合向y集合连边
{					// 如果懒得分边的话就直接一直搜然后结果除以2
// 不过依然保证是从x向y连边,只不过有重复点而已
memset(used, 0, sizeof(used));
if( dfs_match(i) ) ans++;
}
}
return ans;
}
int main()
{
while( ~scanf("%d %d", &n, &m) )
{
bool flag = true;
memset(colour, 0, sizeof(colour));
memset(G, 0, sizeof(G));
//memset(fuck, 0, sizeof(fuck));
//for( int i=1; i<=n; i++ ) G[i].clear();
mp.clear();
for( int i=0, j=1; i<m; i++ )
{
scanf("%s %s", a, b);
string aa = a, bb = b;
if( !mp[aa] ) mp[aa] = j++;
if( !mp[bb] ) mp[bb] = j++;
/*
if( !fuck[mp[aa]][mp[bb]] )
{
G[mp[aa]].push_back(mp[bb]);
G[mp[bb]].push_back(mp[aa]);
fuck[mp[aa]][mp[bb]] = 1;
fuck[mp[bb]][mp[aa]] = 1;
}
*/
G[mp[aa]][mp[bb]] = G[mp[bb]][mp[aa]] = 1;
}
for( int i=1; i<=n; i++ )
{
if( !colour[i] )
{
if( !dfs_graph(i, 1) )
{
flag = false;
printf("No\n");
break;
}
}
}
if( flag )
{
int ans = hungary();
printf("%d\n", ans*2);
}
}
return 0;
}


下面几道都是理(zui)论(ba)AC题:

下面两份AC代码都是华师的同学放出来。

华师校赛决赛A题Goddess Hunter

题目大意:有n个boy和m个girl,每个boy有一个权值,求一个boy和girl的匹配,使得匹配的boy的权值和最大。

比赛的时候并没有想出怎么搞,看完题解才注意到很重要的一个点,那就是每个boy只有一个权值,也就是无论与哪个girl匹配权值都是一样的,这样就可以直接匈牙利搞出来了(当然你也可以用最大流或者KM什么的搞定):

把boys按其给大师的钱,从大到小排序。然后用匈牙利求最大匹配,因为我们总是先匹配给钱多的boy,在用匈牙利算法的情况下,给钱少的boy是没有办法干掉给钱多的boy的。此时有匹配边的boys给大师的钱的总数即为答案。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

const int MAXV = 310;
const int MAXE = MAXV * MAXV;
const int INF = 0x3f3f3f3f;

bool vis[MAXV];
int mat[MAXV][MAXV], link[MAXV], id[MAXV];
int money[MAXV];
int n, m, T;

bool dfs(int u) {
if(vis[u]) return false;
vis[u] = true;
for(int v = 1; v <= m; ++v) if(mat[u][v]) {
if(!link[v] || dfs(link[v])) {
link[v] = u;
return true;
}
}
return false;
}

bool cmp(int a, int b) {
return money[a] > money[b];
}

int hungry() {
for(int i = 1; i <= n; ++i) id[i] = i;
sort(id + 1, id + n + 1, cmp);

memset(link + 1, 0, m * sizeof(int));
int res = 0;
for(int i = 1; i <= n; ++i) {
memset(vis + 1, 0, n * sizeof(bool));
if(dfs(id[i])) res += money[id[i]];
}
return res;
}

int main() {
scanf("%d", &T);
while(T--) {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i)
scanf("%d", &money[i]);
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
scanf("%d", &mat[i][j]);
printf("%d\n", hungry());
}
}


华师校赛决赛C题Matrix

题目大意:有一个n*n矩阵,矩阵上的元素要么为U,要么为D,要求能否通过任意次数的行变换或者列变换来使得主对角线上全为U。虽然很快就想出来成立的条件取决于是否对于每一列都有一个U,且各自在不同行(你反过来说也一样)。然后dfs就一直Tle到结束(虽然似乎是早已料到?)。

其实这道题同样可以用二分图最大匹配来做,具体实现类似上面的Poj 3041,也是通过i和j的连边来求最大匹配,而U表示可以对应的行列可以连边,然后因为对于每一行只能有一个列来与之对应,所以只要判断最大匹配数是否等于n就可以了。(为什么我没想到QwQ…

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int MAXV = 201;
const int MAXE = 40010;

int head[MAXV], to[MAXE], Next[MAXE], ecnt;
int link[MAXV];
bool vis[MAXE];

void init_edge(){
ecnt = 0;
memset(head, -1, sizeof(head));
memset(link, 0, sizeof(link));
}
void add_edge(int u, int v){
Next[ecnt] = head[u]; to[ecnt] = v; head[u] = ecnt++;
}

bool dfs(int u){
for (int i = head[u]; i != -1; i = Next[i]){
int v = to[i];
if (vis[v]) continue; vis[v] = true;

if (!link[v] || dfs(link[v])){
link[v] = u;
return true;
}
}
return false;
}

int n, ans;
char c;

int main(){
while (scanf("%d", &n)!= EOF){
init_edge();
for (int i = 1; i <= n; ++i){
getchar();
for (int j = 1; j <= n; ++j){
c = getchar();
if (c == 'U') add_edge(i, j);
}
}

ans = 0;
for (int i = 1; i <= n; ++i){
memset(vis, false, sizeof(vis));
if (dfs(i)) ++ans;
}

if (ans == n) puts("YES");
else puts("NO");
}
return 0;
}


以上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: