题解:洛谷 P3004 宝箱
【题目来源】
洛谷:P3004 [USACO10DEC] Treasure Chest S - 洛谷
【题目描述】
Bessie 和 Bonnie 发现了一个装满了非凡的金币的宝箱!作为奶牛,尽管,他们无法走进一家商店购买物品,因此他们决定开心的玩一玩这些金币。\(N(1\le N\le 5000)\) 个金币放置在一条直线上,每个金币的价值为\(C_i(1\le C_i\le 5000)\)。Bessie 和Bonnie轮流操作,每一轮中,她从直线的左端或者右端取走一枚金币。金币全部取完时游戏结束。
Bessie 和 Bonnie 都想要让自己尽可能得到更多的财富。Bessie 先开始游戏。请帮忙她算出她能获得的最大价值。假设两个奶牛都以最优的方法进行游戏。
考虑一局游戏,四个金币排成一行,价值如下所示:30 25 10 35
考虑如下的游戏过程:

这是Bessie 能做到的最优游戏方式。
【输入】
行\(1\):一个数\(N\)。行\(2..N+1\):行\(i+1\)为一个整数\(C_i\)
【输出】
行\(1\):一个整数,即Bessie 能赢得的最大价值。
【输入样例】
4
30
25
10
35
【输出样例】
60
【核心思想】
-
问题分析:给定 \(N\) 个金币排成一列,价值为 \(C_i\)。Bessie 和 Bonnie 轮流从两端取金币,双方都采取最优策略。求 Bessie(先手)能获得的最大总价值。这是一个区间DP/博弈论问题,关键在于用 \(f[i][j]\) 表示在区间 \([i, j]\) 内先手能获得的最大价值,利用"总分固定 - 对手最优收益"的博弈思想进行状态转移。
-
算法选择:
- 区间DP:\(f[i][j]\) 表示在金币区间 \([i, j]\) 内,当前先手玩家能获得的最大价值
- 博弈转移:区间 \([i, j]\) 的总价值为 \(s[j] - s[i-1]\),先手取走左端或右端后,对手在剩余区间成为先手。因此 \(f[i][j] = \text{总价值} - \min(f[i+1][j],\ f[i][j-1])\)
- 滚动数组优化:由于 \(f[i][j]\) 只依赖 \(f[i+1][j]\) 和 \(f[i][j-1]\),可用一维数组滚动优化空间
-
关键步骤:
- 读取输入:\(N\) 和 \(N\) 个金币价值 \(C_i\)
- 前缀和:\(s[i] = s[i-1] + C_i\)
- 初始化:\(f[i] = C_i\)(区间长度为 \(1\))
- 区间DP(\(len\) 从 \(2\) 到 \(N\)):
- \(i\) 从 \(1\) 到 \(N - len + 1\),\(j = i + len - 1\)
- \(f[i] = (s[j] - s[i-1]) - \min(f[i+1],\ f[i])\)
- 输出 \(f[1]\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2)\),枚举所有区间
- 空间复杂度:\(O(N)\),滚动数组优化后
-
区间DP博弈的核心思想:
- 总分守恒:区间 \([i, j]\) 内的金币总价值固定,先手收益 = 总价值 - 后手在剩余区间的最优收益
- 最优策略对偶性:双方都采取最优策略时,当前玩家的最优选择是使对手在剩余局面中的收益最小化
- 滚动数组降维:\(f[i][j]\) 的转移只涉及同一行和下一行,逆序更新可将二维降为一维
- 适用于"双端取物博弈 + 最优策略"类经典问题,核心是利用总分守恒和对手最小化进行转移
【解题思路】


