题目描述
石子游戏中,爱丽丝和鲍勃轮流进行自己的回合,爱丽丝先开始 。
有n
块石子排成一排。每个玩家的回合中,可以从行中 移除 最左边的石头或最右边的石头,并获得与该行中剩余石头值之 和 相等的得分。当没有石头可移除时,得分较高者获胜。
鲍勃发现他总是输掉游戏(可怜的鲍勃,他总是输),所以他决定尽力 减小得分的差值 。爱丽丝的目标是最大限度地 扩大得分的差值 。
给你一个整数数组stones
,其中stones[i]
表示 从左边开始 的第i
个石头的值,如果爱丽丝和鲍勃都 发挥出最佳水平 ,请返回他们 得分的差值 。
思路分析
- 首先肯定是看懂题目,当对题目信息完全了解之后,才能更好的更快速的解决它,否则很容易出错。本题目中,有一句话比较难理解——鲍勃发现他总是输掉游戏。
解读:对于这种关乎答案的问题最好的理解方法就是举例子。
stones = [5,3,1,4,2]
假设爱丽丝先拿5: 得分score = 3 + 1 + 4 + 2
后一步鲍勃拿3: 其得分score = 1 + 4 + 2。 拿2:则 score = 3 + 1 + 4
可以看到一人一次的选择下,爱丽丝先选的这次得分一定比鲍勃后选的这次得分高。因为爱丽丝的得分包含了鲍勃的得分以及鲍勃移除的分数。
即谁先选择谁赢。
- 分析什么是发挥出最佳水平
- 有唯一最优解(这个时候就要想动态规划了,不过也可以先往后想)
- 对爱丽丝自己来说,自己的最佳水平,应该是当自己选择时,让自己得分-鲍勃得分 最大。
- 每个人每次有两种移除选择,我们要比较这两次的结果,选择一个对自己最佳的选择。 同时这个最佳选择,一方面于当前选择的得分有关,一方面也与后面的选择有关,所以自然可以想到递归搜索。
- 想到搜索,就要想需不需要记忆化搜索,举个例子就可以明白需要记忆化。
- 那么这个时候就有两种方法,记忆化回溯,或 动态规划,这两种方法等价,只是实现方式不一样。前者,自顶向下,后者,自底向上。
- 动态规划或记忆化搜索,以动态规划举例(两者思考过程其实一样)
- dp矩阵的定义:
- 一维 or 二维: 取首部或尾部,则矩阵前后都会变化,所以应该二维矩阵。
- 定义:定义关注的是当前的含义——
dp[i][j]
指的是自己当前选择之后,自己的分数-别人的分数最大。
- 递推关系:前面说了当前的选择与两部分有关,当前选择的得分,后续的最大得分差值。因为是轮流选择,所以后续的最大得分差值是相对于另一人的,所以两者是减法的关系。
//获取当前选择的得分。 int n = stones.size(); vector<int> sum(n+1); /** 这一块有一个数组定义的问题,这一块是sum[i] 表示 0_i-1的和 为什么不能表示为0_i的和, 通常情况下应该都可以,只需要在处理前定义好,后续按照这个处理即可。 本题中需要往前收缩,后续会处理sum[j-1] 所以会出现数组越界的问题。 不过后面再改也行。这个不是很重要。 **/ for (int i = 0; i < n; i++) { sum[i + 1] = sum[i] + stones[i]; } // dp[i][j] 选择i时,当前得分为 sum[j+1] - sum[i+1] // 选择j时,当前得分为 sum[j] - sum[i] //递推公式 dp[i][j] = max(sum[j + 1] - sum[i + 1] - dp[i + 1][j], sum[j] - sum[i] - dp[i][j - 1]);
- 遍历顺序:看递推公式,
dp[i][j]
与dp[i + 1][j]
和dp[i][j - 1]
有关,所以在处理i
之前,i+1
应该已经处理完了,所以i
为递减,同理,j
为递加。
- dp矩阵的定义:
完整代码
动态规划
class Solution {
public:
int stoneGameVII(vector<int>& stones) {
int n = stones.size();
vector<int> sum(n + 1);
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
sum[i + 1] = sum[i] + stones[i];
}
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
dp[i][j] = max(sum[j + 1] - sum[i + 1] - dp[i + 1][j],
sum[j] - sum[i] - dp[i][j - 1]);
}
}
return dp[0][n - 1];
}
};
记忆化搜索
//来自于力扣官方
class Solution {
public:
int stoneGameVII(vector<int>& stones) {
int n = stones.size();
vector<int> sum(n + 1);
vector<vector<int>> memo(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
sum[i + 1] = sum[i] + stones[i];
}
function<int(int, int)> dfs = [&](int i, int j) -> int {
if (i >= j) {
return 0;
}
if (memo[i][j] != 0) {
return memo[i][j];
}
int res = max(sum[j + 1] - sum[i + 1] - dfs(i + 1, j), sum[j] - sum[i] - dfs(i, j - 1));
memo[i][j] = res;
return res;
};
return dfs(0, n - 1);
}
};
总结与思考
- 在进行问题思考的时候,应该先看清并读懂题目,再进行下一步工作。
- 善于总结,比如在遇到最优化问题的时候就应该思考能不能使用动态规划。
- 本文dp矩阵定义是一个难点,通过读懂题目才能想到该定义。