瑞瑞的魔法背包

【题目来源】

瑞学堂:瑞瑞的魔法背包

【题目描述】

瑞瑞在魔法学院学习,他有一个容量为 \(M\) 的魔法背包。学院仓库里共有 \(N\) 件魔法物品,每件物品具有重量 \(w_i\) 和魔力值 \(v_i\)。由于魔法的限制,瑞瑞不能任意选择物品——在选取物品时,任意两个相邻选取的物品在原始排列中的下标差不能超过 \(K\)

瑞瑞希望最大化背包中物品的总魔力值。请你编写程序,帮助瑞瑞计算在不超过背包容量的前提下,他能获得的最大总魔力值。

注意:如果不选取任何物品,总魔力值为 \(0\)

【输入】

第一行三个整数 \(N,M,K\),用空格分隔,分别表示物品数量、背包容量和相邻选取物品的最大间隔。
接下来 \(N\) 行,每行两个整数 \(w_i\) 和 \(v_i\),表示第 \(i\) 件物品的重量和魔力值。

【输出】

输出一个整数,表示在满足条件的情况下能获得的最大总魔力值。

【输入样例】

3 10 2
2 3
2 4
3 5

【输出样例】

12

【核心思想】

  1. 问题分析:给定 \(N\) 件物品(重量 \(w_i\),魔力值 \(v_i\)),背包容量 \(M\),要求选取物品使总魔力值最大,且任意两个相邻选取物品的下标差 \(\leq K\)。这是一个带间隔约束的01背包 + 单调队列优化问题,关键在于将"下标差约束"转化为滑动窗口,并用单调队列将转移复杂度从 \(O(K)\) 优化到 \(O(1)\)

  2. 算法选择

    • 01背包动态规划\(dp[i][j]\) 表示以第 \(i\) 件物品为最后一个选取物品、总重量为 \(j\) 时的最大魔力值
    • 单调队列优化(Sliding Window Maximum):对每个重量 \(j\),维护一个按物品下标递增、值递减的双端队列,将"枚举前 \(K\) 个物品"的 \(O(K)\) 转移优化为 \(O(1)\) 的队首查询
    • 滑动窗口约束:队列中只保留下标在 \([i-K, i-1]\) 范围内的状态,自动满足相邻间隔约束
  3. 关键步骤

    • 初始化\(dq[0]\) 加入状态 \(\{0, 0\}\)(重量 \(0\)、魔力值 \(0\)、下标 \(0\)),表示不选任何物品的初始状态
    • 处理每件物品 \(i\)\(1\)\(N\)):
      • 查询阶段\(j\)\(w_i\)\(M\)):
        • 访问 \(dq[j - w_i]\),移除队首下标 \(< i - K\) 的元素(超出间隔限制)
        • 若队列非空,dp_cur[j] = q.front().v + v[i],更新全局答案 \(ans\)
      • 入队阶段\(j\)\(w_i\)\(M\)):
        • \(dp\_cur[j]\) 可达,将其加入 \(dq[j]\):从队尾移除值 \(\leq dp\_cur[j]\) 的元素(维护递减单调性),然后入队
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \cdot M)\),每件物品对每个重量 \(j\) 进行 \(O(1)\) 的查询和入队操作
    • 空间复杂度:\(O(N \cdot M)\)\(N \times M\) 个DP状态,单调队列总空间 \(O(N \cdot M)\)
  5. 单调队列优化DP的核心思想

    • 约束转窗口:下标差 \(\leq K\) 的约束等价于"只能从最近 \(K\) 个物品转移",形成长度为 \(K\) 的滑动窗口
    • 按重量分离状态:每个重量 \(j\) 独立维护一个单调队列,因为01背包中重量是严格累加的,不同重量互不影响
    • 单调性保证最优:队列按魔力值递减维护,队首始终是窗口内的最大价值状态,实现 \(O(1)\) 查询最优前驱
    • 延迟删除策略:不立即删除所有过期元素,只在查询时检查队首是否越界,均摊仍为 \(O(1)\)
    • 适用于"带滑动窗口约束的DP优化"问题,核心在于将多维约束通过单调队列降维,实现线性时间复杂度

【算法标签】

01背包

【代码详解】

// 85分版本(剩下3个点都是超时)
#include <bits/stdc++.h>
using namespace std;
#define int long long    // 将int定义为long long,避免DP状态值溢出
const int N = 2005, M = 5005;  // N为物品最大数量,M为背包最大容量
int n, m, k, ans;        // n为物品数量,m为背包容量,k为相邻选取物品的最大间隔,ans记录最大魔力值
int w[N], v[N];          // w[i]为第i件物品的重量,v[i]为第i件物品的魔力值
int dp[N][M];            // dp[i][j]表示以第i件物品为最后一个选取的物品,且总重量恰好为j时的最大魔力值

