leetcode 每日一题 2024年01日04月 被列覆盖的最多行数

题目

本文主要提供一些和题解不同的解题方式

整体上分两步:暴力+优化

2397. 被列覆盖的最多行数

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

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

形式上,假设 s = {c1, c2, ...., cnumSelect} 是你选择的列的集合。对于矩阵中的某一行 row ,如果满足下述条件,则认为这一行被集合 s 覆盖

  • 对于满足 matrix[row][col] == 1 的每个单元格 matrix[row][col]0 <= col <= n - 1),col 均存在于 s 中,或者
  • row不存在 值为 1 的单元格。

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

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

示例 1:

leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第1张图片

输入:matrix = [[0,0,0],[1,0,1],[0,1,1],[0,0,1]], numSelect = 2
输出:3
解释:
图示中显示了一种覆盖 3 行的可行办法。
选择 s = {0, 2} 。
- 第 0 行被覆盖,因为其中没有出现 1 。
- 第 1 行被覆盖,因为值为 1 的两列(即 0 和 2)均存在于 s 中。
- 第 2 行未被覆盖,因为 matrix[2][1] == 1 但是 1 未存在于 s 中。
- 第 3 行被覆盖,因为 matrix[2][2] == 1 且 2 存在于 s 中。
因此,可以覆盖 3 行。
另外 s = {1, 2} 也可以覆盖 3 行,但可以证明无法覆盖更多行。

示例 2:

leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第2张图片

输入:matrix = [[1],[0]], numSelect = 1
输出:2
解释:
选择唯一的一列,两行都被覆盖了,因为整个矩阵都被覆盖了。
所以我们返回 2 。

提示:

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

分析

  1. 首先从提示和题目中我们可以得到,总共有m行n列matrix[i][j]=0或matrix[i][j]=1
  2. 我们需要从n列当中选出numSelect列,numSelect是输入的,小于等于n
  3. S = C nnumSelect:从n列中选出numSelect列可以用前面这个符号表示。如果某i行满足下面两个条件:
    • 该行是全0。
    • 对于i行而言,值是1的列,全在S(从n列中选出numSelect列的集合)中。
  4. 我们称第i行是被覆盖的。
  5. 我们需要从矩阵中选出 numSelect 个列,形成集合,使集合覆盖行数最大化

从n列中选出numSelect列,是一个组合问题
n最大值为12,所以可得最多也只会有C(12, 6) = 12! / (6! * (12-6)!) = 12! / (6! * 6!) = 924中组合。
C(n,m)从n中选m个,在m=n/2时值最

编码

枚举C(n,numSelect)

直接枚举col集合可能出现的情况。

首先我们需要枚举C(n,numSelect)可能出现的情况。

我们使用一个集合来存储,因为列号是不可能重复的:

HashSet<Integer> col = new HashSet<>();

排列组合均可以使用递归进行实现:

  1. 递归出口:当col集合的大小和numSelect一样大时,终止
  2. 递归结构:不断地从n列出选出一列,加入到col集合当中。
    • 并且选出来的这列是不存在col集合当中的。
    • 并且需要回溯,回溯看下面具体代码。
public void getNumSelectSet(int numSelect,int n){
        if(numSelect== col.size()){
          return ;
        }
        /*
         假设numSelect = 4 n=10
         假设一次执行下去第一个满足条件的集合0 1 2 3 抵达递归出口返回
         下一次满足条件的应该是是0 1 2 4 但是实际上col内容:0 1 2 3 4 (因为3从来不曾弹出来)
         所以在每一层递归结构当中需要回溯一下即某个递归分支完成后,就把其对应的数据remove()
         例如加入第3列 col.add(3),该分支递归完成后 col.remove(3)
        */
        for (int i = 0; i < n; i++) {
            if(!col.contains(i)){
                col.add(i);
                getNumSelectSet(numSelect,n);
                col.remove(i); //回溯
            }
           
        }
    }

我们在得到col集合之后,怎么判断得到的集合能否覆盖matrix的某一行呢?

计算覆盖

col集合可以覆盖matrix的某一行说明,matrix[i]中所有是1的列(即i,下标index)都在col中。

举例:

总共有5(n)列 从中选出4(numSelect)列 
matrix[i1]=[1,1,0,1,1]
matrix[i2]=[1,1,0,0,0]
假设col1集合=[0,2,3,4] 选出的是0,2,3,4列,明显无法覆盖matrix[i1]的第1列(实际上是第2列,因为是数组下标减1)
假设col2集合=[0,1,3,4] 明显可以覆盖matrix[i2](只有第0列和1列有1)

我们知道列数是小于等于12的。

我们完全可以用12位二进制数表达这12列。

上面的例子我们就可以用5位二进制表示

col1 = [0,2,3,4] =>选中0,2,3,4列 => binCol1 = 1 1 1 0 1
matrix[i1]=[1,1,0,1,1] =>第0 1 3 4都是1 => binNum1 = 1 1 0 1 1

col2 = [0,1,3,4] => binCol2 = 1,1,0,1,1 
matrix[i2]=[1,1,0,0,0]=>binNum2= 0 0 0 1 1

matrix[i]对应的二进制数就是其本身

当col集合可以覆盖matrix[i]时。col集合对应的二进制数和matrix[i]对应的二进制数相与是 等于martrx[i]对应的二进制数本身的。

