P12247 跳舞机 解题报告
P12247 跳舞机 解题报告
大家好!
欢迎阅读这份 P12247 跳舞机问题的解题报告。如果你觉得官方题解有点“硬核”,别担心,这篇报告会用更接地气的方式,带你一步步弄懂这道题的解法。
1. 问题分析:我们要干什么?
首先,我们来读懂题目。电玩城老板小 O 想在营业的 m
分钟里,让一台跳舞机产生最大的“兴奋值”。
- 资源:一台跳舞机,营业时间为
1
到m
分钟。 - 规则:
- 游戏一次
k
分钟,必须连续。 - 同一时间只能有一个人玩。
- 玩家
i
有自己的“在店时间”[l_i, r_i]
和“兴奋值”w_i
。 - 玩家要玩一局,那么这
k
分钟必须完全在他的“在店时间”内。
- 游戏一次
- 目标:合理安排每一段
k
分钟给谁玩(或者不玩),让总兴奋值最高。
看到“最大化总价值”、“按时间顺序做决策”这类问题,我们脑海里应该闪过一个熟悉的身影——动态规划(Dynamic Programming, DP)。
2. 核心思路:动态规划
DP 的精髓在于把一个大问题分解成小问题,并通过记录小问题的最优解来推导出大问题的最优解。
对于这道题,最自然的分解方式就是沿着时间轴。我们来定义一个状态:
dp[i]
:表示在 1
到 i
分钟这段时间里,我们能获得的最大总兴奋值。
我们的最终目标就是求出 dp[m]
。
3. 状态转移:决策的艺术
现在我们来思考,如何计算 dp[i]
?站在第 i
分钟这个时间点,回顾过去,我们要做一个决策。对于第 i
分钟,有两种可能的情况:
情况一:第 i
分钟,跳舞机是空闲的。
如果从 i-1
分钟到 i
分钟这段时间机器没在用,那就意味着第 i
分钟结束时的最大兴奋值,和第 i-1
分钟结束时是一样的。
所以,我们有第一个状态转移:
dp[i] = dp[i-1]
情况二:第 i
分钟,刚好有一局游戏结束。
如果有一局游戏在第 i
分钟结束,由于每局游戏持续 k
分钟,那么这局游戏一定是在 [i-k+1, i]
这个时间段玩的。
- 这台机器在
i-k
分钟之前是空闲的,这部分能贡献的最大兴奋值是dp[i-k]
。 - 在
[i-k+1, i]
这段时间,我们安排了一个玩家进行游戏。为了让总兴奋值最大,我们肯定会选择一个能在这段时间玩,并且兴奋值最高的玩家。
假设这个最厉害的玩家的兴奋值是 W
,那么这种情况下的总兴奋值就是 dp[i-k] + W
。
这里的 W
是所有满足 l_j <= i-k+1
且 r_j >= i
的玩家 j
中,w_j
的最大值。
综合两种情况,dp[i]
的值就是这两种可能中的较大者:
dp[i] = max( dp[i-1], dp[i-k] + W )
4. 难点与优化:如何快速找到最合适的玩家?
这个 DP 思路看起来很美好,但有个大问题:在计算每个 dp[i]
时,我们怎么快速找到那个 W
?
如果每次都遍历所有 n
个玩家,去检查他们是否满足时间要求 l_j <= i-k+1
且 r_j >= i
,那么计算一个 dp[i]
就需要 O(n)
的时间,总时间复杂度是 O(m*n)
。根据数据范围(n
和 m
都是 50 万级别),这肯定会超时。
我们需要一个更聪明的办法来找到 W
。
优化思路:扫描线 + 优先队列(堆)
让我们换个角度。当我们沿着时间轴从 i=1
走到 m
时,玩家们会“出现”又“消失”。
我们可以维护一个“当前可选择的玩家池”。当我们计算 dp[i]
时,这个池子里放着所有理论上可以开始一局游戏(游戏时段为 [i-k+1, i]
)的玩家。
-
什么时候把玩家加入池子?
一个玩家j
最早可以玩的一局游戏,其结束时间是l_j + k - 1
。所以,当我们的时间i
走到l_j + k - 1
时,玩家j
就成了一个新的“候选人”,我们可以把他加入池子。 -
池子里需要什么信息?
我们需要快速找到兴奋值最高的玩家。优先队列(大顶堆) 是完美的数据结构!我们把玩家的(兴奋值 w, 离店时间 r)
放进堆里,按w
从大到小排序。 -
如何处理玩家离店?
池子里的玩家不能一直待着。当计算dp[i]
时,堆顶的玩家虽然兴奋值最高,但他可能已经离店了(即他的r_j < i
)。这种玩家是“过期”的,不能选择。
所以,在取堆顶玩家之前,我们需要做一个检查:如果堆顶玩家的离店时间r_j
小于当前时间i
,说明他已经走了,就把他从堆里弹出。 我们重复这个过程,直到堆顶的玩家是在店的,或者堆为空。
这个“用到了再检查,过期了再删除”的技巧,通常被称为“懒删除”,非常高效。
5. 算法流程总结
现在,我们可以梳理出完整的、高效的算法流程了:
-
预处理:创建一个数组
qj
(可以理解为很多个篮子),qj[t]
这个篮子存放所有“最早能结束游戏的时间点”为t
的玩家信息(离店时间和兴奋值)。- 遍历所有
n
个玩家,如果一个玩家j
能玩得起至少一局游戏(r_j - l_j + 1 >= k
),就把他的{r_j, w_j}
存入qj[l_j + k - 1]
。
- 遍历所有
-
初始化:
dp
数组全部初始化为0。- 创建一个空的优先队列
s
(大顶堆,用来存{w_j, r_j}
)。
-
主循环(DP过程):从
i = 1
循环到m
:- 决策一(空闲):
dp[i] = dp[i-1]
。 - 加入新候选人:检查
qj[i]
这个篮子,把里面的所有玩家都加入到优先队列s
中。 - 清理过期玩家:循环检查
s
的堆顶。如果堆顶玩家的离店时间小于i
,就s.pop()
,直到堆顶玩家有效或堆为空。 - 决策二(游戏):如果此时
s
不为空,说明有玩家可以玩。取出堆顶玩家的信息{w_top, r_top}
。用这个玩家的兴奋值更新dp[i]
:
dp[i] = max(dp[i], dp[i-k] + w_top)
。
- 决策一(空闲):
-
最终答案:
dp[m]
就是我们要求的最大总兴奋值。
6. 代码解析
我们来看一下题解给出的代码是如何实现这个流程的。
#include <bits/stdc++.h>
using namespace std;
int n, m, k;
long long dp[500005];
vector<pair<int, int>> qj[500005]; // 预处理的“篮子”
signed main() {
cin >> n >> m >> k;
for (int i = 1; i <= n; ++i) {
int l, r, w;
cin >> l >> r >> w;
// 玩家必须待够k分钟
if (l + k - 1 <= r)
// 将 {离店时间 r, 兴奋值 w} 放入 l+k-1 号篮子
qj[l + k - 1].push_back({r, w});
}
dp[0] = 0;
// C++默认是大顶堆,这里用 greater 变成小顶堆,
// 通过存入-w来巧妙地实现大顶堆的效果。
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> s;
for (int i = 1; i <= m; ++i) {
// 决策一:假设机器空闲
dp[i] = dp[i - 1];
// 加入在 i 时刻成为候选人的玩家
for (auto j : qj[i]) {
// j.first 是 r, j.second 是 w
// 存入 {-w, r},这样w越大,-w越小,在小顶堆里越靠前
s.push({-j.second, j.first});
}
// 清理过期的玩家
// s.top().second 是 r, 如果 r < i, 说明玩家已离店
while (!s.empty() && s.top().second < i) s.pop();
// 决策二:如果还有候选玩家
if (!s.empty()) {
// s.top().first 是 -w,所以要取反得到 w
dp[i] = max(dp[i], dp[i - k] - s.top().first);
}
}
cout << dp[m] << "\n";
}
这段代码完美地实现了我们刚才分析的算法。通过预处理和优先队列,成功地将每次查找最优玩家的时间从 O(n)
优化到了 O(log n)
(堆操作的复杂度),使得总时间复杂度降低到可以接受的 O(m \log n + n)
,顺利通过此题。
希望这份报告能帮助你彻底理解这道题的精妙之处!