782 变为棋盘——Leetcode天天刷(2022.8.23)【数学:降维计算】
文章目录
前言
打了下今天的每日一题,不得不说,不愧是
Hard
,虽然感觉自己经过集训的洗礼,能直接手撕了。But,想了几分钟,确实没什么好的思路,但是,可以知道纯暴力的棋盘搜索是不行的。在看了
官方题解
和三叶小姐姐
的文字思路后,才知道这题目可以降维来求解!理解了思路后,即使不看代码,也可以凭借对思路的理解来手撸代码实现了。
题目描述
感兴趣的可以点击跳转打题。
简述一下:就是一个 n ∗ n n * n n∗n 的二维数组,每个单元格不是 0 0 0 就是 1 1 1。
我们可以 任意 交换 两 行 或者两 列,最终使这个矩阵变为 棋盘,棋盘就是任意一个格子的上下左右四个方向的值和当前值不同的矩阵。
我们需要返回输出最少需要的交换次数,如果不存在可行的交换,则输出 − 1 -1 −1。
示例
输入: board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]]
输出: 2
解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。
第二次移动交换了第二行和第三行。
输入: board = [[0, 1], [1, 0]]
输出: 0
解释: 注意左上角的格值为0时也是合法的棋盘,也是合法的棋盘.
输入: board = [[1, 0], [1, 0]]
输出: -1
解释: 任意的变换都不能使这个输入变为合法的棋盘。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/transform-to-chessboard
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处
提示信息
本地调试运行
这个就还好,不想树之类的题目,需要写额外的代码来构造树,我们只需要做好输入格式即可。
输入格式
假定多组输入,每组第一行输入一个整数n,然后接下来n行,每行输入n个整数。
输出格式
根据题目的类函数结构,我们直接输出结果即可。
输入样例
4
0 1 1 0
0 1 1 0
1 0 0 1
1 0 0 1
2
0 1
1 0
2
1 0
1 0
输出样例
2
0
-1
像leetcode这种OJ比较人性化,如果没通过会返回不能通过的测试数据,如果是普遍的一些OJ,我们其实可以在本地写多一个数据生成器,加一个暴力算法,来确保我们代码能够正确。
解法——数学,降维计算,模拟
参考自官方题解和三叶小姐姐的题解文字思路!
我们首先需要明白一个本质问题:行列变化的独立性
,以 **行交换 **为例,我们任意交换两行,我们会发现列的种类没有发生变化,只是0,1的分布改变了。从这个特性,我们就可以知道,行,列交换的顺序先后,对结果不会有影响!这也是我们可以做降维计算的基础!
降维计算
,其实就是分维度来计算,我们原本是需要考虑通过行列变化来使得整个矩阵变为棋盘,这时我们可以不用考虑整个棋盘,而是考虑其中的一行一列!
首先我们考虑 有解
的情况:如果有解,那么对于第一行,如果n为偶数,那么0和1的数量一定相同;如果是奇数,那么差值为1,这个对每一行,每一列都成立。而对于其他行,和第一行的对称位置,要么就完全相同,要么就是完全相反,且相同的行数,和不同的行数,和0,1同理,差值不会超过1。
所以,我们只需要将第一行变为0101。。。或者1010。。。这样的间隔形式即可。
列同理。
那么这个变为棋盘的问题,就拆分成了两个部分:先判断是否有解,在判断需要变换的次数。
判断是否有解
只需要满足我们前面提到的条件即可,我们再梳理下:
-
第一行、列中1,0的数目差的绝对值,设为 sub,有如下公式
s u b = { 0 , n % 2 = = 0 1 , n % 2 = = 1 sub = \left\{ \begin{aligned} 0,\qquad n \% 2 == 0 \\ 1,\qquad n \% 2 == 1 \end{aligned} \right. sub={0,n%2==01,n%2==1 -
其他行,和第一行,要么完全相同,要么完全相反,列同理
-
接条件2,相同和相反的行数以及列数,差值的绝对值和条件1的公式相同。
计算移动次数
我们只需判断第一行和第一列即可。
具体如下, 记移动次数为times
:
因为下标是从0开始,我们可以取偶数位下标的数字,我们分别计算第一行、列的偶数位下标对应的数字加和,记为 r1
和 c1
。
-
n为偶数,那么无外乎是把偶数位不是1的数字换成1,或者把数字1换成0,有如下公式
t i m e s + = min ( r 1 , n / 2 − r 1 ) times += \min(r1, n/2-r1) times+=min(r1,n/2−r1)
列同理。 -
n为奇数,我们则需要根据0,1谁的数量多,决定将多的放偶数位。
思路清晰后,就上代码!
Code(C++)
#include<iostream>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
class Solution
{
public:
// 分维度计算
// 分两步走,第一步判断是否有解——棋盘是否正确!第二步计算移动的次数
// 有解需要判断的条件:1.第一行、列中1,0的数目差值绝对值=1(n为奇数)或者=0(偶数)
// 2.其他行和列中不是第一行/列的相同,就是相反,并整个棋盘中的相同和相反状态的数量差值<=1
// 计算移动次数,由于行、列移动的独立性,我们只需要判断第一行、第一列的情况即可
// 根据n的奇偶性,和0,1的数量,在判断奇偶位的0/1的数目,即为移动次数
int movesToChessboard(vector<vector<int>>& board)
{
int n = board.size();
int rowMask = 0, ReverRowMask = 0; // 第一行的状态
// 第一行中0和1的数量
int count0 = 0, count1 = 0;
// 遍历第一行,计算状态掩码和反状态掩码,并记录0,1的个数
for (int i = 0; i < n; ++i)
{
rowMask <<= 1;
ReverRowMask <<= 1;
if (board[0][i])
{
rowMask |= 1;
++count1;
}
else
{
ReverRowMask |= 1;
++count0;
}
}
// 判断第一行中0,1个数是否合理
if (!((n % 2 == 1 && abs(count0 - count1) == 1) || (n % 2 == 0 && count0 == count1)))
{
return -1;
}
int couRM = 1, couRRM = 0; // 和第一行状态相同的行数,相反的行数
// 接下来遍历每一行
for (int i = 1; i < n; ++i)
{
int rm = 0;
// 计算该行的状态
for (int j = 0; j < n; ++j)
{
rm <<= 1;
rm |= board[i][j];
}
// 和第一行的状态以及反转态比较并计数
if (rm == rowMask)
{
++couRM;
}
else if (rm == ReverRowMask)
{
++couRRM;
}
// 如果两个都不同,那就无解
else
{
return -1;
}
}
// 接下来根据n的奇偶性,和rouMask相同和相反的行数,来判断是否有解
if (!((n % 2 == 1 && abs(couRM - couRRM) == 1) || (n % 2 == 0 && couRM == couRRM)))
{
return -1;
}
// 行判断完了,接下来就是列的判断,基本同上
int colMask = 0, rColMask = 0;
int count2 = 0, count3 = 0;
for (int i = 0; i < n; ++i)
{
colMask <<= 1;
rColMask <<= 1;
if (board[i][0])
{
colMask |= 1;
++count2;
}
else
{
rColMask |= 1;
++count3;
}
}
if (!((n % 2 == 1 && abs(count2 - count3) == 1) || (n % 2 == 0 && count2 == count3)))
{
return -1;
}
int couCM = 1, couRCM = 0;
for (int i = 1; i < n; ++i)
{
int cm = 0;
for (int j = 0; j < n; ++j)
{
cm <<= 1;
cm |= board[j][i];
}
if (colMask == cm)
{
++couCM;
}
else if (cm == rColMask)
{
++couRCM;
}
else
{
return -1;
}
}
if (!((n % 2 == 1 && abs(couCM - couRCM) == 1) || (n % 2 == 0 && couCM == couRCM)))
{
return -1;
}
// 接下来是计算需要移动的次数
int ans = 0;
int r1 = 0, c1 = 0; // 计算第一行、第一列中,下标从0开始的偶数位1的个数
for (int i = 0; i < n; i += 2)
{
r1 += board[0][i];
c1 += board[i][0];
}
// n为偶数,偶数下标用0,用1都行
if (n % 2 == 0)
{
// 有两种策略,一种是偶数下标中的0换为1,一种是将1换为0,取最小值
ans += min(count1 - r1, r1) + min(count2 - c1, c1);
}
// 第一个必须是数量较多的
else
{
// 第一行0比1多
if (count0 > count1)
{
ans += r1; // 用0换1
}
else
{
ans += count1 - r1; // 用1换0
}
if (count3 > count2)
{
ans += c1;
}
else
{
ans += count2 - c1;
}
}
return ans;
}
};
int main()
{
int n;
Solution* sol = new Solution();
while (~scanf("%d", &n))
{
vector<vector<int>> board(n, vector<int>(n));
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
scanf("%d", &board[i][j]);
}
}
printf("%d\n", sol->movesToChessboard(board));
}
delete sol;
return 0;
}
算法分析+解题效率
时间复杂度 O ( n 2 ) O(n^2) O(n2),遍历两次矩阵。
空间复杂度 O ( 1 ) O(1) O(1),只使用了几个变量。
后话
不愧是Hard,打的有点慢!