匈牙利算法

概念

今天我们来看一个没有前几篇讲的那么常用,但是很有用的算法:匈牙利算法(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值