例如`binNum2&binCol2=binNum2

计算matrix每一行对应的二进制数

可以用int数值来表示二进制数值,第i列为1,就代表int从低位(从0)到高位的低i位是1

ArrayList<Integer> status = new ArrayList<>();
for (int[] ints : matrix) {
    int state = 0;
    for (int i = 0; i < ints.length; i++) {
        if(ints[i]!=0){
            state+=(1<<i);
        }
    }
    status.add(state);
}

计算col集合二进制数

计算方法和上面类似

int state = 0;
for (Integer integer : col) {
    state+=(1<<integer);
}

计算覆盖行数

status是matrix每一行对应的二进制数的数组

state是col集合对应的二进制数

计算得到count值。

int count = 0;
for (Integer integer : status) {
    if((integer&state)==integer){
        count++;
    }
}

暴力破解代码

根据上面的分析可以得到下代码

代码

class Solution {
    ArrayList<Integer> status = new ArrayList<>();
    public int maximumRows(int[][] matrix, int numSelect) {
        int m  = matrix.length;
        int n = matrix[0].length;
        for (int[] ints : matrix) {
            int state = 0;
            for (int i = 0; i < ints.length; i++) {
                if(ints[i]!=0){
                    state+=(1<<i);
                }
            }
            status.add(state);
        }
        getNumSelectSet(numSelect,n);
        return max;
    }
    HashSet<Integer> col = new HashSet<>();
    int max = 0;

    public void getNumSelectSet(int numSelect,int n){
        if(numSelect== col.size()){
            int state = 0;
            for (Integer integer : col) {
                state+=(1<<integer);
            }
            int count = 0;
            for (Integer integer : status) {
                if((integer&state)==integer){
                    count++;
                }
            }
            max = Math.max(count,max);
            return ;
        }
        //假设m等于10
        //0,1,2,3
        for (int i = 0; i < n; i++) {
            if(!col.contains(i)){
                col.add(i);
                getNumSelectSet(numSelect,n);
                col.remove(i); //回溯
            }
           
        }
    }
}

直接提交我们可以发现超时了。

leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第3张图片

原因如下

  1. 假设n=10,numSelect=4
  2. 在递归过程中,col会出现在递归过程中因为回溯回产生[0,1,2,3]和[1,0,2,3];[0,1,2,9]和[9,0,1,2]等情况。
  3. 因为回溯,每个递归分支结束之后对应的列号都会被remove。例如加入第9列之前,此时集合是空的,导致重复

减枝

  1. [0,1,2,3]和[1,0,2,3]这种情况的产生,是因为,我们每次尝试加入列都是从0开始。
    现在修改为从上一列的下一列开始。
  2. [0,1,2,9]和[9,0,1,2]这种情况。基于1的减枝规则我们可以发现,当下标index>n-numSelect时,col集合的大小是永远不可能等于numSelect的。
    所以此时我们直接结束循环即可。
class Solution {
    public int maximumRows(int[][] matrix, int numSelect) {
        int n = matrix[0].length;
        for (int[] ints : matrix) {
            int state = 0;
            for (int i = 0; i < ints.length; i++) {
                if(ints[i]!=0){
                    state+=(1<<i);
                }
            }
            status.add(state);
        }
        getNumSelectSet(numSelect,n,0);
        return max;
    }
    ArrayList<Integer> status = new ArrayList<>();
    HashSet<Integer> col = new HashSet<>();

    int max = 0;

    //多加入一个参数index进行减枝
    public void getNumSelectSet(int numSelect,int n,int index){
        if(numSelect== col.size()){
            int state = 0;
            for (Integer integer : col) {
                state+=(1<<integer);
            }
            int count = 0;
            for (Integer integer : status) {
                if((integer&state)==integer){
                    count++;
                }
            }
            max = Math.max(count,max);
            return ;
        }
        if(index==n) return ;
        //假设m等于10
        //0,1,2,3
        for (int i = index; i < n; i++) {
            if(col.size()==0&&i>n-numSelect) break;//减枝
            if(!col.contains(i)){
                col.add(i);
                getNumSelectSet(numSelect,n,i+1);
                col.remove(i); //回溯
            }

        }
    }
}

提交代码发现通过了。
leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第4张图片

那么还有没有更快的方法呢?

我们可以看到col集合是用集合来存储的,在最终找到合法的col集合时计算state

int state = 0;
for (Integer integer : col) {
    state+=(1<<integer);
 }

这个过程完全可以用位运算来完成:

假设现在用col(int类型的数)来代表选择过的列会有如下等式

  1. col.size()=>col==0
  2. !col.contains(i) => col&(1<

位运算

  1. 原本的列号集合直接用col数值替代
  2. depth变量,代表当前选中了几个列
  3. depth==numSelect说明找到一个合法的col
class Solution {
public int maximumRows(int[][] matrix, int numSelect) {
        int n = matrix[0].length;
        for (int[] ints : matrix) {
            int state = 0;
            for (int i = 0; i < ints.length; i++) {
                if(ints[i]!=0){
                    state+=(1<<i);
                }
            }
            status.add(state);
        }
        getNumSelectSet(numSelect,n,0,0,0);
        return max;
    }
    ArrayList<Integer> status = new ArrayList<>();

    int max = 0;

    //递归暴力计算
    public void getNumSelectSet(int numSelect,int n,int index,int col,int depth){
        if(numSelect==depth){
            //Integer.bitCount(col)可以使用这个来代替
            int count = 0;
            for (Integer integer : status) {
                if((integer&col)==integer){
                    count++;
                }
            }
            max = Math.max(count,max);
            return ;
        }
        //假设m等于10
        //0,1,2,3
        for (int i = index; i < n; i++) {
            if(col==0&&i>n-numSelect) break;
            int bC = (1<<i);
            if((col&bC)==0){
                col+=bC;
                depth++;
                getNumSelectSet(numSelect,n,i+1,col,depth);//优化后的代码
                col-=bC;
                depth--;
            }

        }
    }
}

结果:
leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第5张图片

交流

qq群:
leetcode 每日一题 2024年01日04月 被列覆盖的最多行数_第6张图片

你可能感兴趣的:(leetcode每日一题,leetcode,算法,剪枝)