动态规划石子排序java_LeetCode学习笔记——石子游戏(动态规划)

个人博客:The Blog Of WaiterXiaoYY 欢迎来互相交流学习。

对动态规划的理解程度:★★◐☆☆

博弈类问题感觉也是一种脑脑急转弯的题,

博弈类题目其实都有非常巧妙的解法,

但我们学习还是以 稳 为准,不追求那些花里胡哨的做法,

今天,我们从石子游戏入手,去探究一下博弈类问题的奥妙。

石子游戏

亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 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 。

提示:

2 <= piles.length <= 500

piles.length 是偶数。

1 <= piles[i] <= 500

sum(piles) 是奇数。

解题思路

题目很长,你可以直接跳过上面,从这里开始读起,

你和你的朋友在一起玩游戏,有若干堆石子在你们面前,每堆石子的数量用 piles[i] 表示,每次只能从石子堆的两侧拿,你们都很精明,每次只能拿两侧的石子,然后看谁多谁赢,题目要求如果亚历克斯赢就返回true。

现在,我们为了能够获得博弈问题的解题套路,我们把这道题目进行变换一下,使的题目能够更具“一般性”,不用管题目中的亚历克斯和李,变换后其实就是一个先手和后手的问题,肯定有一个人先开始拿,有一个人后开始拿,这就是博弈问题的特点,变换后,我们也不管亚里斯赢不赢了,我们求先手和后手的石子数量的差,如果先手开始的到最后的石子数量大于后手的石子数量,就是先手赢,也是这道题的意思。

好,现在我们明确了题目,我们这时候要开始来分析一下解题思路,这种题我们可以用动态规划来做,动态规划的解题方法是是什么?

找出 状态 和 选择。动态规划的状态其实是很难判断的,判断出状态,然后列出状态转移方程,这也是动态规划中最难的部分。而选择就很简单了,因为不管先手还是后手,只能从上一个人选完剩下的石子堆的两侧进行选择,选择左侧还是右侧的石子就是选择。

选择我们确定了,状态呢?状态一下子是看不出来的,我们要深入到题目里面去,我们知道动态规划还有一个特点,就是求 最优解子问题,我们把所有求的最后的结果分解到子问题中去求解,

比如给一堆石子 :piles = [5, 3, 7, 1],索引从 1 到 4,

我们要求这堆石子的两人的石子数差,就是看子问题的石子数差,那问题来了,子问题是什么呢?

不急,我们再来思考一下,

假如,先手取了第一堆石子,那石子堆就只剩下了 piles=[3, 7, 1],这时就到了我们所谓的后手拿,后手也只能从两侧拿,如果拿了第二堆,那石子堆只剩下 piles=[7, 1],这时候又到我们的先手了,选择第三堆,那剩下就是后手拿了,最后先手的石子数量时 5 + 7,后手的数量 3 + 1,这次是先手胜,可能有同学注意到了,先手之所以能 5 + 7,是因为一开始选择了第一堆,那如果一开始选择的是最后一堆呢,结果可能就会有所变化,

所以通过这样具体分析后,子问题就出来了,当选手做出选择后,会出现两种情况,piles = [3, 7, 1], 或者是 piles = [5, 7, 3], 所以 每次选择都很重要,你一次糊涂选择会导致全盘皆输,但题目有要求,每个人都很精明,所以不可能会有糊涂选择,每次选择都是最佳的选择,而我们就是要找出这个最佳情况,那这个最佳是依靠什么判断出来呢?就是子问题,要看这次选择的是不是最佳,就看这次选择后的子问题的选择的情况,一直这样伸展下去,直到石子都被分完。

所以,这时候我们的状态就出来了,一个是石子堆的范围,就是从第几堆到第几堆的最佳情况,在 piles = [5, 3, 7, 1],要求第一堆到第四堆的最佳情况,就是求 选择完后的子问题,比如 [3, 7, 1],也就是第二堆到第四堆的最佳状况,第二个是当前选择的人,这两个状态就是我们用动态规划所需要展示出来的。

