预测玩家(石子游戏)——递归——动态规划

题目描述

在这里插入图片描述

题目分析

例如数组为{5,200,1,3,6}
如果从贪心算法角度出发:
玩家1:第一步选6,剩余{5,200,1,3}
玩家2:第一步选5,剩余{200,1,3}
玩家1:第二步选200,剩余{1,3}
玩家2必输
由此可见在本题,局部最优得不到全局最优,所以不可以用贪心算法。

如果从全局最优来看:
两个玩家都想赢,并且水平一样。

  • 玩家1若从左边拿5,玩家2若拿200,剩{1,3,6};玩家2若拿6,剩{200,1,3} 。这两种情况下,玩家2要留给玩家1下次可供选择的分数足够小,玩家2肯定拿200,剩下{1,3,6},玩家1后面的得分要么:5+6+1=12,要么:5+1+3=9。
  • 玩家1若从右边拿6,玩家2若拿5,剩{200,1,3},玩家1下次肯定拿200,玩家2得分为:5+3=9;玩家2若拿3,剩下{5,200,1}。这两种情况下,要留给玩家1下次可供选择的分数足够小,玩家2肯定拿3,不给玩家1拿200的机会,剩下{5,200,1},玩家1后面的得分要么:6+5+1=12,要么:6+1+5=12;玩家2得分:3+200=207

综上,在这个数组下玩家1赢不了。

可以发现,玩家是拿左端还是右端,由后面能拿多少分决定,后面拿了多少分呢?还要看后面的后面拿了多少分,这不就是递归求全局最优解吗?递归下去,剩下的数组区间只会越来越短,直到能够看到后面拿了多少分,即剩下两个,直接比较;或者本来数组就只有一个数,那就只有一个选择,这不就是递归出口吗?

递归

递归三要素:
1.递归函数的作用:统机分值
2.出口
3.范围递减的等式

对手选择:左端记为L,右端记为r

(1) 假如你选左边,那么分数总和为:这个值(L)+对手选择后给你留的值(较大的值拿走,较小的值留给你,即min)中选择出较大的值:

  • 若对手选左边,后面的区间为:[ L+2 , r ],选出区间全局最优(所谓的:后面得分),也就是较大值。
  • 若对手选右边,后面的区间为:[ L+1 ,r-1 ],选出区间全局最优(所谓的:后面得分),也就是较大值。

对手选那边不知道,因为对手的决定也同样取决于他后面的分数。

  • 若对手选了左边,说明决定条件实现了(后面区间 [ L+2 , r ] 的值较大),这个左边值+后面区间 [ L+2 , r ] 的值达到了当前全局最优,即区间 [ L+2 , r ] 的值会被对手选走,留下区间[ L+1 ,r-1 ] (较小值)给你。

  • 右边同理

此时,递推方程: 取左边总分: arr[ L ] + min( [ L+2, r ] ,[ L+1 ,r-1 ] )

(2) 假如你选右边,同理。

此时,递推方程:取右边总分:arr[ r ] + min( [ L+1, r -1 ] ,[ L ,r-2 ] )

最后取左边和右边中的最大值:Max(左边总分,右边总分)。

代码

public class Helo {
    public static void main(String[] args){
        int[] arr = new int[]{5,200,2,3,5};
        int sum = 0;
        //求出总分,减去玩家1 p1,就是p2的分数
        for (int i : arr){
            sum+=i;
        }
        int p1 = maxScore(arr,0, arr.length-1);
        System.out.println(p1>sum-p1);
    }
    static int maxScore(int[] arr,int l,int r){
        if(l == r){
            return arr[l];
        }
        int sLeft = 0,rRight = 0;
        if (r-1==1){
            sLeft = arr[l];
            rRight = arr[r];
        }
        if (r-1>=2){
            sLeft = arr[l]+Math.min(maxScore(arr,l+2,r),maxScore(arr,l+1,r-1));
            rRight = arr[r]+Math.min(maxScore(arr,l+1,r-1),maxScore(arr,l,r-2));
        }
        return Math.max(sLeft,rRight);
    }
}

优化

换个思路:
maxScorel表示净得分。

将先手玩家拿到左边分数:arr[L],那么留给后手玩家比先手玩家净得分为:maxScorel(arr,L+1,r)
将sLeft = arr[L] - maxScorel(arr,L+1,r),先手减去后手比先手多的净得分,最终的差值还是先手比后手的净得分,一直递归,若最终差值大于0,说明先手玩家获胜,否则失败。
右边同理。

例如:
1.先手选4,则递归寻找后手比先手净得分;
2.此时L==r,后手接着选2(递归出口,返回),先相当于后手比先手(没数可选)多2,返回净得分2
最终:4-2=2,先手减去后手比先手多的分数后剩下2,就是先手净得分。

代码

static int maxScore1(int[] arr,int l,int r){
        if(l == r){
            return arr[l];
        }
        int sLeft = arr[l]- maxScore1(arr,l+1,r);
        int rRight = arr[r] - maxScore1(arr,l,r-1);
        return Math.max(sLeft,rRight);
    }

动态规划

不难发现:不同选择,L和r的值不同,maxScore1(arr,l+1,r)的值不同,但是L和r固定时,maxScore1(arr,l+1,r)的值也是唯一的,计算过一次就将其存储起来,由L和r确定位置,
即找二维数组dp[length][ length],内容存储差值。
初始化:当L==r时,即dp[i][i]时,有确定的值。

代码

static boolean dp(int[] arr){
        int length = arr.length;
        int[][] dp = new int[length][length];
        for (int i = 0;i<length;i++){
            dp[i][i] = arr[i];
        }
        for (int i=length-2;i>=0;i--){//因为只有两个元素时,才可以确定大小,L=i=length-2
            for (int j = i+1;j<length;j++){//取剩下的元素r=L+1
                dp[i][j] = Math.max(arr[i] - dp[i+1][j],arr[j] - dp[i][j-1]);
                /*可以发现i+1就是j,i就是j-1,可以再次优化为一维数组
                调整:dp[j] = Math.max(arr[i] - dp[j],arr[j] - dp[j-1]);
				调整:return dp[length-1]>=0;
                */
            }
        }
        return dp[0][length-1]>=0;//循环最终元素,存下最终净得分
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Strive_LiJiaLe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值