瑞瑞的魔法背包
【题目来源】
瑞学堂:瑞瑞的魔法背包
【题目描述】
瑞瑞在魔法学院学习,他有一个容量为 \(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
【核心思想】
-
问题分析:给定 \(N\) 件物品(重量 \(w_i\),魔力值 \(v_i\)),背包容量 \(M\),要求选取物品使总魔力值最大,且任意两个相邻选取物品的下标差 \(\leq K\)。这是一个带间隔约束的01背包 + 单调队列优化问题,关键在于将"下标差约束"转化为滑动窗口,并用单调队列将转移复杂度从 \(O(K)\) 优化到 \(O(1)\)。
-
算法选择:
- 01背包动态规划:\(dp[i][j]\) 表示以第 \(i\) 件物品为最后一个选取物品、总重量为 \(j\) 时的最大魔力值
- 单调队列优化(Sliding Window Maximum):对每个重量 \(j\),维护一个按物品下标递增、值递减的双端队列,将"枚举前 \(K\) 个物品"的 \(O(K)\) 转移优化为 \(O(1)\) 的队首查询
- 滑动窗口约束:队列中只保留下标在 \([i-K, i-1]\) 范围内的状态,自动满足相邻间隔约束
-
关键步骤:
- 初始化:\(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]\) 的元素(维护递减单调性),然后入队
- 查询阶段(\(j\) 从 \(w_i\) 到 \(M\)):
- 输出 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N \cdot M)\),每件物品对每个重量 \(j\) 进行 \(O(1)\) 的查询和入队操作
- 空间复杂度:\(O(N \cdot M)\),\(N \times M\) 个DP状态,单调队列总空间 \(O(N \cdot M)\)
-
单调队列优化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
浙公网安备 33010602011771号