【782. 变为棋盘】

来源:力扣(LeetCode)

描述:

一个 n x n 的二维网络 board 仅由 01 组成 。每次移动,你能任意交换两列或是两行的位置。

返回 将这个矩阵变为 “棋盘” 所需的最小移动次数 。如果不存在可行的变换,输出 -1

“棋盘” 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。


示例 1:

1

输入: board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]]
输出: 2
解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。
第二次移动交换了第二行和第三行。

示例 2:

2

输入: board = [[0, 1], [1, 0]]
输出: 0
解释: 注意左上角的格值为0时也是合法的棋盘,也是合法的棋盘.

示例 3:
3

输入: board = [[1, 0], [1, 0]]
输出: -1
解释: 任意的变换都不能使这个输入变为合法的棋盘。

提示:

  • n == board.length
  • n == board[i].length
  • 2 <= n <= 30
  • board[i][j] 将只包含 0或 1

方法:分维度计算

  首先需要思考的是对矩阵做一次交换之后,矩阵的变换状态。比如我们以交换列为代表,在对任意两列进行交换之后,可以看到列交换是不会改变任意相邻两行之间的元素异同对应关系,比如相邻两行的两个元素 board[i][j] , board[i + 1][j] 原本就相同,任意列交换之后这个两个元素对应的关系保持不变,如果这两个元素本来就不同,经过列交换之后也仍然不同,因此可以推出矩阵一定只能包含有两种不同的行,要么与第一行的元素相同,要么每一行的元素刚好与第一行的元素“相反”。如果矩阵可以转换为合法的“棋盘”,假设第 1 行的元素为 [0, 1, 1, 1, 0],则其他行的元素要么为 [0, 1, 1, 1, 0],要么为 [1, 0, 0, 0, 1]。最终的棋盘一定只有两种不同的行,要么以 0 开始的 [0, 1, 0, 1, ⋯],要么以 1 开始的 [1, 0, 1 ,0 , ⋯],因此我们可以推出棋盘也一定可以通过列变换将所有的行变为只有以上两种状态的行,否则无法得到最终合法的“棋盘”。同时我们可以观察到,先换行再换列跟先换列再换行结果是一样的,因为我们可以先将所有的行调整到正确的位置,再将所有的列调整到正确的位置。行与列之间的变换实际是相互独立的,二者互不影响,列变换不会影响相邻两行的异同关系,行变换不会不会影响相邻两列的异同关系。

  由于最终只有两种不同的行,要达成最终的“棋盘”实际上等价于将矩阵的行表示成 0, 1 相互交替的状态,如果一个行无法变为 0, 1 交替的状态,则我们认为矩阵不存在可行的变换。假设矩阵的某行用 [0, 1] 表示之后得到数组为 [0, 1, 1, 1, 0, 0] ,那么只需求出这个数组变成 [0, 1, 0, 1, 0, 1] 或者 [1, 0, 1, 0, 1, 0] 的最少交换次数即可。同理,对于矩阵的列也是如此,这就将二维问题变成了两个一维问题。我们实际只需要分别将矩阵的第一行变为最终状态和第一列变为最终状态,最终的矩阵一定为合法“棋盘”。

  • 首先我们需要检测矩阵的合法性,即该矩阵是否可以变为合法的“棋盘”。我们依次检测矩阵的每一行是否是否可以变为 0, 1 交替,即变为 [0, 1, 0, 1, ⋯],[1, 0, 1, 0, ⋯] 两种可能的行;然后依次检测矩阵的每一列是否可以变为 0, 1 交替,即变为 [0, 1, 0, 1, ⋯],[1, 0, 1, 0, ⋯] 两种可能的列。设行的数目为 n,检测矩阵的行与列时需要进行如下检测:
    • 检测每一行和每一列的状态是否合法:由于列变换不改变相邻两行元素的对应关系,因此我们可以知道矩阵的行要么与第 1 行相同,要么与第 1 行“相反”。设第一行的状态为 rowMask,与之相反对应的状态为 reverseRowMask,我们检测每一行是否属于这两个合法的状态 rowMask, reverseRowMask,如果不合法直接返回,对于列也采用同样的检测方法。由于题目中的行与列的值均为 0 或者 1,且行数和列数最大为 30,我们利用压缩位图来表示每一行或者每一列的状态,可以用一个 32 位整数来表示每一行,其中整数每位上的数字对应着每列上的数字。
    • 检测每一行和每一列中含有的数字是否合法:检测每一行或者每一列若要变为 0, 1 交替的状态,如果 n 为偶数,则每一行中 1 的数目与 0 的数目相等;如果 n 为奇数,则每一行中 1 的数目与 0 的数目相差的绝对值一定为 1。此时我们只需要检测第一行中含有的数字 0, 1 的个数是否合法,对于列我们也采用同样的检测方法。由于我们用一个 32 位整数来表示每一行或者每一列,我们只需要要快速计算出整数的二进制位上含有的 1 的数目即可。
    • 检测不同状态的行数和列数是否合法:我们设矩阵中与第一行相同的行的数量为 count。根据我们之前的推论可知,需要满足两种不同的行交替分情况讨论:如果 n 为偶数,由于必须要满足两种不同的行交替,每种行的数目只能占到总行数的一半,此时一定有 ncount × 2 = n;如果 n 为奇数,由于必须要满足两种不同的行交替,则另一种行的数量只能为 n − count,由于必须满足交替不同,则二者之间的差值的绝对值一定为 1,因此此时一定满足 ∣2 × count − n∣ = 1,满足以上条件才是合法的行数。我们采用同样的方法对矩阵的列数进行检测。
  • 其次我们求出将矩阵变为棋盘的最少交换次数。分为两种情况讨论:
    • 如果 n 为偶数,则此时最终的合法棋盘有两种可能,即第一行的元素的第一个元素 board[0][0] = 0 或者 board[0][0] = 1。我们可以选择将第 1 行变为以 0 开头,此时只需将偶数位上的 0 全部替换为 1 即可;也可以选择将第 1 行变为以 1 开头,此时只需将奇数位上的 0 全部替换为 1 即可。我们可以用位图来快速计算出偶数位或者奇数位上 1 的个数,可以与特定的数进行布尔代数运算即可快速消除奇数位或者偶数位上的 1
    • 如果 n 为奇数,则此时最终的合法棋盘只有一种可能,如果第一行中 0 的数目大于 1 的数目,此时第一行只能变为以 0 为开头交替的序列,此时我们只需要将偶数位上的 0 全部变为 1;如果第一行中 0 的数目小于 1 的数目,此时第一行只能交换变为以 1 为开头交替的序列,此时我们只需要将奇数位上的 0 全部变为 1。可以用位图来快速计算出偶数位或者奇数位上 1 的个数,可以与特定的数进行布尔代数运算即可快速消除奇数位或者偶数位上的 1
    • 由于我们采用 32 位整数表示每一行或者每一列,在快速计算偶数位或者上的 1 的数目时可以采用位运算掩码。比如 32 位整数 x,我们只保留 x 偶数位上的 1,此时我们需要去掉奇数位上的 1,此时只需将 x 与掩码:

