预测赢家
本文将给出本题的思考过程,从递归方法一步一步推导至动态规划方法,个人觉得推完整个过程很有收获,阅读本文大概需要20分钟
原题:https://leetcode-cn.com/problems/predict-the-winner/
问题是给定一个区间,[1,5,2] ,两人交替从数组两头取数,累计取到的数越多的人获胜。给定一个数组,预测第一个玩家是否能获胜。
[1,5,2] -> false
[1, 5, 233, 7] ->true
思考:交替取数,一种直观方法是贪婪策略,如[1, 5, 233, 7],取左右两端较大的数,但是这样并不能保证取得胜利。甲先取7,乙取233,则甲不能获取胜利。
为了判断哪个玩家可以获得胜利,维持一个总分,为玩家1与玩家2之间的得分之差。当数组中的所有元素都被拿取时,总分大于等于0,则玩家1获胜。
转换问题,玩家1赢 ⇔ 总分之差大于0, 问题是如何表示总分之差?
定义dp[i][j] 表示 区间nums[i…j]之间玩家1与玩家2的分数差,[i…j] 为[0…n-1]中的子问题, 如何确定原问题与子问题之间的关系?
现在有区间[i…j], 且i<=k<j, 假设玩家1取i, 那么 玩家2的选择有[i+1,…,j],
假设 玩家1取j, 对于区间[i,…j-1]玩家2可以选取第
dp[i][j] 表示当数组剩下的部分为下标 ii 到下标 jj 时,即在下标范围 [i, j][i,j] 中,当前玩家与另一个玩家的分数之差的最大值,注意当前玩家不一定是玩家1.
当i<j时,每个玩家都会选择最优的方案,使自己的分数最大化
最难的一步,确定动态规划方程:
dp[i][j] = max(nums[i] – dp[i+1][j], nums[j] – dp[i][j-1])
nums[i] – dp[i+1][j], 代表什么意思?? 如果玩家1拿nums[i] , 玩家2会从[i+1,…j] 这个区间取得最优值, 此时相对玩家2相对玩家1来说,拥有dp[i+1][j]的分值,即玩家2与玩家1分数在区间[i+1,…,j]上的差值, 于是,玩家1比玩家2分数的差值为nums[i] – dp[i+1][j]
确定边界情况
i==j时,只能由一个玩家拿,直接返回nums[i], dp[i][j] = nums[i].
i>j时, dp[i][j] = 0
i<j时,dp[i][j] = max(nums[i] – dp[i+1][j], nums[j] – dp[i][j-1])
1.递归搜索
//递归搜索
public boolean PredictTheWinner(int[] nums) {
int res = helper(nums,0,nums.length-1);
return res>=0;
}
int helper(int[] nums,int i,int j){
if(i==j){
return nums[i];
}
return (int)Math.max(nums[i]-helper(nums,i+1,j),nums[j]-helper(nums,i,j-1));
}
递归搜索的代码直观,容易理解,但是会产生大量的冗余计算,根据递归树
状态5,7, 233会被计算多次。
2.带记忆的递归搜索
dp[i][j] 存储树中节点的值,当遇到重复值时,直接返回
//带记忆的递归搜索
int[][] dp;
public boolean PredictTheWinner1(int[] nums){
int n = nums.length;
dp = new int[n][n];
for(int i=0;i<n;i++){
Arrays.fill(dp[i],-1);
}
int res = helper1(nums,0,n-1);
return res>=0;
}
int helper1(int[] nums,int i,int j){
if(i==j){
return nums[i];
}
if(dp[i][j]!=-1){
return dp[i][j];
}
dp[i][j] = (int)Math.max(nums[i]-helper1(nums,i+1,j),nums[j]-helper1(nums,i,j-1));
return dp[i][j];
}
3.动态规划解法
状态方程已经推导,关键在于如何填dp table,即dp[i][j]有哪些元素转移而来? ⇔ 如何枚举i, j ?
值得注意的是,递归搜索是从大区间,[0,…,n-1] ,搜索到[i,…,j], 当i==j时,返回nums[i],是一组自顶向下的搜索, 而dp方法,是自底向上的,即由[i,…j] 求解[0,…n]的解, 先求小区间,根据小区间的值求解大区间。
如何枚举i,j? 可以使得先获得小区间再求大区间?
以下总结几种枚举方法,
(1)正向枚举
For i = 0 to n
For j=0 to n
do sth
对应dp table计算方向为
(2) 逆向枚举
For i=n-1 to 0
For j = i to n
do sth
箭头为table中元素的填充方向
本题中,i<j时,dp[i][j] = max(nums[i] – dp[i+1][j], nums[j] – dp[i][j-1])
dp[i][j] 与 dp[i+1][j], dp[i][j-1]两个元素有关,即由dp[i+1][j]和dp[i][j-1]推出dp[i][j], 且i<=k<j
等式右边推左边, i是由大逐渐减小,j是逐渐增大,且i<j, 只要求dp table右上部分, 可以画出地推方向
可以写出代码
public boolean PredictTheWinner3(int[] nums) {
int n = nums.length;
int[][] dp = new int[n][n];
for(int i=0;i<n;i++){
dp[i][i] = nums[i];
}
for(int i=n-2; i>=0; --i){
for(int j=i+1;j<n;j++){ //不包括对角线,取右上半部分, 左边和下面的先计算
dp[i][j] = Math.max(nums[i]-dp[i+1][j],nums[j]-dp[i][j-1]);
}
}
return dp[0][n-1]>=0;
}
参考题解:
https://leetcode-cn.com/problems/predict-the-winner/solution/shou-hua-tu-jie-san-chong-xie-fa-di-gui-ji-yi-hua-/
https://leetcode-cn.com/problems/predict-the-winner/solution/yu-ce-ying-jia-by-leetcode-solution/