题意: 个村民, 个房间,第 个村民对应第 个房间都有个收益 ,且一个房间只能给一个村民,现在给这些村民分配房间,求获得的最大的收益值。
思路:很明显这是个二分图,与普通二分图不同的是这个是边带权的二分图。看题意即为求该二分图的一个完备匹配,其所有匹配边的和在所有完备匹配中最大,即为求二分图最佳完美匹配。
KM算法(专门用来求解二分图最佳完美匹配):
首先我们先对每个点赋予权值,村民为
集合,房间为
集合,则权值分别为
和
,
其满足
。初始时,其中一个集合权值全为零,另一个集合中一个点的权值为其所连边里最大边的权重。
据此,整个算法流程为:
1.初始化点权。
2.枚举一个集合
中的点。
3.对于当前枚举的点,在子图中寻找完备匹配
(子图中所有边
必须满足
)。
4.若没有找到增广路,则修改部分点权。继续第3步。若找到则继续枚举下一个点直到枚举完。
对此,重点则在于如何修改点权:
每次修改为一个固定值
,集合
上位于匹配中的点的点权
,集合
上位于匹配中的点的点权
。其余点的点权不变。
那么对于一条边
我们可以分类讨论:
均在增广路中,
则点权和与边权关系不变,边与子图关系也不变。
在,
不在,
则点权和与边权关系有变,则边与子图关系可能变化。
不在,
在,
则若边原来不在子图的修改之后也不在。
综上
的取值取第二种边的
。
所以每次修改点权都要花费
的时间单独求一遍
。
整个算法时间复杂度就达到 。
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<string>
#include<queue>
#include<algorithm>
using namespace std;
#define NUM 305
#define mst(array,val,type,Count) memset(array,val,sizeof(type)*(Count))
class Graph
{
public:
int n, g[NUM][NUM];
int wx[NUM], wy[NUM];
bool build()
{
if (scanf("%d", &n) == EOF)return false;
for (int i = 1; i <= n; ++i)
{
wx[i] = 0;
for (int j = 1; j <= n; ++j)
{
scanf("%d", &g[i][j]);
wx[i] = max(wx[i], g[i][j]);
}
}
for (int i = 1; i <= n; ++i)wy[i] = 0;
return true;
}
};
class Kuhn_Munkres : public Graph
{
private:
int y_to[NUM];
bool vis_x[NUM], vis_y[NUM];
bool dfs(const int &vertex);
int KM();
public:
void solve()
{
while(build())
printf("%d\n", KM());
}
} G;
bool Kuhn_Munkres::dfs(const int &vertex)
{
vis_x[vertex] = true;
int weight;
for (int i = 1; i <= n; ++i)
{
if (vis_y[i])continue;
weight = wx[vertex] + wy[i] - g[vertex][i];
if (weight == 0)
{
vis_y[i] = true;
if (y_to[i] == -1 || dfs(y_to[i]))
{
y_to[i] = vertex;
return true;
}
}
}
return false;
}
int Kuhn_Munkres::KM()
{
int d, ans = 0;
memset(y_to, -1, sizeof(int) * (n + 1));
for (int i = 1; i <= n; ++i)
{
mst(vis_x, false, bool, n + 1), mst(vis_y, false, bool, n + 1);
while (!dfs(i))
{
d = -1;
for (int j = 1; j <= n; ++j)
{
if (!vis_x[j])continue;
for (int k = 1; k <= n; ++k)
{
if (vis_y[j])continue;
d = (d == -1) ? (wx[j] + wy[k] - g[j][k]) : min(d, wx[j] + wy[k] - g[j][k]);
}
}
for (int j = 1; j <= n; ++j)
{
if (vis_x[j])wx[j] -= d;
if (vis_y[j])wy[j] += d;
}
mst(vis_x, false, bool, n + 1), mst(vis_y, false, bool, n + 1);
}
}
for (int i = 1; i <= n; ++i)ans += wx[i];
for (int i = 1; i <= n; ++i)ans += wy[i];
return ans;
}
int main()
{
G.solve();
return 0;
}
优化:
KM算法可以优化至
。这时需要用到一个
数组,用来存与集合
相对的集合
中每个节点对应的
值。所以每次修改的时候的
值即为
。
注:每次修改点权时,若是不在增广路中的点的
也要
。
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<string>
#include<queue>
#include<algorithm>
using namespace std;
#define NUM 305
#define INF 0x3f3f3f3f
#define mst(array,val,type,Count) memset(array,val,sizeof(type)*(Count))
class Graph
{
public:
int n, g[NUM][NUM];
int wx[NUM], wy[NUM];
bool build()
{
if (scanf("%d", &n) == EOF)return false;
for (int i = 1; i <= n; ++i)
{
wx[i] = -INF;
for (int j = 1; j <= n; ++j)
{
scanf("%d", &g[i][j]);
wx[i] = max(wx[i], g[i][j]);
}
}
for (int i = 1; i <= n; ++i)wy[i] = 0;
return true;
}
};
class Kuhn_Munkres : public Graph
{
private:
int y_to[NUM], slack[NUM];
bool vis_x[NUM], vis_y[NUM];
bool dfs(const int &vertex);
int KM();
public:
void solve()
{
while(build())
printf("%d\n", KM());
}
} G;
bool Kuhn_Munkres::dfs(const int &vertex)
{
vis_x[vertex] = true;
int weight;
for (int i = 1; i <= n; ++i)
{
if (vis_y[i])continue;
weight = wx[vertex] + wy[i] - g[vertex][i];
if (weight == 0)
{
vis_y[i] = true;
if (y_to[i] == -1 || dfs(y_to[i]))
{
y_to[i] = vertex;
return true;
}
}
else
slack[i] = min(slack[i], weight);
}
return false;
}
int Kuhn_Munkres::KM()
{
int d, ans = 0;
memset(y_to, -1, sizeof(int) * (n + 1));
for (int i = 1; i <= n; ++i)
{
mst(slack, INF, int, n + 1), mst(vis_x, false, bool, n + 1), mst(vis_y, false, bool, n + 1);
while (!dfs(i))
{
d = INF;
for (int j = 1; j <= n; ++j)
{
if (vis_y[j])continue;
d = min(d, slack[j]);
}
for (int j = 1; j <= n; ++j)
{
if (vis_x[j])wx[j] -= d;
if (vis_y[j])wy[j] += d;
else slack[j] -= d;
}
memset(vis_x, false, sizeof(bool) * (n + 1)), memset(vis_y, false, sizeof(bool) * (n + 1));
}
}
for (int i = 1; i <= n; ++i)ans += wx[i];
for (int i = 1; i <= n; ++i)ans += wy[i];
return ans;
}
int main()
{
G.solve();
return 0;
}
KM算法应用于解决最佳完美匹配,若不是求完备匹配的最大权匹配,需要把不存在的边都添上,新添的边的边权赋为0即可。
而若是求最小权完备匹配,则把边权全部取相反数即可。最后求出来的结果再取反即可。