题目描述
“吃货”和“馋嘴”两人到披萨店点了一份铁盘(圆形)披萨,并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。
但是粗心服务员将披萨切成了每块大小都完全不同奇数块,且肉眼能分辨出大小。
由于两人都想吃到最多的披萨,他们商量了一个他们认为公平的分法:从“吃货”开始,轮流取披萨。
除了第-块披萨可以任意选取以外,其他都必须从缺口开始选。 他俩选披萨的思路不同。
“馋嘴”每次都会选最大块的拨萨,而且“吃货”知道“馋嘴”的想法。
已知披萨小块的数量以及每块的大小,求“吃货”能分得的最大的披萨大小的总和。
输入描述
第1行为一个正整数奇数 N ,表示披萨小块数量。其中 3 ≤ N< 500
接下来的第 2 行到第 N+1 (共 N 行),每行为一个正整数,表示第i块披萨的大小, 1≤i≤N 。
披萨小块从某一块开始,按照一个方向次序顺序编号为 1 ~ N ,每块披萨的大小范围为[1,2147483647]。
输出描述
”吃货“能分得到的最大的披萨大小的总和。
输入:
5
8
2
10
5
7
输出:
19
说明:
此例子中,有 5 块披萨。每块大小依次为 8 、2 、10 、5 、7。
按照如下顺序拿披萨,可以使”吃货拿到最多披萨:
“吃货”拿大小为 10 的披萨
“馋嘴”拿大小为5的披萨
“吃货”拿大小为7 的披萨
“馋嘴”拿大小为 8 的披萨
”吃货“拿大小为2 的披萨
至此,披萨瓜分完毕,”吃货“拿到的披萨总大小为 10+7+2=19
可能存在多种拿法,以上只是其中一种。
思考
本题是环形披萨分块的博弈型动态规划问题,核心是结合“环形拆线性”+“博弈规则下的DP/记忆化搜索”,最大化吃货的总收益,以下是核心逻辑拆解:
一、问题核心特征
- 环形结构:披萨是圆形,首次选任意块后,剩余披萨形成线性缺口(只能从缺口两端选);
- 博弈规则:
- 吃货先选,首次可任选一块,后续只能从缺口两端选;
- 馋嘴每次选缺口两端中更大的块(吃货已知该规则,需以此为前提决策);
- 总块数为奇数,最终吃货比馋嘴多拿1块。
二、通用解题框架(两种等价思路)
思路1:记忆化搜索(递归+缓存)
核心步骤
- 环形遍历首次选择:遍历吃货首次选的每一块(i),计算该选择下的最大收益,最终取所有首次选择的最大值;
- 递归处理剩余披萨:
- 首次选i后,剩余披萨的缺口两端为
i-1和i+1(环形取模); - 每轮先让馋嘴按“选两端更大块”的规则拿走一块,缩小缺口范围;
- 吃货再从新的缺口两端选,递归计算后续最大收益(记忆化缓存
cache[l][r]避免重复计算);
- 首次选i后,剩余披萨的缺口两端为
- 终止条件:剩余可拿块数≤1时,收益为0。
关键逻辑
cache[l][r]:记录缺口两端为l和r时,吃货能拿到的最大收益;- 馋嘴的决策:比较
list[l]和list[r],拿走更大的那块,更新缺口边界; - 吃货的决策:在馋嘴拿完后,从新边界两端选,取“拿左+后续收益”和“拿右+后续收益”的最大值。
思路2:动态规划(DP,环形拆线性)
核心步骤
- 环形拆线性:遍历吃货首次选择的每一块(i),将剩余披萨(环形)拆分为线性序列
[i+1, ..., n-1, 0, ..., i-1]; - DP状态定义:
dp[l][r]表示线性子序列sub[l..r]中,当前决策方能拿到的最大收益; - DP状态转移:
- 子序列长度为1:直接拿走该块,
dp[l][r] = sub[l]; - 偶数步(吃货决策):选两端中“当前块+剩余子序列收益”更大的,
dp[l][r] = max(sub[l]+dp[l+1][r], sub[r]+dp[l][r-1]); - 奇数步(馋嘴决策):馋嘴选两端更大的块,吃货只能拿剩余部分,
dp[l][r] = dp[l+1][r](若sub[l]更大)或dp[l][r-1](若sub[r]更大);
- 子序列长度为1:直接拿走该块,
- 总收益计算:首次选择的块大小 + 剩余线性序列的DP最大值,遍历所有首次选择取最大。
三、核心关键点
- 环形转线性:披萨的环形结构仅影响“首次选择后的剩余序列拆分”,后续均按线性缺口处理;
- 博弈规则的落地:
- 馋嘴的决策是“贪心选大”,需在每轮优先执行该规则,缩小可选范围;
- 吃货的决策是“基于馋嘴贪心的最优选择”,需通过DP/递归枚举所有可能并取最大值;
- 时间复杂度:
- 记忆化搜索:O(N²)(N<500,N²=25万,无性能压力);
- DP:O(N³)(遍历首次选择O(N) + 每个线性序列DP O(N²)),仍满足题目限制。
四、核心结论
本题的核心是**“环形拆线性”+“博弈型DP/记忆化搜索”**:
- 先拆解环形结构(遍历首次选择),将问题转化为线性缺口的博弈;
- 再利用DP/记忆化搜索,结合“馋嘴贪心选大”的规则,枚举吃货的所有可选决策,最大化总收益。
代码
// 记忆化搜索
function solution() {
const N = Number(readline());
const list = [];
for (let i = 0; i < N; i++) {
list[i] = Number(readline());
}
const cache = Array.from({length: N}, () => Array(N).fill(-1));
const getMaxSum = (l, r, t) => {
if (t <= 1) return 0;
l = (l + N) % N;
r = r % N;
if (list[l] > list[r]) {
l = (l - 1 + N) % N;
} else {
r = (r + 1) % N;
}
if (cache[l][r] !== -1) {
return cache[l][r];
}
const s1 = list[l] + getMaxSum(l - 1, r, t - 2);
const s2 = list[r] + getMaxSum(l, r + 1, t - 2);
cache[l][r] = Math.max(s1, s2);
return cache[l][r];
};
let maxSum = 0;
for (let i = 0; i < N; i++) {
const current = list[i] + getMaxSum(i-1, i+1, N-1);
if (current > maxSum) {
maxSum = current;
}
}
console.log(maxSum);
}
// dp
function solution2() {
const N = Number(readline());
const list = [];
for (let i = 0; i < N; i++) {
list[i] = Number(readline());
}
let maxSum = 0;
// 遍历第一次选择的每一种可能(环形拆分为线性)
for (let i = 0; i < N; i++) {
// 第一次选择i后,剩余序列为 i+1 ~ n-1 拼接 0 ~ i-1(线性)
const sub = list.slice(i+1).concat(list.slice(0, i));
const m = sub.length; // m = n-1(奇数-1=偶数)
// 初始化dp table,dp[l][r]表示子序列sub[l..r]的最大收益
const dp = Array.from({ length: m }, () => Array(m).fill(0));
// 填充dp(从长度为1的子序列开始)
for (let len = 1; len <= m; len++) {
for (let l = 0; l + len - 1 < m; l++) {
const r = l + len - 1;
if (l === r) {
// 只剩一块时,当前选择者直接拿走
dp[l][r] = sub[l];
} else {
// 判断当前轮次:总长度m为偶数,len为当前子序列长度
// 初始总步数为m步(偶数),每选一次减少1步,步数奇偶性决定轮到谁
const steps = m - (r - l); // 剩余步数 = 总步数 - 已选步数
if (steps % 2 === 0) {
// 偶数步:轮到吃货(自己)选择,选两端中收益最大的
const takeLeft = sub[l] + dp[l + 1][r];
const takeRight = sub[r] + dp[l][r - 1];
dp[l][r] = Math.max(takeLeft, takeRight);
} else {
// 奇数步:轮到馋嘴选择,他会选两端中更大的,剩余给吃货
if (sub[l] >= sub[r]) {
// 馋嘴选左,吃货只能从l+1..r中选
dp[l][r] = dp[l + 1][r];
} else {
// 馋嘴选右,吃货只能从l..r-1中选
dp[l][r] = dp[l][r - 1];
}
}
}
}
}
// 第一次选择i的总收益 = arr[i] + 剩余序列的最大收益
const total = list[i] + dp[0][m - 1];
if (total > maxSum) {
maxSum = total;
}
}
console.log(maxSum);
}
cases = [
`5
8
2
10
5
7`,
`3
5
10
3`,
`7
1
3
5
7
9
11
13`
];
let caseIndex = 0;
let lineIndex = 0;
const readline = (function () {
let lines = [];
return function () {
if (lineIndex === 0) {
lines = cases[caseIndex]
.trim()
.split("\n")
.map((line) => line.trim());
}
return lines[lineIndex++];
};
})();
cases.forEach((_, i) => {
caseIndex = i;
lineIndex = 0;
solution2();
console.log('-------');
});
322

被折叠的 条评论
为什么被折叠?



