782.变为棋盘
本题为 2022-08-23 每日一题
难度:【困难】
题目链接:https://leetcode.cn/problems/transform-to-chessboard/
题目
一个 n x n 的二维网络 board 仅由 0 和 1 组成 。每次移动,你能任意交换两列或是两行的位置。
返回 将这个矩阵变为 “棋盘” 所需的最小移动次数 。如果不存在可行的变换,输出 -1。
“棋盘” 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。
示例1:
输入: board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]]
输出: 2
解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。
第二次移动交换了第二行和第三行。
示例2:
输入: board = [[0, 1], [1, 0]]
输出: 0
解释: 注意左上角的格值为0时也是合法的棋盘,也是合法的棋盘.
示例3:
输入: board = [[1, 0], [1, 0]]
输出: -1
解释: 任意的变换都不能使这个输入变为合法的棋盘。
题解
解法一
原作者链接:https://www.cnblogs.com/grandyang/p/9053705.html
计算最少步骤的前提是 - 0和1能够组成棋盘,于是新的问题来了 —— 什么样的情况下能够组成棋盘?
我们先观察题目给出的合理的棋盘
这是一个4*4的棋盘,对于这种问题来说我们通常要观察每一行每一列的0和1的个数。那么我们不难判断 —— 对于长度为偶数的棋盘(以下简称偶数棋盘,奇数棋盘同理),每一行和列的0和1的数量相同且都是 n/2
个。
接下来我们绘制一组3*3的奇数棋盘
这是两个3*3的棋盘,我们可以发现每一行每一列的1或者0的个数都是 (n/2)
个或者 (n+1)/2
个。
除此之外,我们打乱一下3*3的棋盘
在根据示例1给出的原始棋盘来看,我们又不难发现,能够组成棋盘的 0和1 组成的任何矩形中,4个角的分布情况只有3种
- 4个角都是0
- 4个角都是1
- 4个角有2个1和2个0
那么从上面3个情况又能得出结论 —— 4个角的异或运算一定是0
- 0 ^ 0 ^ 0 ^ 0 = 0
- 1 ^ 1 ^ 1 ^ 1 = 0
- 0 ^ 1 ^ 0 ^ 1 = 0
从上述内容来看,判断能否组成棋盘的条件如下:
- 首行/首列的1的个数要么是
(n/2)
要么是(n+1)/2
- 任意矩形的四个角的异或运算结果为0
除此之外,我们在实际人工交换行列的时候不难发现——只要维护好第一行和第一列,其他行和列都会完成相应匹配。
接下来计算最小交换步骤:
首先我们需要引入一个概念 【错位】,指的是提供的行和列和标准棋盘的行和列的值存在不同的元素。
比如说:提供的行是 10001
标准行是 01010
那么除了中间的0
之外,其他全部是不同的,那么他们存在4个错位。
我们假设目标内容总是类似如同 1010101
这种 以1开头的数列 ,那么我们就可以用以下代码通过下标的奇偶性来判断是否错位
if (arr[idx] == idx % 2);
// arr[idx] 为元素的值 如图所示 idx=3 则arr[idx]=1
// idx % 2 可以判断奇偶性,idx=3 则 idx%2=1
// 那么idx=3时该if成立,说明idx=3的元素为错位元素
值得注意的是,这个if语句中, idx%2
本质上是算出了 0101010101
这种序列,与我们的目标序列完全相反,所以若if成立则为错位。
回到计算步数。我们要对棋盘的长度 n 进行分类讨论。
- n 是奇数,例如前面提到的
10001
,如果直接对比到10101
显然是做不到的,因为0和1
的个数就不匹配,所以他的错位应当是4,目标序列应当是01010
。我们直接对比10101
获得的错位数应是1,正确的错位数则是 长度 - 1。 - n是偶数,不会出现上述问题,因为0和1的数量总是相同的,直接进行步骤计算即可。但是也会出现例外情况,比如我们传入了
0101
,但是我们前面提到说我们总会去判断1010
类似的序列,那么就会算出错位=4 - 所以最终的错位的值应当是
min(奇数错位,偶数错位)
处理完行处理列,列和行的处理方法一样。最后我们把行错位个数和列错位个数相加,然后除2,就是最小的交换次数。除2的原因是什么呢,每次交换是交换2个行/列,那么就可以同时处理2个错位。
/**
* 执行用时:1 ms, 在所有 Java 提交中击败了92.31%的用户
* 内存消耗:41.3 MB, 在所有 Java 提交中击败了17.95%的用户
*/
class Solution {
public int movesToChessboard(int[][] board) {
// 棋盘的边长
int n = board.length;
// 判断矩形的四个角
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if ((board[0][0] ^ board[i][0] ^ board[0][j] ^ board[i][j]) == 1) {
return -1;
}
}
}
// 统计首行首列的 1的个数 和错位个数
int rowSum = 0, colSum = 0, rowDiff = 0, colDiff = 0;
for (int i = 0; i < n; ++i) {
rowSum += board[0][i];
colSum += board[i][0];
rowDiff += (board[i][0] == i % 2) ? 1 : 0;
colDiff += (board[0][i] == i % 2) ? 1 : 0;
}
// 判断 1的个数 的合法性
if (n / 2 != rowSum && (n + 1) / 2 != rowSum)
return -1;
if (n / 2 != colSum && (n + 1) / 2 != colSum)
return -1;
// 根据奇偶计算交换次数
if (n % 2 == 1) {
if (rowDiff % 2 == 1) {
rowDiff = n - rowDiff;
}
if (colDiff % 2 == 1) {
colDiff = n - colDiff;
}
} else {
rowDiff = Math.min(n - rowDiff, rowDiff);
colDiff = Math.min(n - colDiff, colDiff);
}
return (rowDiff + colDiff) / 2;
}
}
解法二
原作者链接:https://leetcode.cn/problems/transform-to-chessboard/solution/transform-to-chessboard-bian-wei-qi-pan-by-jiangxu/
解法二的核心要点是 有序
什么是有序?假如第一行是 10011
那么下一行就应该是 01100
,列也同理。
也就是说,对于所有的行和列来说,都有且仅有2种排序方式,而且这两种排序方式恰好互补,这个情况下叫做有序。在这个前提情况下,我们不难肯定只要维护好第一行和第一列的值就可以完成棋盘的效果。
计算交换步骤的次数,也就是交换两个放在错误位置的行即可。所以只需要统计在错位位置的行有多少个,再把他除2即可。
class Solution {
public int movesToChessboard(int[][] board) {
// 检测是否可以变为棋盘
if (check(board)) {
// 取出第一行和第一列,检测最小交换次数
int[] row = board[0];
int[] col = new int[board.length];
for (int i = 0; i < board.length; i++) {
col[i] = board[i][0];
}
return find(row) + find(col);
} else {
return -1;
}
}
private boolean isSame(int[] a, int[] b) {
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
private boolean isOpposite(int[] a, int[] b) {
for (int i = 0; i < a.length; i++) {
if (a[i] + b[i] != 1) {
return false;
}
}
return true;
}
private boolean check(int[][] board) {
// 检测行是否只有两种模式
// 以第一行为基准,检测其余所有的行,这些行要么和第一行完全相同,要么和第一行完全相反,否则不可能变换成棋盘
int[] first = board[0];
int cntSame = 1;
int cntOpposite = 0;
for (int i = 1; i < board.length; i++) {
if (isSame(first, board[i])) {
cntSame++;
} else if (isOpposite(first, board[i])) {
cntOpposite++;
} else {
return false;
}
}
// 检测两种模式的数量分布是否正确
if (cntSame == cntOpposite || cntSame == cntOpposite + 1 || cntSame == cntOpposite - 1) {
// 行只有两种模式,且分布正确,进行列检测,
// 行只有两种模式的情况下列必然也只有两种模式,只检测列的两种模式数量分布是否正确,只用第一个数字代表不同的两种模式进行计数
int cnt0 = 0;
int cnt1 = 0;
for (int i : first) {
if (i == 0) {
cnt0++;
} else {
cnt1++;
}
}
// 检测第一行中 0 和 1 的数量(代表两种模式的数量)是否分布正确
if (cnt0 == cnt1 || cnt0 == cnt1 + 1 || cnt0 == cnt1 - 1) {
return true;
} else {
return false;
}
} else {
return false;
}
}
private int find(int[] tmp) {
// 只检测 10101010…… 情况的错位数
int start = 1;
int error = 0;
for (int i : tmp) {
// 统计有多少错位
if (i != start) {
error++;
}
start = 1 - start;
}
// 需要交换的次数是错位的一半,因为一次交换可以消除两个错位
// 排列为有序有两种可能,一种是 10101010……,一种是 01010101……
// 两种情况下计算的错位数相加等于行数,所以我们只需要计算一种
if (tmp.length % 2 == 0) {
// 如果行数是偶数,排列为 10101010…… 或 01010101…… 都是可能的
// 取两种情况下错位数的最小值
return Math.min(tmp.length - error, error) >> 1;
} else {
// 如果行数是奇数,其实只可能排列成一种情况,这取决于 1 和 0 的数量
// 1 比较多,必然只可能排成 10101010……,0 比较多,只能排成 01010101……
// 不可能排列成的那种情况下计算出来的错位数是一个奇数,所以可以通过检测错位数是否为奇数来判断采取哪个情况
if (error % 2 == 0) {
return error >> 1;
} else {
return (tmp.length - error) >> 1;
}
}
}
}
如果有更好的解题思路欢迎留言评论区