「leetcode」486. 预测赢家:【三种递归+动态规划】由浅入深,步步到位

思路

在做这道题目的时候,最直接的想法,就是计算出player1可能得到的最大分数,然后用数组总和减去player1的得分就是player2的得分,然后两者比较一下就可以了。

那么问题是如何计算player1可能得到的最大分数呢。

单独计算玩家得分

以player1选数字的过程,画图如下:

在这里插入图片描述

可以发现是一个递归的过程。

按照递归三部曲来:

  1. 确定递归函数的含义,参数以及返回值。

定义函数getScore,就是用来获取玩家1的最大得分。 参数为start 和 end 代表获取[start, end]这个区间的最大值,当然还需要传入nums。

返回值就是玩家1的最大得分。

代码如下:

int getScore(vector<int>& nums, int start, int end) {
  1. 确定终止条件

当start == end的时候,玩家A的得分就是nums[start],代码如下:

    if (start == end) {
        return nums[start];
    }
  1. 确定单层递归逻辑

玩家1的得分,等于集合左元素的数值+ 玩家2选择后集合的最小值(因为玩家2也是最聪明的)

而且剩余集合中的元素数量为2,或者大于2,的处理逻辑是不一样的!

如图:当集合中的元素数量大于2,那么玩家1先选,玩家2依然有选择的权利。
在这里插入图片描述

所以代码如下:

    if ((end - start) >= 2) {
        selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1));
        selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2));
    }

如图:当集合中的元素数量等于2,那么玩家1先选,玩家2没得选。
在这里插入图片描述

所以代码如下:

    if ((end - start) == 1) {
        selectLeft = nums[start];
        selectRight = nums[end];
    }

单层递归逻辑代码如下:

    int selectLeft, selectRight;
    if ((end - start) >= 2) {
        selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1));
        selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2));
    }
    if ((end - start) == 1) {
        selectLeft = nums[start];
        selectRight = nums[end];
    }

    return max(selectLeft, selectRight);

这些可以写出这道题目整体代码如下:

class Solution {
private:
int getScore(vector<int>& nums, int start, int end) {
    if (start == end) {
        return nums[start];
    }
    int selectLeft, selectRight;
    if ((end - start) >= 2) {
        selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1));
        selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2));
    }
    if ((end - start) == 1) {
        selectLeft = nums[start];
        selectRight = nums[end];
    }

    return max(selectLeft, selectRight);
}
public:
    bool PredictTheWinner(vector<int>& nums) {
        int sum = 0;
        for (int i : nums) {
            sum += i;
        }
        int player1 = getScore(nums, 0, nums.size() - 1);
        int player2 = sum - player1;
        return player1 >= player2;
    }
};

可以有一个优化,就是把重复计算的数值提取出来,如下:

class Solution {
private:
int getScore(vector<int>& nums, int start, int end) {
    int selectLeft, selectRight;
    int gap = end - start;
    if (gap == 0) {
        return nums[start];
    } else if (gap == 1) { // 此时直接取左右的值就可以
        selectLeft = nums[start];
        selectRight = nums[end];
    } else if (gap >= 2) { // 如果gap大于2,递归计算selectLeft和selectRight
        // 计算的过程为什么用min,因为要按照对手也是最聪明的来计算。
        int num = getScore(nums, start + 1, end - 1);
        selectLeft = nums[start] +
                min(getScore(nums, start + 2, end), num);
        selectRight = nums[end] +
                min(num, getScore(nums, start, end - 2));
    }
    return max(selectLeft, selectRight);
}
public:
    bool PredictTheWinner(vector<int>& nums) {
        int sum = 0;
        for (int i : nums) {
            sum += i;
        }
        int player1 = getScore(nums, 0, nums.size() - 1);
        int player2 = sum - player1;
        // 如果最终两个玩家的分数相等,那么玩家 1 仍为赢家,所以是大于等于。
        return player1 >= player2;
    }
};

计算两个玩家的差值

以上是单独计算出两个选手的得分,逻辑上直观,但是代码确实比较冗余。

因为就我们要求的结果其实就是两个选手的胜负,那么不用两个选手的得分,而是把问题转换为两个选手所拿元素的差值。

