题目描述
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
简单枚举
对于很多算法问题,最直接的想法就是枚举,这题也可以用简单枚举解决:
- 在
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]
- 以上一步所有可能的选择情况,分别对比每一行,看是否能覆盖
- 覆盖行数最多的情况就是最终答案
简单枚举的思路简单,但实现起来却没那么简单,时间复杂度也高,光是枚举所有可能的列选择情况,就至少有 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!×(n−numSelect)!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为例
- 把每一行对应数字看作二进制,分别是
000
、101
、011
、001
,计算成数字分别是0、5、3、1。 - 使用二进制表示列选择,1表示选中,0表示不选中。那么所有可能的列选择有
0(000)
、1(001)
、2(010)
、3(011)
、4(100)
、5(101)
、6(110)
、7(111)
- 其中 1 的个数 ≠ n u m S e l e c t 1的个数\neq numSelect 1的个数=numSelect的情况,表示选中的列数量不等于numSelect,明显不符合要求,可以直接跳过。
- 对于符合要求的每种列选择,也就是每一个数字(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<vector<int>>& matrix, int numSelect) {int rowNum = matrix.size();int colNum = matrix[0].size();vector<int> states = vector<int>(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<vector<int>>& matrix, int numSelect) {int rowNum = matrix.size();int colNum = matrix[0].size();vector<int> states = vector<int>(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) {// doStuffint 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的用途和用法,至于之中原理,可以参考哈弗大学的课件,或者博客文章。
参考
- https://leetcode.cn/problems/maximum-rows-covered-by-columns/
- https://read.seas.harvard.edu/~kohler/class/cs207-s12/lec12.html
- https://programmingforinsomniacs.blogspot.com/2018/03/gospers-hack-explained.html
- https://www.wayuekeji.com/index