题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 个金币排成一列,价值为 \(C_i\)。Bessie 和 Bonnie 轮流从两端取金币,双方都采取最优策略。求 Bessie(先手)能获得的最大总价值。这是一个区间DP/博弈论问题,关键在于用 \(f[i][j]\) 表示在区间 \([i, j]\) 内先手能获得的最大价值,利用"总分固定 - 对手最优收益"的博弈思想进行状态转移。

  2. 算法选择

    • 区间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]\),可用一维数组滚动优化空间
  3. 关键步骤

    • 读取输入\(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]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2)\),枚举所有区间
    • 空间复杂度:\(O(N)\),滚动数组优化后
  5. 区间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
posted @ 2026-08-28 15:57  团爸讲算法  阅读(3)  评论(0)    收藏  举报