文章目錄
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 。
解題報告
dp[i][j]
表示 [i~j]
這個區間取石子,先手
這個石子游戲取石子有兩個方向,所以我們需要在一個區間內進行分析。
實現代碼
class Solution{
public:
bool stoneGame(vector<int>&piles){
int len=piles.size(),j,sum=0;
vector<vector<pair<int,int>>>dp(len,vector<pair<int, int>>(len, {0,0}));
for(int i=0;i<len;i++){
dp[i][i].first=piles[i];
sum+=piles[i];
}
for(int dis=1;dis<len;dis++){
for(int i=0;i<len&&i+dis<len;i++){
j=i+dis;
int left = piles[i] + dp[i+1][j].second;
int right = piles[j] + dp[i][j-1].second;
// 套用狀態轉移方程
if (left > right) {
dp[i][j].first = left;
dp[i][j].second = dp[i+1][j].first;
}
else {
dp[i][j].first = right;
dp[i][j].second = dp[i][j-1].first;
}
}
}
return dp[0][len-1].first>dp[0][len-1].second;
}
};
Leetcode 1140. 石子游戲 II
問題描述
亞歷克斯和李繼續他們的石子游戲。許多堆石子 排成一行,每堆都有正整數顆石子 piles[i]
。遊戲以誰手中的石子最多來決出勝負。
亞歷克斯和李輪流進行,亞歷克斯先開始。最初,M = 1
。
在每個玩家的回合中,該玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M
。然後,令 M = max(M, X)
。
遊戲一直持續到所有石子都被拿走。
假設亞歷克斯和李都發揮出最佳水平,返回亞歷克斯可以得到的最大數量的石頭。
示例:
輸入:piles = [2,7,9,4,4]
輸出:10
解釋:
如果亞歷克斯在開始時拿走一堆石子,李拿走兩堆,接着亞歷克斯也拿走兩堆。在這種情況下,亞歷克斯可以拿到 2 + 4 + 4 = 10 顆石子。
如果亞歷克斯在開始時拿走兩堆石子,那麼李就可以拿走剩下全部三堆石子。在這種情況下,亞歷克斯可以拿到 2 + 7 = 9 顆石子。
所以我們返回更大的 10。
提示:
1 <= piles.length <= 100
1 <= piles[i] <= 10 ^ 4
解題報告
dp[i][j]
表示剩餘[i : len - 1]
堆時,M = j
的情況下,先取的人能獲得的最多石子數。
轉移方程爲:
i + 2M >= len
時,dp[i][M] = sum[i : len - 1]
, 剩下的堆數能夠直接全部取走,那麼最優的情況就是剩下的石子總和i + 2M < len
時,dp[i][M] = max(dp[i][M], sum[i : len - 1] - dp[i + x][max(M, x)])
, 其中1 <= x <= 2M
,剩下的堆數不能全部取走,那麼最優情況就是讓下一個人取的更少。對於我所有的x
取值,下一個人從x
開始取起,M
變爲max(M, x)
,所以下一個人能取dp[i + x][max(M, x)]
,我最多能取sum[i : len - 1] - dp[i + x][max(M, x)]
。
實現時,需要兩重循環來計算 dp[i][j]
, 其中計算 dp[i][j]
具體的值時,還需要一重循環枚舉當前先手取多少堆石子,所以一共是三重循環。
其中 i
的取值範圍是 len-1~0
,j
的取值範圍是 1~len/2+1
,因爲我們在計算 dp[i][j]
時是通過 dp[i+x][max(M, x)]
轉化來的,而 i<=2*M
,所以 i+2*M
必定是小於 len
,所以 M
必定是小於 len/2
的。
實現代碼
class Solution {
public:
int stoneGameII(vector<int>& piles) {
int len = piles.size();
int sum = 0;
// dp[i][j]表示當前是第i波,m=j;
vector<vector<int>>dp(len, vector<int>(len, 0));
for(int i = len - 1; i >= 0; i--){
sum += piles[i];// 表示當前所剩下的所有棋子的和
for(int M = 1; M <len/2+1; M++){
if(i + 2 * M-1 >=len-1){
dp[i][M] = sum;
continue;
}
for(int x=1;x<=2*M&&i+x<len;x++){
dp[i][M] = max(dp[i][M], sum - dp[i + x][max(M, x)]);
}
}
}
return dp[0][1];
}
};
這裏有一個疑問??
for(int x=1;x<=2*M&&i+x<len;x++){
dp[i][M] = max(dp[i][M], sum - dp[i + x][max(M, x)]);
}
爲什麼 i+x<len
[蘊含 dp
的第一維大小爲 len
]可以,i+x<=len
[蘊含 dp
的第二維大小爲 len+1
] 也可以呢??
Leetcode 1406. 石子游戲 III
問題描述
Alice 和 Bob 用幾堆石子在做遊戲。幾堆石子排成一行,每堆石子都對應一個得分,由數組 stoneValue 給出。
Alice 和 Bob 輪流取石子,Alice 總是先開始。在每個玩家的回合中,該玩家可以拿走剩下石子中的的前 1、2 或 3 堆石子 。比賽一直持續到所有石頭都被拿走。
每個玩家的最終得分爲他所拿到的每堆石子的對應得分之和。每個玩家的初始分數都是 0 。比賽的目標是決出最高分,得分最高的選手將會贏得比賽,比賽也可能會出現平局。
假設 Alice 和 Bob 都採取 最優策略 。如果 Alice 贏了就返回 “Alice” ,Bob 贏了就返回 “Bob”,平局(分數相同)返回 “Tie” 。
示例 1:
輸入:values = [1,2,3,7]
輸出:“Bob”
解釋:Alice 總是會輸,她的最佳選擇是拿走前三堆,得分變成 6 。但是 Bob 的得分爲 7,Bob 獲勝。
提示:
- 1 <= values.length <= 50000
- -1000 <= values[i] <= 1000
解題報告
dp[i]
表示剩餘[i : len - 1]
堆時,先取的人能獲得的最多石子數。
很明顯,dp[i]=max(sum-dp[i+1], sum-dp[i+2], sum-dp[i-3])
實現代碼
錯誤實現:
class Solution {
public:
string stoneGameIII(vector<int>& stoneValue) {
int len=stoneValue.size(),sum=stoneValue[len-1];
vector<int>dp(len, 0);
dp[len-1]=stoneValue[len-1];
for(int i=len-2;i>=0;i--){
dp[i]=INT_MIN;
sum+=stoneValue[i];
if(i+2>=len-1) dp[i]=sum;
else {
for(int j=1;j<=3&&j+i<len;j++){
dp[i]=max(dp[i], sum-dp[i+j]);
}
}
}
if(dp[0]>sum-dp[0]) return "Alice";
else if(dp[0]<sum-dp[0]) return "Bob";
else return "Tie";
}
};
這種實現完全模仿第二題的,但是這道題 -1000 <= values[i] <= 1000
,當 i+3>=len
時,不是後面的石子均取走。
由於石子堆中有負數,所以 dp[]
初始化 INT_MIN
。
因爲 dp[]
初始化 INT_MIN
,所以 dp[]
的大小爲 len+1
,因爲最後只剩下 [1,3]
堆石子時,可以一次性取完
class Solution {
public:
string stoneGameIII(vector<int>& stoneValue) {
int len=stoneValue.size(),sum=stoneValue[len-1];
vector<int>dp(len+1, 0);
dp[len-1]=stoneValue[len-1];
for(int i=len-2;i>=0;i--){
dp[i]=INT_MIN;
sum+=stoneValue[i];
for(int j=1;j<=3&&j+i<=len;j++){
dp[i]=max(dp[i], sum-dp[i+j]);
}
}
if(dp[0]>sum-dp[0]) return "Alice";
else if(dp[0]<sum-dp[0]) return "Bob";
else return "Tie";
}
};
for(int j=1;j<=3&&j+i<=len;j++){
dp[i]=max(dp[i], sum-dp[i+j]);
}
這裏,j=1
表示當前先手只取一堆石頭 stoneValue[i]
,後手從 stoneValue[]
的第 i+1
堆石頭開始取。
總結
- 第一個題目可以在兩個方向取石子,所以需要兩維的數組來分析取物。
第二個和第三個題目只能在一個方向取石子,只要一維就可以分析取物,但是第二題取石子的堆數是不確定的,而且取石子的堆數在較大的範圍內變動,所以需要第二維來分析取多少堆石子。第三題取石子的堆數也是不確定的,但是隻有三個取值,所以一維數組就可以解決問題。 - 這種博弈題目,在轉移方程中需要體現博弈的過程,第一題通過給
dp
的每個元素設置成pair<int, int>
來區分前後手;而第二題和第三題均是“爲了使得先手得到最優值,後手接下來走的一步在某個範圍是最差的”
參考資料
[1] Leetcode 877.石子游戲
[2] Leetcode 877 題解區:labuladong
[3] Leetcode 1140. 石子游戲 II
[4]Leetcode 1140 題解區:zyh518
[5] Leetcode 1406. 石子游戲 III