這道演算法題其實並不難,如果你把文章從頭到尾看完的話基本上能看懂,但如果你看到最後的話大概率會說一句:這是什麼沙雕題目?!
題目來源於 LeetCode 第 877 號問題:石子游戲。
為了更好理解,我改編了一下題目,描述是這樣的:
題目描述
喜羊羊和灰太狼用幾堆石子在做遊戲。偶數堆石子排成一行,每堆都有正整數顆石子 piles[i]
。
遊戲以誰手中的石子最多來決出勝負。石子的總數是奇數,所以沒有平局。
喜羊羊和灰太狼輪流進行,喜羊羊先開始。 每回合,玩家從行的開始或結束處取走整堆石頭。 這種情況一直持續到沒有更多的石子堆為止,此時手中石子最多的玩家獲勝。
假設喜羊羊和灰太狼都發揮出最佳水平,當喜羊羊贏得比賽時返回 true
,當灰太狼贏得比賽時返回 false
。
題目分析
舉兩個例子來幫助理解題意。
例子一:
輸入:[ 5,3,4,5 ]
輸出:true
解釋:
喜羊羊先開始,只能拿前 5 顆或後 5 顆石子 。
假設他取了前 5 顆,這一行就變成了 [ 3 ,4,5 ] 。
如果灰太狼拿走前 3 顆,那麼剩下的是 [ 4,5 ],喜羊羊拿走後 5 顆贏得 10 分。
如果灰太狼拿走後 5 顆,那麼剩下的是 [ 3,4 ],喜羊羊拿走後 4 顆贏得 9 分。
這表明,取前 5 顆石子對喜羊羊來說是一個勝利的舉動,所以我們返回 true 。
例子二:
輸入:[ 5,10000,2,3 ]
輸出:true
解釋:
喜羊羊先開始,只能拿前 5 顆或後 3 顆石子 。
假設他取了後 3 顆,這一行就變成了 [ 5,10000,2 ]。
灰太狼肯定會在剩下的這一行中取走前 5 顆,這一行就變成了 [ 10000,2 ]。
然後喜羊羊取走前 10000 顆,總共贏得 10003 分,灰太狼贏得 7 分。
這表明,取後 3 顆石子對喜羊羊來說是一個勝利的舉動,所以我們返回 true 。
這個例子表明,並不是需要每次都挑選最大的那堆石頭。
題目回答
涉及到最優解的問題,那麼肯定要去嘗試一下使用 **動態規劃 **來解決了。
先看一下力扣的正規題解:
讓我們改變遊戲規則,使得每當灰太狼得分時,都會從喜羊羊的分數中扣除。
令 dp(i, j)
為喜羊羊可以獲得的最大分數,其中剩下的堆中的石子數是 piles[i], piles[i+1], ..., piles[j]
。這在比分遊戲中很自然:我們想知道遊戲中每個位置的值。
我們可以根據 dp(i + 1,j)
和 dp(i,j-1)
來制定 dp(i,j)
的遞迴,我們可以使用動態程式設計以不重複這個遞迴中的工作。(該方法可以輸出正確的答案,因為狀態形成一個DAG(有向無環圖)。)
當剩下的堆的石子數是 piles[i], piles[i+1], ..., piles[j]
時,輪到的玩家最多有 2 種行為。
可以通過比較 j-i
和 N modulo 2
來找出輪到的人。
如果玩家是喜羊羊,那麼它將取走 piles[i]
或 piles[j]
顆石子,增加它的分數。之後,總分為 piles[i] + dp(i+1, j)
或 piles[j] + dp(i, j-1)
;我們想要其中的最大可能得分。
如果玩家是灰太狼,那麼它將取走 piles[i]
或 piles[j]
顆石子,減少喜羊羊這一數量的分數。之後,總分為 -piles[i] + dp(i+1, j)
或 -piles[j] + dp(i, j-1)
;我們想要其中的最小可能得分。
程式碼如下:
上面的程式碼並不算複雜,當然,如果你看不懂也沒關係,不影響解決問題,請看下面的數學分析。
數學分析
因為石頭的數量是奇數,因此只有兩種結果,輸或者贏。
喜羊羊先開始拿石頭,隨便拿!然後比較石頭數量:
- 如果石頭數量多於對手,贏了;
- 如果石頭數量少於對手,自己拿石頭的順序和對手拿石頭的順序對調,還是贏。
所以程式碼如下:
class Solution {
public boolean stoneGame(int[] piles) {
return true;
}
}
複製程式碼
看完之後,你的心情是怎麼樣的?
此題的LeetCode 的評論區裡一片吐槽:這是什麼沙雕題目!
可能搞過 ACM 等競賽的人都會微微一笑:不會幾萬個套路怎麼好意思說自己是 acmer 。我們這些普通人為之驚奇的題目,到他們這裡就是徹底被玩壞了,各種稀奇古怪的秒解。