力扣486-预测赢家

预测赢家

本文将给出本题的思考过程,从递归方法一步一步推导至动态规划方法,个人觉得推完整个过程很有收获,阅读本文大概需要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/

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值