Leetcode 289:生命遊戲(最詳細的解法!!!)
根據百度百科,生命遊戲,簡稱為生命,是英國數學家約翰·何頓·康威在1970年發明的細胞自動機。
給定一個包含 m × n 個格子的面板,每一個格子都可以看成是一個細胞。每個細胞具有一個初始狀態 live(1)即為活細胞, 或 dead(0)即為死細胞。每個細胞與其八個相鄰位置(水平,垂直,對角線)的細胞都遵循以下四條生存定律:
- 如果活細胞周圍八個位置的活細胞數少於兩個,則該位置活細胞死亡;
- 如果活細胞周圍八個位置有兩個或三個活細胞,則該位置活細胞仍然存活;
- 如果活細胞周圍八個位置有超過三個活細胞,則該位置活細胞死亡;
- 如果死細胞周圍正好有三個活細胞,則該位置死細胞復活;
根據當前狀態,寫一個函式來計算面板上細胞的下一個(一次更新後的)狀態。下一個狀態是通過將上述規則同時應用於當前狀態下的每個細胞所形成的,其中細胞的出生和死亡是同時發生的。
示例:
輸入:
[
[0,1,0],
[0,0,1],
[1,1,1],
[0,0,0]
]
輸出:
[
[0,0,0],
[1,0,1],
[0,1,1],
[0,1,0]
]
進階:
- 你可以使用原地演算法解決本題嗎?請注意,面板上所有格子需要同時被更新:你不能先更新某些格子,然後使用它們的更新後的值再更新其他格子。
- 本題中,我們使用二維陣列來表示面板。原則上,面板是無限的,但當活細胞侵佔了面板邊界時會造成問題。你將如何解決這些問題?
解題思路
類似問題
這個問題非常有意思,有興趣的同學可以將其做成視覺化看看效果,我在後期會給出視覺化程式碼示例。我們回到這個問題,這個問題非常簡單,由於細胞的出生和死亡是同時發生的(和之前問題類似,如果遍歷前面元素直接修改結果會對後面的元素造成影響),所以最簡單的做法就是新建一個matrix
。
class Solution:
def gameOfLife(self, board):
"""
:type board: List[List[int]]
:rtype: void Do not return anything, modify board in-place instead.
"""
self.w, self.h = len(board[0]), len(board)
mat = [[0]*self.w for _ in range(self.h)]
for r in range(self.h):
for c in range(self.w):
mat[r][c] = board[r][c]
for r in range(self.h):
for c in range(self.w):
lives = self._neighborsLives(mat, (r, c))
if lives < 2:
board[r][c] = 0
if lives > 3:
board[r][c] = 0
if lives == 3 and not board[r][c]:
board[r][c] = 1
def _neighborsLives(self, board, node):
count = 0
neighbors = [(0, 1), (0, -1), (1, 0), (-1, 0), (-1, -1), (1, 1), (-1, 1), (1, -1)]
for i, j in neighbors:
if 0 <= node[0] + i < self.h and\
0 <= node[1] + j < self.w:
count += board[node[0]+i][node[1]+j] & 1
return count
如果不是用額外空間該怎麼做?我們可以通過狀態轉換(在之前的問題中我們已經不止一次使用這種策略了),我們知道原先通過1bit
只能記錄當前是死還是活,無法記錄之前的狀態,如果想要記錄之前的狀態,我們只有增大資訊的容量,增大為多少呢?2bit
就可以啦。
- die <- die 00
- die <- live 01
- live <- die 10
- live <- live 11
我們這裡使用2bit
表示狀態變化(current to next)。我們先遍歷board
,看每個節點周圍有多少個初始狀態是live
,如果這個節點原先是live
並且周圍的2<=lives<4
,我們知道它現在的狀態應該是live->live
,也就是對應上面的編碼3
。如果這個節點原先是die
並且周圍是lives==3
,我們知道它現在的狀態應該是die->live
,也就是對應上面的編碼2
。這些都做完之後,我們在此遍歷board
,將其中的2
和3
調整回來(live or die
),怎麼調呢?很簡單,我們注意到我們的2
和3
編碼很巧妙,直接>>1
就可以得到一次遊戲後的狀態了。
class Solution:
def gameOfLife(self, board):
"""
:type board: List[List[int]]
:rtype: void Do not return anything, modify board in-place instead.
"""
r, c = len(board), len(board[0])
neighbors = [(0, 1), (0, -1), (1, 0), (-1, 0), (-1, -1), (1, 1), (-1, 1), (1, -1)]
for i in range(r):
for j in range(c):
lives = 0
for m, n in neighbors:
if 0 <= i + m < r and 0 <= j + n < c:
lives += board[i+m][j+n] & 1
if board[i][j] and 2 <= lives < 4:
board[i][j] = 3
if not board[i][j] and lives == 3:
board[i][j] = 2
for i in range(r):
for j in range(c):
board[i][j] >>= 1
要注意我們在Leetcode 73:矩陣置零(最詳細的解法!!!)問題中沒有采用這種增大資訊量的方式,因為我們可以對問題進行全域性考慮(也就是一旦輸入結果確定,我們就會知道輸出結果)。而這個問題按理說應該是可以通過全域性考慮的,但是很麻煩,遠沒有通過增大資訊量來解容易。那麼之前那個問題應該也可以通過增大資訊量來接吧?大家可以試試。
如果矩陣無限大我們該怎麼辦?我們希望我們儲存的資訊儘可能少,所以我們可以只記錄存活節點的座標,每次更新記錄存活節點座標的陣列。
class Solution:
def gameOfLife(self, board):
"""
:type board: List[List[int]]
:rtype: void Do not return anything, modify board in-place instead.
"""
live = {(i, j) for i, row in enumerate(board) for j, live in enumerate(row) if live}
live = self.gameOfLifeInfinite(live)
for i, row in enumerate(board):
for j in range(len(row)):
row[j] = int((i, j) in live)
def gameOfLifeInfinite(self, live):
neighbors = collections.Counter()
for i, j in live:
for I in (i-1, i, i+1):
for J in (j-1, j, j+1):
if I != i or J != j:
neighbors[I, J] += 1
new_live = set()
for key, val in neighbors.items():
if val == 3 or val == 2 and key in live:
new_live.add(key)
return new_live
reference:
https://leetcode.com/problems/game-of-life/discuss/73223/Easiest-JAVA-solution-with-explanation
https://segmentfault.com/a/1190000003819277#articleHeader1
https://leetcode.com/problems/game-of-life/discuss/73217/Infinite-board-solution
我將該問題的其他語言版本新增到了我的GitHub Leetcode
如有問題,希望大家指出!!!