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,所以总量也是可控的。
简单枚举的思路简单,但实现起来却没那么简单,时间复杂度也高,光是枚举所有可能的列选择情况,就至少有 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。0(000)
、1(001)
、2(010)
、3(011)
、4(100)
、5(101)
、6(110)
、7(111)
&
运算,如果结果还是列选择对应的数字,表示列选择覆盖了这一行代码实现代码:
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的个数
在上述实现中,为了筛选出不符合条件的枚举,增加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的用途和用法,至于之中原理,可以参考哈弗大学的课件,或者博客文章。