LeetCode第280场周赛 - 数组的最大与和 - 匈牙利算法/KM算法

LeetCode第280场周赛 - 数组的最大与和 - 匈牙利算法/KM算法

关键词: leetcode | 280 周赛 | KM | 匈牙利算法

题目:

解题思路:

  • 题目的本质是将数组nums中的n个数字分配到不同的篮子中,每个数字分配到每个篮子时都有一个权值(按位与运算结果值),需要将整体分配方案的权值和最大化。
  • 可以明显地看出是两类物品的匹配问题,又涉及权重的最大化,故可将题目抽象为一个二分图最大权匹配问题。
  • 先考虑最朴素的方法,将每个数字和每个篮子的匹配方案都枚举一遍,计算出每种方案的权值和后比较获得最大值即可。算法时间复杂度为 O ( ( n ∗ n u m S l o t s ) ! ) O((n*numSlots)!) O((nnumSlots)!)。(当然是不可行的)
  • 当然,二分图匹配问题毕竟很经典了,如果能想到二分图匹配问题,就应当能想到可能存在已有算法(但是当然没学习过相关算法的话也很难现场想出解决算法)。实际上确实存在已有算法——KM算法,能够解决二分图最大权匹配问题,即找到二分图匹配方案中的权值和最大方案。
  • 那么先上代码,再慢慢解释KM算法的原理(实际上我也只是听说过该算法,这次也好好学习一下)

代码:

class Solution {

    // 建图
    void buildGraph(vector<vector<int>>& edges, vector<int>& nums, int numSlots) {
        int n = nums.size();
        edges = vector<vector<int>>(n,vector<int>(2*numSlots));
        for(int i=0; i<n; i++) {
            for(int j=0; j<numSlots; j++) {
                edges[i][j] = edges[i][j+numSlots] = (nums[i]&(j+1));   // j,j+numSlots 分别表示编号为 j+1 的篮子的两个空位
            }
        }
    }

    // KM算法寻找最大权匹配
    // 在匈牙利算法的基础上增加期望度机制。
        // 初始化左半图每个number的期望度为该number最大可获得权值,右半图每个slot的期望度为0。
        // 每次匹配时,只考虑边权等于相连number和slot期望度和的边。
        // 当一次匹配中无法成功时,执行期望度更新操作:将涉及的所有number期望度减一、所有slot期望度加一。
    int KM(vector<vector<int>>& edges) {
        int n = edges.size(), m = edges[0].size();  // 二分图两边的点数
        vector<int> match(m, -1); // match[i]为teacher[i]匹配的student编号
        vector<int> exNumber(n); // number期望
        vector<int> exSlot(m, 0); // slot的期望
        for (int i = 0; i < n; ++i) {
            exNumber[i] = *max_element(edges[i].begin(), edges[i].end());
        }
        // 为每个number匹配teacher
        for (int i = 0; i < n; ++i) {
            while(true) {
                vector<bool> visNumber(n, false);
                vector<bool> visSlot(m, false);
                if (dfs(i, m, edges, match, visNumber, visSlot, exNumber, exSlot)) break;
                // 无法匹配降低期望
                for (int j = 0; j < n; ++j) {
                    if (visNumber[j]) exNumber[j]--;
                }
                for (int j = 0; j < m; ++j) {
                    if (visSlot[j]) exSlot[j]++;
                }
            }
        }

        int ans = 0;
				// 将每一对匹配的权值加和(这里通过遍历slot的匹配情况来实现)
        for (int i = 0; i < m; ++i) if(match[i]!=-1) {
            ans += edges[match[i]][i];
        }
        return ans;
    }

    // 匈牙利算法寻找完美匹配
    // 为第i个number寻找匹配的slot。对每个可匹配slot,若已被其他number匹配,则递归为其他number寻找另外的可匹配slot;找不到则为该number匹配其他slot
    bool dfs(int i, int m, vector<vector<int>>& edges, vector<int>& match, vector<bool>& visNumber, vector<bool>& visSlot, vector<int>& exNumber, vector<int>& exSlot) {
        visNumber[i] = true;
        for (int j = 0; j < m; ++j) {
            if (visSlot[j]) continue;
            int diff = exNumber[i] + exSlot[j] - edges[i][j];
            if (!diff) {
                visSlot[j] = true;
                if (match[j] == -1 || dfs(match[j], m, edges, match, visNumber, visSlot, exNumber, exSlot)) {
                    match[j] = i;
                    return true;
                }
            }
        }
        return false;
    }
public:
    int maximumANDSum(vector<int>& nums, int numSlots) {
        /* 
         * 问题抽象为 “n个数字 - 2*numSlots个篮子” 的二分图最大匹配问题
         ** 二分图中的点:
         *** n个数字:nums中需要被分配的n个数字
         *** 2*numSlots个篮子:每个篮子最多可以放两个数字
         ** 二分图中的边:
         *** 每个数字和每个篮子间都有一条边,边权为数字与篮子编号的 按位与运算 结果,表示把该数字放到该篮子时可获得的权值
         */
        
        // 建图(用n*2numSlots的邻接矩阵表示图。因为后续算法中只需要考虑边权而不需要考虑点权,故只需保存边权信息。)
        vector<vector<int>> edges;
        buildGraph(edges,nums,numSlots);
        // 计算(使用KM算法,计算最优匹配方案权值和)
        return KM(edges);
    }
};

KM算法细节解释:

  • 建议先浏览参考文章[1]后再阅读本段,参考文章[1]中以简单的例子介绍了KM算法的整体流程,本段仅以笔者浅薄的知识做一些补充,相当于参考文章[1]的进阶版。
  • KM算法本质上在匈牙利算法的基础上增加期望度机制。
    • 初始化左半图每个number的期望度为该number最大可获得权值,右半图每个slot的期望度为0。
    • 每次匹配时,只考虑边权等于相连number和slot期望度和的边。
    • 当一次匹配中无法成功时,执行期望度更新操作:将涉及的所有number期望度减一、所有slot期望度加一。
  • 可行性:
    • 相当于限制了参与搜索的边的集合,使得权值较大的边能被优先搜索,保证了算法可行的充分性。
    • 每次降低期望度时,将本次搜索中左半图所有点期望度减1、右半图所有点期望度加1。每次降低期望度时,设涉及到的左半图点个数为 n 1 n_1 n1,则涉及到的右半图点个数为 n 1 − 1 n_1-1 n11(不搜第 n 1 n_1 n1个左半图点时恰好匹配,而加入该点后不匹配,因此相关左半图点刚好比相关右半图点多一个)。
    • 对已被匹配的边而言,对应左半图点期望度减1,右半图点期望度加1,故这些边仍然在被搜索的边集合中。期望度更新操作后,每个左半图点相关的权值稍小的边被加入搜索集中。由于更新时以1为粒度,所以能保证搜索过程中不会漏掉边,保证了算法可行的必要性。

参考:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值