石子游戏Ⅱ【LC1140】
爱丽丝和鲍勃继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子
piles[i]
。游戏以谁手中的石子最多来决出胜负。爱丽丝和鲍勃轮流进行,爱丽丝先开始。最初,
M = 1
。在每个玩家的回合中,该玩家可以拿走剩下的 前
X
堆的所有石子,其中1 <= X <= 2M
。然后,令M = max(M, X)
。游戏一直持续到所有石子都被拿走。
假设爱丽丝和鲍勃都发挥出最佳水平,返回爱丽丝可以得到的最大数量的石头。
就是说还挺难的
记忆化搜索+dp
-
思路:使用记忆化搜索每个可能的状态,并记录至dp数组中,定义 d f s ( i , m ) dfs(i, m) dfs(i,m) 表示从 p i l e s [ i ] piles[i] piles[i]开始取石子,每次最多取 2 ∗ m 2*m 2∗m堆石子,可以获得的最大石子数量
那么 d f s ( 0 , 1 ) dfs(0, 1) dfs(0,1) 即为最终结果
-
如何求得可以获得的最大石子数量?
对于每个节点,由于剩余的石子总数是固定的,如果拿了某几堆石子后,对手能得到的石子数最少,那么自己能得到的石子数就是最多的。【保证每位选手发挥出最佳水平】
-
为了方便求出剩余石子的总数,可以记录在后缀和数组中
-
在搜索过程中,会遇到重复的状态,因此可以记录至dp数组中,再次遇到这个结果时,直接从dp数组中获取
-
-
实现
-
首先预处理后缀和数组 s [ i ] s[i] s[i]:表示 [ i , n − 1 ] [i,n-1] [i,n−1]堆石子总数
-
递归函数设计:
-
参数:当前石堆序号i,截止目前可以一次取的最大数量 m m m,每次最多可以取得 2 m 2m 2m堆
-
返回值:int 能够取得的最大石子数量
-
什么时候返回:已经搜索过该状态时返回 d p [ i ] [ m ] dp[i][m] dp[i][m],剩余石堆全部可以取完时,返回 s [ i ] s[i] s[i]
-
递归逻辑
本轮取 x ∈ [ 1 , 2 m ] x \in [1,2m] x∈[1,2m]堆石子时,对方从第 i + x i+x i+x堆石子开始取 ,能够取的最大石子数量为 d f s ( i + x , m a x ( m , x ) ) dfs(i+x,max(m,x)) dfs(i+x,max(m,x)),那么我方能取最多的石子数量为 s [ i ] − m i n ( d f s ( i + x , m a x ( x , m ) ) ) s[i]-min(dfs(i+x,max(x,m))) s[i]−min(dfs(i+x,max(x,m)))
-
class Solution { int[] s; int[][] dp; public int stoneGameII(int[] piles) { int n = piles.length; s = new int[n]; dp = new int[n][n]; for (int i = n - 1; i >= 0; i--){ s[i] = piles[i] + (i + 1 < n ? s[i + 1] : 0); Arrays.fill(dp[i], -1); } return dfs(0, 1); } public int dfs(int i, int m){ if (i + m * 2 >= s.length) return s[i]; if (dp[i][m]!= -1) return dp[i][m]; int min = Integer.MAX_VALUE; for (int x = 1; x <= 2 * m; x++){ min = Math.min(min, dfs(i + x, Math.max(x, m))); } dp[i][m] = s[i] - min; return dp[i][m]; } }
-
复杂度
- 时间复杂度: O ( n 3 ) O(n^3) O(n3),状态个数为 O ( n 2 ) O(n^2) O(n2),单个状态计算的时间复杂度为 O ( n ) O(n) O(n)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2)
-
-
优化:减小dp数组的内存,m的最大值为 ⌊ n + 1 4 ⌋ \lfloor \frac{n+1}{4}\rfloor ⌊4n+1⌋
如果每次都拿满 2 m 2m 2m堆,那么最后有
( 2 + 4 + 8 + M ) + 2 M < n 4 M − 2 < n 4 M ≤ n + 1 M ≤ ⌊ n + 1 4 ⌋ (2+4+8 + M)+2M <n\\ 4M-2<n\\ 4M\le n+1\\ M \le \lfloor \frac{n+1}{4}\rfloor (2+4+8+M)+2M<n4M−2<n4M≤n+1M≤⌊4n+1⌋
4M-2 是看下一次拿的能不能小于 n,递归过程没有记录递归边界
转化为动态规划
-
动态规划
-
确定dp数组(dp table)以及下标的含义
d p [ i ] [ m ] dp[i][m] dp[i][m]表示考虑区间为 [ i , n − 1 ] [i,n-1] [i,n−1]时,一次性可以取走的石头堆数最大为 2 m 2m 2m,在双方都做最好选择的情况下,可以获得的最大石子数量。
那么 d p [ 0 ] [ 1 ] dp[0][1] dp[0][1]即为可以拿到的石子最大数量。
-
确定递推公式
-
当剩余石头堆数小于等于 2 m 2m 2m时,直接取完
d p [ i ] [ m ] = s [ i ] ; dp[i][m]=s[i]; dp[i][m]=s[i]; -
但剩余石头数大于 2 m 2m 2m时,枚举本次可以取石子的堆数为 x ∈ [ 1 , 2 m ] x \in[1,2m] x∈[1,2m],本次最大数量取决于 d p [ i + x ] [ m a x ( m , x ) ] dp[i+x][max(m,x)] dp[i+x][max(m,x)]的最小值
d p [ i ] [ m ] = m a x ( s [ i ] − d p [ i + x ] [ m a x ( m , x ) ] ) dp[i][m]= max(s[i] - dp[i+x][max(m,x)] ) dp[i][m]=max(s[i]−dp[i+x][max(m,x)])
-
-
dp数组如何初始化
无需初始化,在递推时消化
-
确定遍历顺序
倒序遍历piles,正序遍历右端点
-
为什么?
在记忆化搜索中,我们从起点 (0,1)出发向下「递」,此时 i不断变大;「归」就是从叶子出发向着起点计算,此时 i不断变小。所以要改成递推(只有「归」),i必须从大到小计算。对于 M来说正序逆序都可以,因为我们是从 f [ i + x ] [ ] f[i+x][] f[i+x][]转移来的,无论先算哪个 M 都是正确的
-
-
举例推导dp数组
dp数组的第二维度->最大为 ⌊ n 2 ⌋ + 1 \lfloor \frac{n}{2}\rfloor+1 ⌊2n⌋+1
-
如果每次取石子时都拿满 2 m 2m 2m堆,那么为了保证每次取石子时有石子可取
【2M-2 是已经拿的累加值】
极端情况: i = 2 + 4 + 8 + M = 2 M − 2 非极端情况: i ≥ 2 M − 2 M ≤ ( i + 2 ) / 2 M ≤ ⌊ i 2 ⌋ + 1 极端情况 :i=2+4+8 + M =2M-2\\ 非极端情况:i \ge2M-2\\ M\le (i+2)/2\\ M \le \lfloor \frac{i}{2}\rfloor+1 极端情况:i=2+4+8+M=2M−2非极端情况:i≥2M−2M≤(i+2)/2M≤⌊2i⌋+1
class Solution { public int stoneGameII(int[] piles) { int s = 0, n = piles.length; // int[][] f = new int[n][n + 1]; int[][] f = new int[n][n/2 + 2]; for (int i = n - 1; i >= 0; --i) { s += piles[i]; for (int m = 1; m <= i / 2 + 1; ++m) { if (i + m * 2 >= n) f[i][m] = s; else { int mn = Integer.MAX_VALUE; for (int x = 1; x <= m * 2; ++x) mn = Math.min(mn, f[i + x][Math.max(m, x)]); f[i][m] = s - mn; } } } return f[0][1]; } } 作者:灵茶山艾府 链接:https://leetcode.cn/problems/stone-game-ii/solutions/2125753/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-jjax/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
-
复杂度
- 时间复杂度: O ( n 3 ) O(n^3) O(n3),状态个数为 O ( n 2 ) O(n^2) O(n2),单个状态计算的时间复杂度为 O ( n ) O(n) O(n)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2)
-