Leetcode: Game of Life
According to the Wikipedia's article: "The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970." Given a board with m by n cells, each cell has an initial state live (1) or dead (0). Each cell interacts with its eight neighbors (horizontal, vertical, diagonal) using the following four rules (taken from the above Wikipedia article): Any live cell with fewer than two live neighbors dies, as if caused by under-population. Any live cell with two or three live neighbors lives on to the next generation. Any live cell with more than three live neighbors dies, as if by over-population.. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction. Write a function to compute the next state (after one update) of the board given its current state. Follow up: Could you solve it in-place? Remember that the board needs to be updated at the same time: You cannot update some cells first and then use their updated values to update other cells. In this question, we represent the board using a 2D array. In principle, the board is infinite, which would cause problems when the active area encroaches the border of the array. How would you address these problems?
编解码法
复杂度
时间 O(NN) 空间 O(1)
思路
最简单的方法是再建一个矩阵保存,不过当inplace解时,如果我们直接根据每个点周围的存活数量来修改当前值,由于矩阵是顺序遍历的,这样会影响到下一个点的计算。如何在修改值的同时又保证下一个点的计算不会被影响呢?实际上我们只要将值稍作编码就行了,因为题目给出的是一个int矩阵,大有空间可以利用。这里我们假设对于某个点,值的含义为
[2nd bit, 1st bit] = [next state, current state]
- 00 dead (next) <- dead (current)
- 01 dead (next) <- live (current)
- 10 live (next) <- dead (current)
- 11 live (next) <- live (current)
To get the current state, simply do
board[i][j] & 1
To get the next state, simply do
board[i][j] >> 1
1 public class Solution { 2 int[][] directions = new int[][]{{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}; 3 public void gameOfLife(int[][] board) { 4 if (board==null || board.length==0 || board[0].length==0) return; 5 int m = board.length; 6 int n = board[0].length; 7 for (int i=0; i<m; i++) { 8 for (int j=0; j<n; j++) { 9 int lives = 0; 10 for (int[] dir : directions) { 11 int x = i + dir[0]; 12 int y = j + dir[1]; 13 if (x>=0 && x<m && y>=0 && y<n) { 14 if ((board[x][y] & 1) == 1) lives++; 15 } 16 } 17 18 //update board[i][j] 19 if (board[i][j] == 0) { 20 if (lives == 3) board[i][j] = 2; 21 else board[i][j] = 0; 22 } 23 else { //board[i][j] == 1 24 if (lives<2 || lives>3) board[i][j] = 1; 25 else board[i][j] = 3; 26 } 27 } 28 } 29 30 //decode 31 for (int i=0; i<m; i++) { 32 for (int j=0; j<n; j++) { 33 board[i][j] >>= 1; 34 } 35 } 36 } 37 }
Follow up1:
In this question, we represent the board using a 2D array. In principle, the board is infinite, which would cause problems when the active area encroaches the border of the array. How would you address these problems?
Wiki: Alternatively, the programmer may abandon the notion of representing the Life field with a 2-dimensional array, and use a different data structure, like a vector of coordinate pairs representing live cells
Reference: What I do is I have the coordinates of all living cells in a set. Then I count the living neighbors of all cells by going through the living cells and increasing the counter of their neighbors. Afterwards I just collect the new set of living cells by picking those with the right amount of neighbors.
1 private Set<Coord> gameOfLife(Set<Coord> live) { 2 Map<Coord,Integer> neighbours = new HashMap<>(); 3 for (Coord cell : live) { 4 for (int i = cell.i-1; i<cell.i+2; i++) { 5 for (int j = cell.j-1; j<cell.j+2; j++) { 6 if (i==cell.i && j==cell.j) continue; 7 Coord c = new Coord(i,j); 8 if (neighbours.containsKey(c)) { 9 neighbours.put(c, neighbours.get(c) + 1); 10 } else { 11 neighbours.put(c, 1); 12 } 13 } 14 } 15 } 16 Set<Coord> newLive = new HashSet<>(); 17 for (Map.Entry<Coord,Integer> cell : neighbours.entrySet()) { 18 if (cell.getValue() == 3 || cell.getValue() == 2 && live.contains(cell.getKey())) { 19 newLive.add(cell.getKey()); 20 } 21 } 22 return newLive;
where Coord is:
1 private static class Coord { 2 int i; 3 int j; 4 private Coord(int i, int j) { 5 this.i = i; 6 this.j = j; 7 } 8 public boolean equals(Object o) { 9 return o instanceof Coord && ((Coord)o).i == i && ((Coord)o).j == j; 10 } 11 public int hashCode() { 12 int hashCode = 1; 13 hashCode = 31 * hashCode + i; 14 hashCode = 31 * hashCode + j; 15 return hashCode; 16 } 17 }
and the wrapper:
1 public void gameOfLife(int[][] board) { 2 Set<Coord> live = new HashSet<>(); 3 int m = board.length; 4 int n = board[0].length; 5 for (int i = 0; i<m; i++) { 6 for (int j = 0; j<n; j++) { 7 if (board[i][j] == 1) { 8 live.add(new Coord(i,j)); 9 } 10 } 11 }; 12 live = gameOfLife(live); 13 for (int i = 0; i<m; i++) { 14 for (int j = 0; j<n; j++) { 15 board[i][j] = live.contains(new Coord(i,j))?1:0; 16 } 17 }; 18 19 }
Follow Up2: 参见:http://segmentfault.com/a/1190000003819277
如果循环矩阵如何解决?循环的意思是假设一个3x3的矩阵,则a[0][0]
的左边是a[0][2]
,其左上是a[2][2]
这样我们的坐标要多加一个数组长度,使用坐标时还要取模
1 for(int i = 0; i < m; i++){ 2 for(int j = 0; j < n; j++){ 3 int lives = 0; 4 // 多加一个数组长度 5 for(int y = i + m - 1; y <= i + m + 1; y++){ 6 for(int x = j + n - 1; x <= j + n + 1; x++){ 7 // 使用的时候要取模 8 lives += board[y % m][x % n] & 1; 9 } 10 } 11 if(lives == 3 || lives - board[i][j] == 3){ 12 board[i][j] |= 2; 13 } 14 } 15 }
-
如果多核的机器如何优化?
因为是多核,我们可以用线程来实现并行计算。如图,将矩阵分块后,每个线程只负责其所在的分块的计算,不过主线程每一轮都要更新一下这些分块的边缘,并提供给相邻分块。所以这里的开销就是主线程和子线程通信这个边缘信息的开销。如果线程变多分块变多,边缘信息也会变多,开销会增大。所以选取线程的数量是这个开销和并行计算能力的折衷。 -
如果是多台机器如何优化?
同样的,我们可以用一个主机器负责处理边缘信息,而多个子机器处理每个分块的信息,因为是分布式的,我们的矩阵可以分块的存储在不同机器的内存中,这样矩阵就可以很大。而主机在每一轮开始时,将边缘信息通过网络发送给各个分块机器,然后分块机器计算好自己的分块后,把新自己内边缘信息反馈给主机器。下一轮,等主机器收集齐所有边缘后,就可以继续重复。
不过多台机器时还有一个更好的方法,就是使用Map Reduce。Map Reduce的简单版本是这样的,首先我们的Mapper读入一个file,这个file中每一行代表一个存活的节点的坐标(我猜想一个mapper负责多个living cell,而一个reducer对应一个邻居),然后Mapper做出8个Key-Value对,对这个存活节点的邻居cell,分发出一个1。这里Reducer是对应每个cell的,每个reducer累加自己cell得到了多少个1,就知道自己的cell周围有多少存活cell,就能知道该cell下一轮是否可以存活,如果可以存活则分发回mapper的文件中,等待下次读取,如果不能则舍弃。 - 如果要进一步优化Map Reduce,那我们主要优化的地方则是mapper和reducer通信的开销,因为对于每个存活节点,mapper都要向8个reducer发一次信息。我们可以在mapper中用一个哈希表,当mapper读取文件的某一行时,先不向8个reducer发送信息,而是以这8个cell作为key,将1累加入哈希表中。这样等mapper读完文件后,再把哈希表中的cell和该cell对应的累加1次数,分发给相应cell的reducer,这样就可以减少一些通信开销。相当于是现在mapper内做了一次累加。这种优化在只有一个mapper是无效的,因为这就等于直接在mapper中统计完了,但是如果多个mapper同时执行时,相当于在每个mapper里先统计一会,再交给reducer一起统计每个mapper的统计结果。