我们需要一个二维数组 dp[i][j], 用来表示第 i 堆石子到第 j 堆石子的最佳情况,还需要一个表示当前选择的人,我们用 first 和second 表示,像这样 dp[i][j].first,dp[i][j].second,表示第 i 堆石子到第 j 堆石子谁先开始选择,注意这里说的是 先开始,上面我们说的先手开始选后,那剩下的石子堆对于后手来说也是先开始,要理解这里。

接着讲状态和选择,转换成状态转移方程,

# 对于先手来说

dp[i][j].first = max(选择左侧的石子堆, 选择右侧的石子堆);

dp[i][j].first = max(piles[i] + dp[i + 1][j].second, piles[j] + dp[i][j - 1].second);

# 如果先手先选择了左侧的石子堆,那么剩下的石子堆范围就变成了[i+1, j],由后手来先开始选择

# 如果先手先选择了右侧的石子堆,那么剩下的石子堆范围就变成了[i, j + 1],由后手来先开始选择

# 最后取两者的最大值,也就是最佳情况

# 对于后手来说

if 先手选择了左侧:

dp[i][j].second = dp[i+1][j].first;

if 先手选择了右侧:

dp[i][j].second = dp[i][j + 1].first;

# 我是后手,我需要等先手先做出选择

# 如果先手选择了左边,那剩下的石子堆范围就变成了[i+1, j],这时候我变成了先手

# 如果先手选择了右边,那剩下的石子堆范围就变成了[i, j + 1],这时候我变成了先手

状态转移方程写出来了,接下来就是要确定我们的初始状态,

初始状态是i 和 j 相等,也就是当面前只有piles[i]这一堆石头的时候,

dp[i][j].first = piles[i];

dp[i][j].second = 0;

# 当只有一堆石头的时候,先手拿了,后手就没有了

初始状态,

1130710cea6094aaa02c2479b2ada28c.png

在石子堆0~1中,(5,3)的子问题是(5,0)和(3,0),当先手选择了5,那后手只剩下了3,

6e1b83c773d67b8a3e1128e02d214e98.png

对于石子堆0~2中,(10,5)的子问题是(5,3)和(7,3),根据状态转移方程,当先手选择了7,后手选择了5,只剩下3,所以先手是10,后手是5,

对于石子堆0~3,(12,4)的子问题是(10,5)和(4,7)分别对应选择右边和左边,,在选择左右两边中判断,发现当先先选择左边的5,剩下中后手先选能达到的最大是7,所以加起来是12,后手选的是4,

3f6aa40128385a00848616e189fa39bb.png

相信经过上面一大串啰嗦的分析,应该能对石子游戏有一定的认识了,在看代码之前,先解释一下first和second如何定义,我们可以弄一个类将其封装起来,在后面只需要创建它的实例就可以了,

class Pair {

//代表先手

int first;

//代表后手

int second;

Pair(int fisrt, int second) {

this.first = first;

this.second = second;

}

}

接下来我们看看代码吧。

代码

class Solution {

public boolean stoneGame(int[] piles) {

//石子堆的长度

int len = piles.length;

//创建pair的实例,同时初始化二维数组

Pair[][] dp = new Pair[len][len];

for(int i = 0; i < len; i++) {

for(int j = i; j < len; j++) {

dp[i][j] = new Pair(0, 0);

}

}

//将初始值填入

for(int i = 0; i < len; i++) {

dp[i][i].first = piles[i];

dp[i][i].second = 0;

}

//l代表的是石子堆的数量,从两堆开始

for(int l = 2; l <= len; l++) {

//i代表的是石子堆范围的左侧索引

for(int i = 0; i <= len - l; i++) {

//j代表的是石子堆范围的右侧索引

int j = i + l - 1;

//left表示选择左侧石子能达到的最佳状态

int left = piles[i] + dp[i + 1][j].second;

//右侧表示选择右侧石子能达到的最佳状态

int right = piles[j] + dp[i][j - 1].first;

//如果选择左侧大于选择右侧,则将在该范围的情况下先手的值更新为left,且后手的数量时等于在[i+1,j]范围中作为先手所能获得的最佳数量,否则反之

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][n - 1].first - dp[0][n - 1].second;

//本题的结果,比较是否先手会大,否则返回false

//return dp[0][len - 1].first > dp[0][len - 1].second;

}

}

整理于2020.4.15,有参考于labuladong大佬的讲解。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值