最近閑來無事刷LeetCode,發現這道題的Accept Rate還是挺高的,嘗試着做了一下,結果悲劇了,把過程寫下來,希望能長點記性。該題的描述翻譯成中文如下:
你正在和你的朋友玩尼姆游戲(Nim Game): 桌子上有一堆石塊,你和你的朋友輪流去拿這些石塊,每次只能拿1塊、2塊或者3塊。在石塊被拿光前,最后一次拿到石塊的人獲勝。你將首先去拿這些石塊。 你和你的朋友都非常聰明,並且擁有應對該游戲的最佳策略。寫一個函數來決定在給定石塊數量的情況下,你是否能夠獲勝。比如:如果桌子上有4塊石塊,那么你將不可能獲勝:不管你先拿出1塊、2塊還是3塊石頭,最后一次拿光石塊的永遠都會是你的朋友。(There is a heap of stones on the table, each time one of you take turns to remove 1 to 3 stones. The one who removes the last stone will be the winner. You will take the first turn to remove the stones.Both of you are very clever and have optimal strategies for the game. Write a function to determine whether you can win the game given the number of stones in the heap.For example, if there are 4 stones in the heap, then you will never win the game: no matter 1, 2, or 3 stones you remove, the last stone will always be removed by your friend.)
拿到題目后,習慣性地點開Hint看了一下:
Hint:
- 如果桌子上有5塊石頭,你能找到確保你會獲勝的拿石塊的方法嗎?(If there are 5 stones in the heap, could you figure out a way to remove the stones such that you will always be the winner?)
第一反應自然是遞歸了,於是寫下了下面的code:
1 public static boolean canWinNim_recurion(int stoneCount) { 2 if(stoneCount<=0) { 3 System.out.println("illegal input"); 4 return false; 5 } 6 /* you will win */ 7 if((0<stoneCount)&&(stoneCount<4)) { 8 return true; 9 } 10 11 return !(canWinNim_recurion(stoneCount-1)&&canWinNim_recurion(stoneCount-2)&&canWinNim_recurion(stoneCount-3)); 12 }
提交后給出StackOverflowError,自己在本機上用大數(1348820612)試驗了一下也是StackOverflowError。原因自然是遞歸太深了。於是又寫下了下面的code:
1 public static boolean canWinNim(int stoneCount) { 2 if(stoneCount<=0) { 3 System.out.println("illegal input"); 4 return false; 5 } 6 7 boolean[] canWinArray = new boolean[stoneCount]; 8 for(int i=1;i<=stoneCount;i++) { 9 if(i==1||i==2||i==3) { 10 canWinArray[i-1] = true; 11 continue; 12 } 13 canWinArray[i-1] = !(canWinArray[i-2]&&canWinArray[i-3]&&canWinArray[i-4]); 14 } 15 return canWinArray[stoneCount-1]; 16 }
這一次的思路是把遞歸改成循環,而且為了簡單起見,是自底向上的循環。沒記錯的話這其實是一個DP的解法。但是提交后,給出Memory Limit Exceeded錯誤,那就是O(n)的空間復雜度不符合要求了。於是給出了下面的空間復雜度為常量的code:
1 public boolean canWinNim2(int stoneCount) { 2 boolean canWin = false; 3 4 if(stoneCount<=0) { 5 System.out.println("illegal input"); 6 return false; 7 } 8 9 /* only need a boolean array of length 3 ? */ 10 //boolean[] canWinArray = new boolean[stoneCount]; 11 boolean[] last3CanWinArray = new boolean[3]; 12 13 for(int i=1;i<=stoneCount;i++) { 14 if(i==1||i==2||i==3) { 15 last3CanWinArray[i-1] = true; 16 canWin = true; 17 continue; 18 } 19 canWin = !(last3CanWinArray[0]&&last3CanWinArray[1]&&last3CanWinArray[2]); 20 // update the array 21 last3CanWinArray[0] = last3CanWinArray[1]; // the index cannot be i-2 i-3 etc. 22 last3CanWinArray[1] = last3CanWinArray[2]; 23 last3CanWinArray[2] = canWin; 24 } 25 return canWin; 26 }
但是提交后提示Time Limit Exceeded。這個算法的時間復雜度為O(n),難道還不符合要求嗎? 百思不得其解,google了一下,發現這道題的考察意圖在找規律而不在寫code。然后終於找到了規律:被4整除的石頭數,都不可能贏;其他數則能贏。那么代碼就太簡單了,一行就搞定:
1 public boolean canWinNim_brainteaser(int stoneCount) { 2 return stoneCount%4!=0; 3 }
提交后提示成功,至此算是把這題刷完了。
失敗總結:其實,在這道題的描述和Hint后頭,有一個tag標簽,點開可以看到清清楚楚地寫着:brainteaser。也就是腦筋急轉彎。而我還老是往DP方面去想,結果自然是悲劇了。刷這道題給了我一個教訓:就像高中時刷題一樣,對待算法題一定要注意審題,不漏掉題目給出的每一個細節,重視每一個給出的提示,並按照提示給出的方向去思考。