概念
今天我们来看一个没有前几篇讲的那么常用,但是很有用的算法:匈牙利算法(Hungarian algorithm)。匈牙利算法主要用于解决一些与二分图匹配有关的问题,所以我们先来了解一下二分图。
二分图(Bipartite graph)是一类特殊的图,它可以被划分为两个部分,每个部分内的点互不相连。下图是典型的二分图。
可以看到,在上面的二分图中,每条边的端点都分别处于点集X和Y中。匈牙利算法主要用来解决两个问题:求二分图的最大匹配数和最小点覆盖数。
这么说起来过于抽象了,我们现在从实际问题出发。
最大匹配问题
看完上面讲的,相信读者会觉得云里雾里的:这是啥?这有啥用?所以我们把这张二分图稍微做点手脚,变成下面这样:
现在Boys和Girls分别是两个点集,里面的点分别是男生和女生,边表示他们之间存在“暧昧关系"。最大匹配问题相当于,假如你是红娘,可以撮合任何一对有暧昧关系的男女,那么你最多能成全多少对情侣?(数学表述:在二分图中最多能找到多少条没有公共端点的边)
现在我们来看看匈牙利算法是怎么运作的:
我们从B1看起(男女平等,从女生这边看起也是可以的),他与G2有暧昧,那我们就先暂时把他与G2连接(注意这时只是你作为一个红娘在纸上构想,你没有真正行动,此时的安排都是暂时的)。
来看B2,B2也喜欢G2,这时G2已经“名花有主”了(虽然只是我们设想的),那怎么办呢?我们倒回去看G2目前被安排的男友,是B1,B1有没有别的选项呢?有,G4,G4还没有被安排,那我们就给B1安排上G4。
然后B3,B3直接配上G1就好了,这没什么问题。至于B4,他只钟情于G4,G4目前配的是B1。B1除了G4还可以选G2,但是呢,如果B1选了G2,G2的原配B2就没得选了。我们绕了一大圈,发现B4只能注定单身了,可怜。(其实从来没被考虑过的G3更可怜)
int M, N; //M, N分别表示左、右侧集合的元素数量
int Map[MAXM][MAXN]; //邻接矩阵存图
int p[MAXN]; //记录当前右侧元素所对应的左侧元素
bool vis[MAXN]; //记录右侧元素是否已被访问过
bool match(int i)
{
for (int j = 1; j <= N; ++j)
if (Map[i][j] && !vis[j]) //有边且未访问
{
vis[j] = true; //记录状态为访问过
if (p[j] == 0 || match(p[j])) //如果暂无匹配,或者原来匹配的左侧元素可以找到新的匹配
{
p[j] = i; //当前左侧元素成为当前右侧元素的新匹配
return true; //返回匹配成功
}
}
return false; //循环结束,仍未找到匹配,返回匹配失败
}
int Hungarian()
{
int cnt = 0;
for (int i = 1; i <= M; ++i)
{
memset(vis, 0, sizeof(vis)); //重置vis数组
if (match(i))
cnt++;
}
return cnt;
}
素数伴侣问题
public class Main{
/**
*
* @param map 给定的二分图,map[i][j]等于1表示i到j连通,为0则表示不连通
* @param used
* @param linked linked[j]=i 表示顶点j到顶点i连接
* @param i 当前i顶点出发,寻找增广路径
* @return 如果能找到顶点start开始的增广路径则返回true,否则返回false
*/
public boolean dfs(int[][] map,boolean[] used,int[] linked,int i){
for(int j=0;j<linked.length;j++){
if(used[j] == false && map[i][j] == 1){ // 如果j没有被访问过,并且i和j连通
used[j] = true; // 标记为访问过
if(linked[j]==-1 || dfs(map,used,linked,linked[j])){ // 如果顶点j没有到左边顶点的连接或者,顶点j连接的左边顶点i可以找到其他增广路径
linked[j] = i; // 则将顶点j和顶点i连接
return true;
}
}
}
return false;
}
/**
* 判断数i是否为素数
*/
public boolean isPrime(int num){
for(int i=2;i<=Math.sqrt(num);i++){
if(num % i == 0){
return false;
}
}
return true;
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
Main main = new Main();
int n = sc.nextInt();
List<Integer> os = new ArrayList<>();
List<Integer> js = new ArrayList<>();
// 1、分组
// 素数除了2以外都是奇数,两个数的和为奇数,则必然一个数为偶数,一个数为奇数,因此将用户输入的数字,按奇偶性分为两组
for(int i=0;i<n;i++){
int tmp = sc.nextInt();
if(tmp % 2 == 0){
os.add(tmp);
}else{
js.add(tmp);
}
}
int M = js.size();
int N = os.size();
// 2、构造二分图(确认素数伴侣)
int[][] map = new int[M][N];
for(int i=0;i<js.size();i++){
for(int j=0;j<N;j++){
// 判断数js[i]和os[j]和是否为素数,如果是,则grap[i][j] = true
if(main.isPrime(js.get(i)+os.get(j))){
map[i][j] = 1;
}
}
}
int[] linked = new int[N];
for(int k=0;k<N;k++){
linked[k] = -1;
}
int cnt = 0;
for (int i = 0; i < M; i++)
{
boolean[] used = new boolean[N];
if (main.dfs(map,used,linked,i)){
cnt++;
}
}
System.out.println(cnt);
}
}
}
参考博文:https://zhuanlan.zhihu.com/p/96229700