目录
1388. 3n 块披萨 - 力扣(LeetCode)
难点分析
对题目进行一个通读之后,我们发现这道题的难点主要集中在以下几个方面:
1. 该结构建立于环形结构上,也就是第一位和最后一位相邻。
2. 选取的点之间,是不能够相邻的。
思路分析
通过阅读我们很快就能发现难点,或者说卡住我们思路的点。只要对它们采取适当的策略攻克即可。但是一切都基于选取一定的算法之上。
考虑到该道题目是“最优化”的。面对最优化,我们可以选择贪心、动态规划以及分治。接下来我们从状态的角度考虑。因为每一次状态对后面一次状态都会有所影响,即反应了状态是重叠的。所以,我们很自然的想到可以选择动态规划。说一点题外话,我知道大家会想“不是说动态规划具有 无后效性吗?”。这是正确的,但是需要明白所谓的无后效性是决策和计算的角度!而动态规划的使用条件便是“状态重叠”。千万别被带歪了,学叉了。
其次状态规划的核心就是保存一定信息使得可以通过状态转移方程可以推导出下一阶段的信息。在状态的择取上,我们往往选择最小的覆盖集合。例如在这道题中,我们很快反应出来。我们每次选择一块PIZZA,相邻的PIZZA就会消失。那么相当于我们每次选取三块PIZZA,而价值为中间那块PIZZA所占份额。
首先,面对我们环形结构的第一个策略就是无视防御,先思考它为链形结构的状态。这样子我们很快就能反应过来,这是一道区间DP。也就是我们的目标是覆盖区间 [1, N], 而我们每一次的状态是[l, r]。但是别忘了,就这样我们还不能够推到出下一阶段的信息。原因在于选取个数。为了方便我们的计算我们肯定会设计 dp[l][r][k] 表示 区间[l, r]内,选取不相邻的k个PIZZA。于是我们可以的得到状态转移方程:
或
但是考虑到,状态dp[l][r][k-1] 包含 dp[l][r-2][k-1]的情况。这很容易理解,如果第 r 块PIZZA不选去,那么 dp[l][r][k-1] 就是 dp[l][r-1][k-1]。
所以我们可以把朴素想法转变为:
但秉持着最小的覆盖集合理念。我们发现我们只关注从[1, r]的区间,所以我们可以固定l = 1。至此我们可以去点一个维度,使得数组变为 dp[r][k] 表示 [1, r]内选取k个不相邻的数字最大价值。
到目前为止,我们攻克了难点2。接下来,我们攻克难点1。因为我们以上的分析是基于链形结构之上的。最简单的思考方式就是剪断 “环形结构” 使其转变为 “链形结构”。因为在编写/思考 状态转移方程的时候,我们发现状态数组之间有覆盖关系。所以 N 次剪断是没必要的。例如 dp[r][k] 表示的区间范围是 [1, r]。 如果 [1, i-1] 范围的PIZZA 不选去那么就代表的是在 [i, r] 区间内选 k 个PIZZA。 以此类推,我们只需要考虑 第一块PIZZA 可不可以选不选就可以涵盖所有情况。注意一个细节,选取第一块PIZZA,最后一块PIZZA必不能选取。
AC代码
class Solution {
public:
int calculate(const vector<int>& slices) {
int N = slices.size(), n = (N + 1) / 3;
vector<vector<int>> dp(N, vector<int>(n + 1, INT_MIN));
//简单情况
dp[0][0] = 0;//第一块不选
dp[0][1] = slices[0];//第一块选
dp[1][0] = 0;//[1,2]都不选
dp[1][1] = max(slices[0], slices[1]);//[1,2]选一个
for (int i = 2; i < N; i++) {
dp[i][0] = 0;
for (int j = 1; j <= n; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i - 2][j - 1] + slices[i]);
}
}
return dp[N - 1][n];
}
int maxSizeSlices(vector<int>& slices) {
vector<int> v1(slices.begin() + 1, slices.end());
vector<int> v2(slices.begin(), slices.end() - 1);
int ans1 = calculate(v1);//第一块不能选
int ans2 = calculate(v2);//第一块可以选
return max(ans1, ans2);
}
};