leetcode-master/problems/0486.预测赢家.md

280 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 题目地址
## 思路
在做这道题目的时候最直接的想法就是计算出player1可能得到的最大分数然后用数组总和减去player1的得分就是player2的得分然后两者比较一下就可以了。
那么问题是如何计算player1可能得到的最大分数呢。
## 单独计算玩家得分
以player1选数字的过程画图如下
<img src='../pics/486.预测赢家2.png' width=600> </img></div>
可以发现是一个递归的过程。
按照递归三部曲来:
1. 确定递归函数的含义,参数以及返回值。
定义函数getScore就是用来获取玩家1的最大得分。 参数为start 和 end 代表获取[start, end]这个区间的最大值当然还需要传入nums。
返回值就是玩家1的最大得分。
代码如下:
```
int getScore(vector<int>& nums, int start, int end) {
```
2. 确定终止条件
当start == end的时候玩家A的得分就是nums[start],代码如下:
```
if (start == end) {
return nums[start];
}
```
3. 确定单层递归逻辑
玩家1的得分等于集合左元素的数值+ 玩家2选择后集合的最小值因为玩家2也是最聪明的
而且剩余集合中的元素数量为2或者大于2的处理逻辑是不一样的
如图当集合中的元素数量大于2那么玩家1先选玩家2依然有选择的权利。
<img src='../pics/486.预测赢家3.png' width=600> </img></div>
所以代码如下:
```
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没得选。
<img src='../pics/486.预测赢家4.png' width=600> </img></div>
所以代码如下:
```
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 ;
}
};
```
此时效率已经比较高了
<img src='../pics/486.预测赢家.png' width=600> </img></div>
那么在看一下动态规划的思路。
## 动态规划
定义一个二维数组先明确是用来干什么的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] 至关重要。
从下图中,可以看出在推导方程的时候一定要从右下角向上推导,而且矩阵左半部分根本不用管!
<img src='../pics/486.预测赢家1.png' width=600> </img></div>
按照上图中的规则,不难列出推导公式的循环方式如下:
```
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;
}
};
```