题目
题目链接
一个
n
×
n
n\times n
n×n的二维网络
b
o
a
r
d
board
board仅由
0
0
0和
1
1
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
解释: 任意的变换都不能使这个输入变为合法的棋盘。
思路
观察上图,不难发现任意交换两列不会影响行之间0和1的异同关系【性质1】。
所以,想要凑成棋盘,矩阵一定只能包含有两种不同的行【性质2】,要么与第一行的元素相同,要么每一行的元素刚好与第一行的元素“相反”。
如第一排元素是 [ 0 , 1 , 1 , 0 ] [0,1,1,0] [0,1,1,0],其他行只能是 [ 0 , 1 , 1 , 0 ] [0,1,1,0] [0,1,1,0]或者 [ 1 , 0 , 0 , 1 ] [1,0,0,1] [1,0,0,1]。
- 所以检查能否通过交换变成一个棋盘,可以通过判断其他行/列与第一行/列是否只存在相同或者相反的关系。
- 行/列变换都不会改变某行/列0和1的个数,所以每一行的0和1的个数是固定不变的。若能变成一个棋盘,当n是偶数时:0的个数=1的个数=n//2;当n时奇数时:0和1的个数之差=1。
这个矩阵只由0和1组成,且长度在30以内。可以使用一个32位的int数字表示。
如何表示?
假设第一行是
A
=
[
1
,
0
,
1
,
0
]
A=[1,0,1,0]
A=[1,0,1,0],使用
i
n
t
int
int类型的
n
u
m
num
num来表示:
num = 0
for i in len(A):
num |= A[i]<<i
代码解释:
num=0,则num的二进制表示: ⋯ 0000000 \cdots0000000 ⋯0000000
然后将 A A A数组的第 i i i个元素左移i位;
- 当数组 A A A元素是0时,左移 i i i位二进制还是全0,和 n u m num num的进行或运算, n u m num num不变,表示 n u m num num第 i i i位二进制就是0,不用改变。
- 当数组 A A A元素是1时,左移 i i i位二进制变成第 i i i位是1,其他全是0,和 n u m num num的进行或运算, n u m num num第 i i i位由0变成1,表示存储 A A A的第 i i i个元素。
最后 n u m num num二进制表示为: ⋯ 0001010 \cdots0001010 ⋯0001010
首先编成判断矩阵能否变成棋盘的代码:
n = len(board)#获取棋盘维度n
rowMask = colMask = 0#定义用于表示第一行和第一列的int类型变量
for i in range(n):
#将矩阵存储到int变量中
rowMask |= board[0][i] << i
colMask |= board[i][0] << i
#创建与第一行和第一列相反的变量
#1<<n-1 其实就是……0000+n个1
#^表示异或,同0异1
reverseRowMask = ((1 << n) - 1) ^ rowMask
reverseColMask = ((1 << n) - 1) ^ colMask
#用于记录有多少个和第一行/第一列相同的行/列
rowCnt = colCnt = 0
for i in range(n):
#矩阵第i行/列的int表示
currRowMask = currColMask = 0
for j in range(n):
currColMask |= board[j][i] << j
currRowMask |= board[i][j] << j
#(和第一行不一样 并且 和第一行不是相反关系) 并且 (和第一列不一样 并且 和第一列不是相反关系)
if (currRowMask != rowMask and currRowMask != reverseRowMask) and (currColMask != colMask and currColMask != reverseColMask):
return -1
#和第一行/列一样就+1,这个数后面会用于判断能否变成棋盘
rowCnt += currRowMask == rowMask
colCnt += currColMask == colMask
接下来判断变成棋盘需要走几步
交换两列并不会影响行之间的 0 − 1 0-1 0−1位置,所以行操作与列操作可以看作是两个独立的操作(解耦)。
如果这个矩阵能够变成一个棋盘,受到【性质1】和【性质2】影响,只需要改动某一行和某一列变成0-1交替,其他行和列也会变成0-1交替的状态。
所以二维问题就变成了两个一维的问题。在这里只讨论第一行和第一列,即只需要分别将矩阵的第一行变为最终状态和第一列变为最终状态,最终的矩阵一定为合法“棋盘”。
只需要求出交换第一列和第一行使之称为0-1交替状态的步数之和即可。又因为是交换操作,只会用0交换1,用1交换0;所以只需要计算出需要保留的1的个数,再用1的总数-保留的个数就是需要交换的步数了。0同理。
交换需要分两种情况讨论:
- 当n为偶数,此时最终的合法棋盘有两种可能,即第一行的元素的第一个元素
board
[
0
]
[
0
]
=
0
\textit{board}[0][0] = 0
board[0][0]=0或者
board
[
0
]
[
0
]
=
1
\textit{board}[0][0] = 1
board[0][0]=1。
若选择第一行以0开头,此时只需将偶数位上的 0 0 0全部替换为 1 1 1 即可;同理选择将第 1 1 1行变为以 1 1 1开头,此时只需将奇数位上的 0 0 0全部替换为 1 1 1即可。 - 若n为奇数,则此时最终的合法棋盘只有一种可能。
如果第一行中0的数目大于1的数目,此时第一行只能变为以0为开头交替的序列,此时我们只需要将偶数位上的0全部变为1;
如果第一行中0的数目小于1的数目,此时第一行只能交换变为以1为开头交替的序列,此时我们只需要将奇数位上的0全部变为1;
由于我们采用
32
32
32位整数表示每一行或者每一列,在快速计算偶数位或者上的
1
1
1的数目时可以采用位运算掩码。比如
32
32
32位整数
x
x
x,我们只保留
x
x
x偶数位上的1,此时我们需要去掉奇数位上的1,此时只需将x与掩码:
(
10101010101010101010101010101010
)
2
=
0
x
A
A
A
A
A
A
A
A
(1010 1010 1010 1010 1010 1010 1010 1010)_2=0xAAAAAAAA
(10101010101010101010101010101010)2=0xAAAAAAAA
相与即可;
我们只保留 x x x奇数位上的 1 1 1,此时我们需要去掉偶数位上的 1 1 1,此时只需将 x x x与掩码:
( 01010101010101010101010101010101 ) 2 = 0 x 55555555 (0101 0101 0101 0101 0101 0101 0101 0101)_2=0x55555555 (01010101010101010101010101010101)2=0x55555555
相与即可。
相与后使用 b i t c o u n t ( ) bit_count() bitcount()计算1的个数就是需要保留的个数了,使用总数-保留数即可得出移动步数。
代码如下:
def getMoves(mask, count):
'''
mask:第一行/列的int变量
count:有几行/列和第一行/列一样
'''
ones = mask.bit_count()#1的个数
if n & 1: # 若长度是奇数
if abs(n - 2 * ones) != 1 or abs(n - 2 * count) != 1:#0和1的数量相差1,不满足则不能变成棋盘
return -1
if ones == n // 2: # 若1的个数是偶数,ones == n//2为1的个数
return n // 2 - (mask & 0xAAAAAAAA).bit_count()#1的个数-需要保留的个数=移动步数
else:#1的个数是奇数,(n+1)//2为1的个数
return (n+1)//2 - (mask & 0x55555555).bit_count()
else:#若长度是偶数
if ones !=n//2 or count != n//2:
return -1
#交换0或者交换0,选最小的
count0 = n // 2 - (mask & 0xAAAAAAAA).bit_count()
count1 = n // 2 - (mask & 0x55555555).bit_count()
return min(count0, count1)
完整代码
class Solution(object):
def movestochessboard(self, board):
"""
:type board: List[List[int]]
:rtype: int
"""
n = len(board)
rowMask = colMask = 0
for i in range(n):
rowMask |= board[0][i] << i
colMask |= board[i][0] << i
reverseRowMask = ((1 << n) - 1) ^ rowMask
reverseColMask = ((1 << n) - 1) ^ colMask
rowCnt = colCnt = 0
for i in range(n):
currRowMask = currColMask = 0
for j in range(n):
currColMask |= board[j][i] << j
currRowMask |= board[i][j] << j
if (currRowMask != rowMask and currRowMask != reverseRowMask) and (currColMask != colMask and currColMask != reverseColMask):
return -1
rowCnt += currRowMask == rowMask
colCnt += currColMask == colMask
def getMoves(mask, count):
ones = mask.bit_count()
if n & 1: # 长度是奇数
if abs(n - 2 * ones) != 1 or abs(n - 2 * count) != 1:
return -1
if ones == n // 2:
# 1是偶数,ones == n//2为1的个数
return n // 2 - (mask & 0xAAAAAAAA).bit_count()
else:
return (n+1)//2 - (mask & 0x55555555).bit_count()
else:
if ones !=n//2 or count != n//2:
return -1
count0 = n // 2 - (mask & 0xAAAAAAAA).bit_count()
count1 = n // 2 - (mask & 0x55555555).bit_count()
return min(count0, count1)
rowMoves = getMoves(rowMask, rowCnt)
colMoves = getMoves(colMask, colCnt)
return -1 if rowMoves == -1 or colMoves == -1 else rowMoves + colMoves
知识点总结
- 当矩阵只有0或者1,可以使用int变量和位移操作压缩存储。
- 判断一个整数是不是偶数:n&1==0则是偶数,反之为奇数
- 存在一个整数
n
u
m
1
num1
num1,二进制表示为:
0
⋯
⋯
⏟
全0
1
⋯
⏟
n
个非0的二进制数
\underbrace{0\cdots \cdots}_{\text{全0}}\underbrace{1\cdots }_{n\text{个非0的二进制数}}
全0
0⋯⋯n个非0的二进制数
1⋯。
获取 n u m 2 num2 num2,二进制表示为 n u m 1 num1 num1的反码:
(1) n u m 2 = 0 num2=0 num2=0
(2)将 n u m 2 num2 num2前 n n n位变成 1 1 1: ( n u m 2 < < n ) − 1 (num2<<n)-1 (num2<<n)−1
(3)与二进制进行异或^操作: ( n u m 2 = n u m 1 ˆ n u m 2 ) (num2 = num1\ \^\ num2) (num2=num1 ˆnum2) - 计算某整数二进制表示中1的个数: n u m . b i t _ c o u n t ( ) num.bit\_count() num.bit_count()
- 可以使用异或操作+特定的数字+ n u m . b i t _ c o u n t ( ) num.bit\_count() num.bit_count()计算0或1的个数