代码如下:

class Solution {
private:
int getScore(vector<int>& nums, int start, int end) {
    if (end == start) {
        return nums[start];
    }
    int selectLeft = nums[start] - getScore(nums, start + 1, end);
    int selectRight = nums[end] - getScore(nums, start, end - 1);
    return max(selectLeft, selectRight);
}
public:
    bool PredictTheWinner(vector<int>& nums) {
        return getScore(nums, 0, nums.size() - 1) >=0 ;
    }
};

计算的过程有一些是冗余的,在递归的过程中,可以使用一个memory数组记录一下中间结果,代码如下:

class Solution {
private:
int getScore(vector<int>& nums, int start, int end, int memory[21][21]) {
    if (end == start) {
        return nums[start];
    }
    if (memory[start][end]) return memory[start][end];
    int selectLeft = nums[start] - getScore(nums, start + 1, end, memory);
    int selectRight = nums[end] - getScore(nums, start, end - 1, memory);
    memory[start][end] = max(selectLeft, selectRight);
    return memory[start][end];
}
public:
    bool PredictTheWinner(vector<int>& nums) {
        int memory[21][21] = {0}; // 记录递归中中间结果
        return getScore(nums, 0, nums.size() - 1, memory) >= 0 ;
    }
};

此时效率已经比较高了
在这里插入图片描述

那么在看一下动态规划的思路。

动态规划

定义一个二维数组,先明确是用来干什么的,dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j)。

假如玩家1先取左端 nums[i],那么玩家2能赢对方的差值是dp[i+1][j] ,如果玩家1先取取右端 nums[j],玩家2能赢对方的差值就是dp[i][j-1],

那么 不难理解如下公式:

dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1]));

确定了状态转移公式之后,就要想想如何遍历。

一些同学确定的方程,却不知道该如何遍历这个遍历推算出方程的结果,我们来看一下。

首先要给dp[i][j]进行初始化,首先当i == j的时候,nums[i]就是dp[i][j]的值。

代码如下:

// 当i == j的时候,nums[i]就是dp[i][j]
for (int i = 0; i < nums.size(); i++) {
    dp[i][i] = nums[i];
}

接下来就要推导公式了,首先要知道最终求是dp[0][nums.size() - 1]是否大于等于0,也就是求dp[0][nums.size() - 1] 至关重要。

从下图中,可以看出在推导方程的时候一定要从右下角向上推导,而且矩阵左半部分根本不用管!

在这里插入图片描述

按照上图中的规则,不难列出推导公式的循环方式如下:

for(int i = nums.size() - 2; i >= 0; i--) {
    for (int j = i + 1; j < nums.size(); j++) {
        dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1]));
    }
}

最后整体动态规划的代码:

class Solution {
public:
    bool PredictTheWinner(vector<int>& nums) {
        // dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j)
        int dp[22][22] = {0};
        // 当i == j的时候,nums[i]就是dp[i][j]
        for (int i = 0; i < nums.size(); i++) {
            dp[i][i] = nums[i];
        }
        for(int i = nums.size() - 2; i >= 0; i--) {
            for (int j = i + 1; j < nums.size(); j++) {
                dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1]));
            }
        }
        return dp[0][nums.size() - 1] >= 0;
    }
};

我是程序员Carl,先后在腾讯和百度从事技术研发多年,利用工作之余重刷leetcode,更多精彩文章尽在公众号:代码随想录,关注后,回复「Java」「C++」「python」「简历模板」等等,有我整理多年的学习资料,可以加我微信,备注「组队刷题」,拉你进入刷题群(无任何广告,纯个人分享),每天一道经典题目分析,我选的每一道题目都不是孤立的,而是由浅入深一脉相承的,如果跟住节奏每篇连续着看,定会融会贯通。本文 https://github.com/youngyangyang04/leetcode-master 已经收录,里面还有leetcode刷题攻略、各个类型经典题目刷题顺序、思维导图,看一看一定会有所收获!

以下资料希望对你有帮助:

如果感觉题解对你有帮助,不要吝啬给一个👍吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码随想录

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

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

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

打赏作者

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

抵扣说明:

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

余额充值