877. 石子游戲
877. 石子游戲
1. 題目描述
題目連結
亞歷克斯和李用幾堆石子在做遊戲。偶數堆石子排成一行,每堆都有正整數顆石子 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 。
提示:
2 <= piles.length <= 500
piles.length 是偶數。
1 <= piles[i] <= 500
sum(piles) 是奇數。
2. 題目分析
剛看到這題目時,我單純的以為只要使用貪心演算法,每次在開始處和結束處選擇更大的那堆石子,最後拿到總的石子數就會是最多的,後面發現有一些用例測試過不去。所以重新思考這個流程,發現,這個題目要求的是的全域性最優解,而不是區域性最優解,我們如果使用貪心演算法,找到的只是區域性最優解,但不一定是全域性最優解。
比如說[6,9,4,2],貪心演算法的話,第一人首先會選擇6,那麼第二人則選擇9,第一人再選擇4,第二人會選擇2,那麼最終第一人獲得石子數:6+4=10;第二人:9+2=11;11 > 10,所以第二人會獲勝,這顯然不是我們不要的結果。
那如果找全域性最優解呢?其中困擾我的一點是,我並不知道當前的決定是如何影響後面的結果,那我要怎麼樣才能找到最優解。同時,我要考慮讓第一人每次拿到最優解,又要讓第二人也是每次拿到最優解,想著想著就把自己繞暈了。。。。
這說明我動態規劃的思想還沒完全成熟,很難在這種遞迴子問題中旅順一條最優解的思路【藍瘦,香菇】。
像這種有兩個角色的問題,我一開始思考問題是,分別從兩個角色的角度思考,那麼就會有兩個變數,所以想著想著,自己都亂了,更不用說找到實現問題的編碼思路了。所以針對這種問題,最好是隻從一個角度出發,既然另外一個角色會依賴第一個角色的決定,那麼用替換法,通過一個第一角度替換第二角色的角度。
比如說這個選擇石子的問題,我們每次只能拿兩端的石頭堆的石頭,但我們又不知道拿完後剩下的石頭堆的情況,因此我們考慮先解決子問題。例如我們求出2個相鄰石頭堆的勝負情況,我們可以根據求出的資料求出相鄰3個石頭堆的勝負情況,以此類推,我們可以根據n-1個相鄰石頭堆的勝負情況,求出n個相鄰石頭堆的勝負情況,即我們的原問題。
每次取石頭堆只能從兩端取,那麼第一個可以選擇的石子堆的方式有兩種:
- 第一種:第一個人拿第i堆,那麼第二個就可以從第(i+1)堆和第(j)堆中選擇最優的石子堆(前面已經分析過了,不一定是更多的那一堆);
- 第二種:第一個人拿第j堆,那麼第二個就可以從第(i)堆和第(j-1)堆中選擇最優的石子堆(前面已經分析過了,不一定是更多的那一堆);
因此,第一人選擇的時候,必須選擇這兩種方法中的最優石子堆。
根據我們的類推,我們可以設:
dp[i][j]表示為,從第i堆到第j堆石子中,第一個人最多可以比第二個人多拿到的石子數,即在一次選擇石頭堆的過程中,有兩個步驟,第一個人在第(i+1)堆和第(j)堆中選擇最優的石子堆,那麼第二個就會根據第一人的選擇,從剩下的石頭堆中選擇最優的石子堆,然後兩者相減,差值取最大,那麼這個結果就是當前第一個人做出的最優選擇。
狀態轉換方程是:dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1])。
- piles[i] - dp[i+1][j]表示第一人取走第(i)堆的石頭堆, dp[i+1][j]表示第二個就可以從第(i+1)堆和第(j)堆中選擇最優的石子堆,然後piles[i] - dp[i+1][j]表示第一個人最多可以比第二個人多拿到的石子數;
- piles[j] - dp[i][j-1]表示第一人取走第(j)堆的石頭堆,dp[i][j-1]表示第二個就可以從第(i)堆和第(j-1)堆中選擇最優的石子堆,然後piles[j] - dp[i][j-1]表示第一個人最多可以比第二個人多拿到的石子數;
- 對於dp[i][i]來說,就是隻有一堆石子可供選擇,那麼第一個直接選擇這一堆,自然是piles[i]本身;
3. 解決思路
狀態:dp[i][j]表示為,從第i堆到第j堆石子中,第一個人最多可以比第二個人多拿到的石子數。
狀態轉換方程是:dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1])
另外一定小機靈的分析方法,既然根據題目提供的條件,偶數堆+總數為奇數,說明一定存在有個勝利者,那麼就一定存在一條選擇方式能保證順利到達勝利,所以只要第一個人選擇這條路徑,那麼也能一定獲得勝利。也就是說,第一個開始的人選擇最優解一定會贏。
4. 程式碼實現(java)
- 方法一:
package com.algorithm.leetcode.dynamicAllocation;
/**
* Created by 凌 on 2018/12/12.
* 描述:877. 石子游戲
*/
public class StoneGame {
public static void main(String[] args) {
// int[] nums = {5,3,4,5};
// int[] nums = {4,5,7,9,2,6};
int[] nums = {3,7,2,3};
boolean result = stoneGame(nums);
System.out.println(result);
}
public static boolean stoneGame(int[] piles) {
int len = piles.length;
int[][] dp = new int[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = piles[i];
}
//依次計算相鄰2個石頭堆到n個石頭堆的情形
//兩層迴圈的意思:每次外層迴圈控制i到j中間的數量,j是從第2堆到第n堆,第二層迴圈控制從第一堆(下標為0)到第dis堆
for (int dis = 1; dis < len; dis++) {
for (int i = 0; i < len-dis; i++) {
int j = dis+i;
dp[i][j] = Math.max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]);
}
}
return dp[0][len-1] > 0;
}
}
- 方法二:
public static boolean stoneGame(int[] piles) {
return true;
}