(1010 1010 1010 1010 1010 1010 1010 1010)2 = 0x AAAA AAAA

相与即可;我们只保留 x 奇数位上的 1,此时我们需要去掉偶数位上的 1,此时只需将 x 与掩码:

(0101 0101 0101 0101 0101 0101 0101 0101)2 = 0x 5555 5555

代码:

class Solution {
public:
    int getMoves(int mask, int count, int n) {
        int ones = __builtin_popcount(mask);
        if (n & 1) {
            /* 如果 n 为奇数,则每一行中 1 与 0 的数目相差为 1,且满足相邻行交替 */
            if (abs(n - 2 * ones) != 1 || abs(n - 2 * count) != 1 ) {
                return -1;
            }
            if (ones == (n >> 1)) {
                /* 偶数位变为 1 的最小交换次数 */
                return n / 2 - __builtin_popcount(mask & 0xAAAAAAAA);
            } else {
                /* 奇数位变为 1 的最小交换次数 */
                return (n + 1) / 2 - __builtin_popcount(mask & 0x55555555);
            }
        } else { 
            /* 如果 n 为偶数,则每一行中 1 与 0 的数目相等,且满足相邻行交替 */
            if (ones != (n >> 1) || count != (n >> 1)) {
                return -1;
            }
            /* 偶数位变为 1 的最小交换次数 */
            int count0 = n / 2 - __builtin_popcount(mask & 0xAAAAAAAA);
            /* 奇数位变为 1 的最小交换次数 */
            int count1 = n / 2 - __builtin_popcount(mask & 0x55555555);  
            return min(count0, count1);
        }
    }

    int movesToChessboard(vector<vector<int>>& board) {
        int n = board.size();
        int rowMask = 0, colMask = 0;        

        /* 检查棋盘的第一行与第一列 */
        for (int i = 0; i < n; i++) {
            rowMask |= (board[0][i] << i);
            colMask |= (board[i][0] << i);
        }
        int reverseRowMask = ((1 << n) - 1) ^ rowMask;
        int reverseColMask = ((1 << n) - 1) ^ colMask;
        int rowCnt = 0, colCnt = 0;
        for (int i = 0; i < n; i++) {
            int currRowMask = 0;
            int currColMask = 0;
            for (int j = 0; j < n; j++) {
                currRowMask |= (board[i][j] << j);
                currColMask |= (board[j][i] << j);
            }
            /* 检测每一行的状态是否合法 */
            if (currRowMask != rowMask && currRowMask != reverseRowMask) {
                return -1;
            } else if (currRowMask == rowMask) {
                /* 记录与第一行相同的行数 */
                rowCnt++;
            }
            /* 检测每一列的状态是否合法 */
            if (currColMask != colMask && currColMask != reverseColMask) {
                return -1;
            } else if (currColMask == colMask) {
                /* 记录与第一列相同的列数 */
                colCnt++;
            }
        }
        int rowMoves = getMoves(rowMask, rowCnt, n);
        int colMoves = getMoves(colMask, colCnt, n);
        return (rowMoves == -1 || colMoves == -1) ? -1 : (rowMoves + colMoves); 
    }
};

执行用时:8 ms, 在所有 C++ 提交中击败了71.43%的用户
内存消耗:9.8 MB, 在所有 C++ 提交中击败了62.50%的用户
复杂度分析
时间复杂度:O(n2),其中 n 为矩阵的行数。我们只需要遍历矩阵一遍即可。
空间复杂度:O(1)。
author:LeetCode-Solution

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千北@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值