【算法标签】
普及+ #区间DP
【代码详解】
//该代码会有一个MLE
// 引入所有标准库头文件,方便使用各种标准库功能
#include <bits/stdc++.h>
// 使用标准命名空间,避免每次使用标准库函数时都需要加 std:: 前缀
using namespace std;
// 定义常量
const int N = 5005; // 定义金币数量的最大值为5005
// 定义全局变量
int f[N][N]; // f[i][j]: 表示在区间(i, j)内先拿金币时,能取得的最大价值
int n, c[N]; // n: 金币的总数量,c[N]: 存储每个金币的价值,c[i]表示第i个金币的价值
int s[N]; // s[N]: 前缀和数组,s[i]表示前i个金币的总价值
// 主函数,程序的入口点
int main()
{
// 读取金币的总数量 n
cin >> n; // N金币数量
// 循环读取每个金币的价值,并计算前缀和数组 s[j]
for (int j = 1; j <= n; j++)
{
// 读取第j个金币的价值
cin >> c[j]; // 读入金币价值
// 计算前缀和:s[j] = s[j-1] + 当前金币的价值
s[j] = s[j - 1] + c[j]; // s[i]前缀和,前i金币价值和
}
// =============================
// 边界条件初始化
// =============================
// 对于区间长度为1的情况,即只有一个金币,先拿的最大价值就是该金币的价值
for (int i = 1; i <= n; i++)
{
f[i][i] = c[i]; // 边界:区间长度1,只有一个金币,先拿最大值是金币值
}
// =============================
// 动态规划部分:计算在区间(i, j)内先拿金币时能取得的最大价值
// =============================
// 外层循环:遍历所有可能的区间长度 len,从2到n
for (int len = 2; len <= n; len++)
{
// 中层循环:遍历所有可能的起始位置 i,确保区间(i, j)在有效范围内
for (int i = 1; i <= n - len + 1; i++)
{
// 计算区间的结束位置 j
int j = i + len - 1;
// 更新在区间(i, j)内先拿金币时能取得的最大价值
// f[i][j] = s[j] - s[i-1] - min(f[i+1][j], f[i][j-1])
// 解释:
// s[j] - s[i-1] 表示区间(i, j)内所有金币的总价值
// min(f[i+1][j], f[i][j-1]) 表示对手在剩下的区间内能取得的最大价值(取最小值,因为对手会采取最优策略)
// 因此,f[i][j] 表示当前玩家在区间(i, j)内先拿金币时,能取得的最大价值
f[i][j] = s[j] - s[i - 1] - min(f[i + 1][j], f[i][j - 1]);
}
}
// =============================
// 输出结果:在整个区间(1, n)内先拿金币时能取得的最大价值
// =============================
// 输出 f[1][n],即在整个金币序列中先拿金币时能取得的最大价值
cout << f[1][n] << endl;
// 程序正常结束,返回 0
return 0;
}
// 引入所有标准库头文件,方便使用各种标准库功能
#include <bits/stdc++.h>
// 使用标准命名空间,避免每次使用标准库函数时都需要加 std:: 前缀
using namespace std;
// 定义常量
const int N = 5005; // 定义金币数量的最大值为5005
// 定义全局变量
int f[N]; // f[i]: 表示在区间(i, j)内先拿金币时,能取得的最大价值(当前实现可能有误,应为区间[i, j])
int n, c[N]; // n: 金币的总数量,c[N]: 存储每个金币的价值,c[i]表示第i个金币的价值
int s[N]; // s[N]: 前缀和数组,s[i]表示前i个金币的总价值
// 主函数,程序的入口点
int main()
{
// 读取金币的总数量 n
cin >> n; // N金币数量
// 循环读取每个金币的价值,并计算前缀和数组 s[j]
for (int j = 1; j <= n; j++)
{
// 读取第j个金币的价值
cin >> c[j]; // 读入金币价值
// 计算前缀和:s[j] = s[j-1] + 当前金币的价值
s[j] = s[j - 1] + c[j]; // s[i]前缀和,前i金币价值和
}
// =============================
// 边界条件初始化
// =============================
// 对于区间长度为1的情况,即只有一个金币,先拿的最大价值就是该金币的价值
for (int i = 1; i <= n; i++)
{
f[i] = c[i]; // 边界:区间长度1,只有一个金币,先拿最大值是金币值
}
// =============================
// 动态规划部分:计算在区间(i, j)内先拿金币时能取得的最大价值
// =============================
// 外层循环:遍历所有可能的区间长度 len,从2到n
for (int len = 2; len <= n; len++)
{
// 中层循环:遍历所有可能的起始位置 i,确保区间(i, j)在有效范围内
for (int i = 1; i <= n - len + 1; i++)
{
// 计算区间的结束位置 j
int j = i + len - 1;
// 更新在区间(i, j)内先拿金币时能取得的最大价值
// f[i] = s[j] - s[i-1] - min(f[i+1], f[i])
// 解释:
// s[j] - s[i-1] 表示区间(i, j)内所有金币的总价值
// min(f[i+1], f[i]) 表示对手在剩下的区间内能取得的最大价值(取最小值,因为对手会采取最优策略)
// 因此,f[i] 表示当前玩家在区间(i, j)内先拿金币时,能取得的最大价值
f[i] = s[j] - s[i - 1] - min(f[i + 1], f[i]);
}
}
// =============================
// 输出结果:在整个区间(1, n)内先拿金币时能取得的最大价值
// =============================
// 输出 f[1],即在整个金币序列中先拿金币时能取得的最大价值
cout << f[1] << endl;
// 程序正常结束,返回 0
return 0;
}
【运行结果】
4
30
25
10
35
60
浙公网安备 33010602011771号