signed main()            // 使用signed main配合#define int long long
{
    cin >> n >> m >> k;  // 读入物品数量、背包容量、最大间隔
    for (int i = 1; i <= n; i++)  // 读入每件物品的重量和魔力值
        cin >> w[i] >> v[i];

    memset(dp, -0x3f, sizeof(dp));  // 将dp数组初始化为负无穷,表示不可达状态

    // 动态规划:枚举每件物品作为最后一个选取的物品
    for (int i = 1; i <= n; i++)  // i为当前考虑的物品(作为最后一个选取的物品)
    {
        // 初始化:只选第i件物品的情况
        if (w[i] <= m)            // 如果第i件物品重量不超过背包容量
            dp[i][w[i]] = v[i];   // 以i结尾,重量为w[i]时的魔力值为v[i]

        // 状态转移:枚举当前背包总重量j
        for (int j = w[i] + 1; j <= m; j++)  // j从w[i]+1到m(确保能放下第i件物品)
        {
            // 枚举上一个选取的物品t,要求i-t <= k(相邻选取物品下标差不超过k)
            // t的范围为[max(1, i-k), i-1],即上一个物品可以是往前数k个位置内的任意一个
            for (int t = max(1LL, (int)(i - k)); t <= i - 1; t++)
            {
                // 状态转移:从以t结尾、重量为j-w[i]的状态,加上第i件物品
                dp[i][j] = max(dp[i][j], dp[t][j - w[i]] + v[i]);
            }
        }
    }

    // 统计答案:在所有以任意物品结尾、任意重量的状态中找到最大值
    for (int i = 1; i <= n; i++)      // 枚举最后一个物品
        for (int j = 0; j <= m; j++)  // 枚举总重量
            ans = max(ans, dp[i][j]);  // 更新最大魔力值

    // 注意:ans可能为负无穷(如果没有选取任何物品),但题目说明不选取时总魔力值为0
    // 由于dp初始化为负无穷,如果没有合法状态,ans仍为负无穷,需要与0取max
    cout << max(0LL, ans) << endl;    // 输出最大总魔力值(至少为0)
    return 0;
}
// AC版本
#include <bits/stdc++.h>
using namespace std;
const int N = 2005, M = 5005;  // N为物品最大数量,M为背包最大容量
int n, m, k, ans;              // n为物品数量,m为背包容量,k为相邻选取物品的最大间隔,ans记录最大魔力值
int w[N], v[N];                // w[i]为第i件物品的重量,v[i]为第i件物品的魔力值
int dp_cur[M];                 // dp_cur[j]表示当前物品i作为最后一个选取物品时,总重量为j的最大魔力值

// 定义结构体用于单调队列:存储状态值和对应的物品下标
struct Node
{
    int v, idx;                // v为dp状态值(最大魔力值),idx为该状态对应的物品下标
};

int main()
{
    cin >> n >> m >> k;       // 读入物品数量、背包容量、最大间隔
    for (int i = 1; i <= n; i++)  // 读入每件物品的重量和魔力值
        cin >> w[i] >> v[i];

    // dq[j]为单调队列,存储总重量为j时的最优dp状态(按物品下标维护滑动窗口)
    // 每个重量j对应一个单调队列,队列中元素按物品下标递增、值递减
    vector<deque<Node>> dq(m + 1);
    dq[0].push_back({0, 0});   // 初始化:重量为0时,不选任何物品,魔力值为0,下标为0

    // 依次处理每件物品
    for (int i = 1; i <= n; i++)  // i为当前考虑的物品
    {
        memset(dp_cur, -1, sizeof(dp_cur));  // 初始化当前dp数组为-1(不可达)

        // 第一步:从单调队列中查询以第i件物品结尾的最优状态
        for (int j = w[i]; j <= m; j++)  // 枚举总重量j(至少为w[i]才能放下第i件物品)
        {
            auto &q = dq[j - w[i]];      // 查询重量为j-w[i]的单调队列(放入i之前的状态)
            // 维护滑动窗口:移除下标小于i-k的元素(间隔超过k的不合法)
            while (!q.empty() && q.front().idx > 0 && q.front().idx < i - k)
                q.pop_front();
            // 如果队列非空且队首状态可达
            if (!q.empty())
            {
                if (q.front().v != -1)   // 如果队首状态值有效
                {
                    dp_cur[j] = q.front().v + v[i];  // 状态转移:加上第i件物品的魔力值
                    if (dp_cur[j] > ans)             // 更新全局最大魔力值
                    {
                        ans = dp_cur[j];
                    }
                }
            }
        }

        // 第二步:将当前物品i产生的状态加入对应重量的单调队列
        for (int j = w[i]; j <= m; j++)  // 枚举总重量j
        {
            if (dp_cur[j] != -1)         // 如果当前状态可达
            {
                auto &q = dq[j];         // 获取重量为j的单调队列
                // 维护单调性:移除队列尾部值小于等于当前值的元素(保持递减)
                while (!q.empty() && q.back().v <= dp_cur[j])
                    q.pop_back();
                q.push_back({dp_cur[j], i});  // 将当前状态加入队列尾部
            }
        }
    }
    cout << ans << endl;                // 输出最大总魔力值
    return 0;
}

【运行结果】

3 10 2
2 3
2 4
3 5
12
posted @ 2026-08-06 17:01  团爸讲算法  阅读(10)  评论(0)    收藏  举报