【leetcode:2397. 被列覆盖的最多行数】子集枚举、二进制枚举以及Gosper’s hack

题目描述

2397. 被列覆盖的最多行数

给你一个下标从 0 开始、大小为 m x n 的二进制矩阵 matrix ;另给你一个整数 numSelect,表示你必须从 matrix 中选择的 不同 列的数量。

如果一行中所有的 1 都被你选中的列所覆盖,则认为这一行被 覆盖 了。

你需要从矩阵中选出 numSelect 个列,使集合覆盖的行数最大化。

返回一个整数,表示可以由 numSelect 列构成的集合 覆盖最大行数

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 12
  • matrix[i][j] 要么是 0 要么是 1
  • 1 <= numSelect <= n

简单枚举

对于很多算法问题,最直接的想法就是枚举,这题也可以用简单枚举解决:

  1. n列中选出numSelect个,总量是可以枚举出来的,由于n<=12,所以总量也是可控的。
    • n = 3 n = 3 n=3 n u m S e l e c t = 2 numSelect = 2 numSelect=2为例,所有可能的选择情况有 [ 0 , 1 , 1 ] [0, 1, 1] [0,1,1] [ 1 , 0 , 1 ] [1, 0, 1] [1,0,1] [ 1 , 1 , 0 ] [1, 1, 0] [1,1,0]
  2. 以上一步所有可能的选择情况,分别对比每一行,看是否能覆盖
  3. 覆盖行数最多的情况就是最终答案

简单枚举的思路简单,但实现起来却没那么简单,时间复杂度也高,光是枚举所有可能的列选择情况,就至少有 C n n u m S e l e c t = n ! n u m S e l e c t ! × ( n − n u m S e l e c t ) ! C_{n}^{numSelect} = \frac{n!}{numSelect! \times (n - numSelect)!} CnnumSelect=numSelect!×(nnumSelect)!n! 种,对于每一种枚举情况又要遍历每一行的所有元素,也就是 O ( m × n ) O(m \times n) O(m×n)

二进制枚举

在简单枚举的基础上,可以想到二进制枚举。也就是用一个数的二进制来表示列选择情况,结合位运算就可以提升复杂度。

同时将每一行对应的数字看作二进制,计算对应的数字。

输入: matrix = [[0,0,0],[1,0,1],[0,1,1],[0,0,1]], numSelect = 2为例

  1. 把每一行对应数字看作二进制,分别是000101011001,计算成数字分别是0、5、3、1。
  2. 使用二进制表示列选择,1表示选中,0表示不选中。那么所有可能的列选择有0(000)1(001)2(010)3(011)4(100)5(101)6(110)7(111)
  3. 其中 1 的个数 ≠ n u m S e l e c t 1的个数\neq numSelect 1的个数=numSelect的情况,表示选中的列数量不等于numSelect,明显不符合要求,可以直接跳过。
  4. 对于符合要求的每种列选择,也就是每一个数字(3、5、6)
    • 遍历每一行,这里是第1步中每一行计算出的数字
    • 列选择对应的数字与行对应的数字做&运算,如果结果还是列选择对应的数字,表示列选择覆盖了这一行

代码实现代码:

class Solution {
private:
    int num1ofInt(int n) {
        // 计算n的二进制表示中1的个数
        int res = 0;
        while(n > 0) {
            res += (n & 1);
            n >>= 1;
        }
        return res;
    }
public:
    int maximumRows(vector>& matrix, int numSelect) {
        int rowNum = matrix.size();
        int colNum = matrix[0].size();
        vector states = vector(rowNum, 0);
        for(int i = 0;i < rowNum;++i) {
            for(int j = 0;j < colNum;++j) {
                states[i] += (matrix[i][j] << (colNum - j - 1));
            }
        }
        int res = 0;
        int permutationNum = (1 << colNum);
        // 0 表示一列也不选,不满足要求
        for(int i = 1;i < permutationNum;++i) {
            if(num1ofInt(i) != numSelect) {
                continue;
            }
            int t = 0;
            for(int j = 0;j < rowNum;++j) {
                if((states[j] & i) == states[j]) {
                    ++t;
                }
            }
            res = max(res, t);
        }
        return res;
    }
};

其中自己实现的num1ofInt()可以用__builtin_popcount()代替,其作用是:

This function is used to count the number of set bits in an unsigned integer. In other words, it counts the number of 1’s in the binary form of a positive integer.

该函数用来统计正整数的二进制表示中1的个数

Gosper’s Hack

在上述实现中,为了筛选出不符合条件的枚举,增加if(__builtin_popcount(i) != numSelect) { continue; }的判断,有没有方法去掉该判断,只遍历符合条件的枚举呢?

这正是Gosper’s Hack的用武之地!

什么是Gosper’s Hack?简单来说就是:

Gosper’s Hack iterates through all n-bit values that have k bits set to 1, from lowest to highest.

遍历长度为n的二进制序列中1的数量为k的情况

这不就是为解决这里的问题而生的嘛!那如何实现呢?使用起来很简单,3行代码:

void GospersHack(int k, int n)
{
    int set = (1 << k) - 1;
    int limit = (1 << n);
    while (set < limit)
    {
        // DoStuff(set);
        
        // Gosper's hack:
        int c = set & - set;
        int r = set + c;
        set = (((r ^ set) >> 2) / c) | r;
    }
}

这里的DoStuff()就是我们判断是否覆盖的地方。

所以使用Gosper’s Hack提升之后的代码如下 :

class Solution {
public:
    int maximumRows(vector>& matrix, int numSelect) {
        int rowNum = matrix.size();
        int colNum = matrix[0].size();
        vector states = vector(rowNum, 0);
        for(int i = 0;i < rowNum;++i) {
            for(int j = 0;j < colNum;++j) {
                states[i] += (matrix[i][j] << (colNum - j - 1));
            }
        }
        
        int res = 0;
        int limit = (1 << colNum);
        int set = (1 << numSelect) - 1;
        while(set < limit) {
            // doStuff
            int t = 0;
            for(int j = 0;j < rowNum;++j) {
                if((states[j] & set) == states[j]) {
                    ++t;
                }
            }
            res = max(res, t);
            
            // Gosper's hack:
            int c = set & -set;
            int r = set + c;
            set = (((r ^ set) >> 2) / c) | r;
        }
        return res;
    }
};

本文章只介绍了Gosper’s Hack的用途和用法,至于之中原理,可以参考哈弗大学的课件,或者博客文章。

参考

  1. https://leetcode.cn/problems/maximum-rows-covered-by-columns/
  2. https://read.seas.harvard.edu/~kohler/class/cs207-s12/lec12.html
  3. https://programmingforinsomniacs.blogspot.com/2018/03/gospers-hack-explained.html
  4. https://www.wayuekeji.com/index

你可能感兴趣的:(leetcode,leetcode,算法,职场和发展,c++)