P12247 跳舞机 解题报告


P12247 跳舞机 解题报告

大家好!

欢迎阅读这份 P12247 跳舞机问题的解题报告。如果你觉得官方题解有点“硬核”,别担心,这篇报告会用更接地气的方式,带你一步步弄懂这道题的解法。

1. 问题分析:我们要干什么?

首先,我们来读懂题目。电玩城老板小 O 想在营业的 m 分钟里,让一台跳舞机产生最大的“兴奋值”。

  • 资源:一台跳舞机,营业时间为 1m 分钟。
  • 规则
    1. 游戏一次 k 分钟,必须连续。
    2. 同一时间只能有一个人玩。
    3. 玩家 i 有自己的“在店时间” [l_i, r_i] 和“兴奋值” w_i
    4. 玩家要玩一局,那么这 k 分钟必须完全在他的“在店时间”内。
  • 目标:合理安排每一段 k 分钟给谁玩(或者不玩),让总兴奋值最高。

看到“最大化总价值”、“按时间顺序做决策”这类问题,我们脑海里应该闪过一个熟悉的身影——动态规划(Dynamic Programming, DP)

2. 核心思路:动态规划

DP 的精髓在于把一个大问题分解成小问题,并通过记录小问题的最优解来推导出大问题的最优解。

对于这道题,最自然的分解方式就是沿着时间轴。我们来定义一个状态:

dp[i]:表示在 1i 分钟这段时间里,我们能获得的最大总兴奋值。

我们的最终目标就是求出 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+1r_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+1r_j >= i,那么计算一个 dp[i] 就需要 O(n) 的时间,总时间复杂度是 O(m*n)。根据数据范围(nm 都是 50 万级别),这肯定会超时。

我们需要一个更聪明的办法来找到 W

优化思路:扫描线 + 优先队列(堆)

让我们换个角度。当我们沿着时间轴从 i=1 走到 m 时,玩家们会“出现”又“消失”。

我们可以维护一个“当前可选择的玩家池”。当我们计算 dp[i] 时,这个池子里放着所有理论上可以开始一局游戏(游戏时段为 [i-k+1, i])的玩家。

  1. 什么时候把玩家加入池子?
    一个玩家 j 最早可以玩的一局游戏,其结束时间是 l_j + k - 1。所以,当我们的时间 i 走到 l_j + k - 1 时,玩家 j 就成了一个新的“候选人”,我们可以把他加入池子。

  2. 池子里需要什么信息?
    我们需要快速找到兴奋值最高的玩家。优先队列(大顶堆) 是完美的数据结构!我们把玩家的 (兴奋值 w, 离店时间 r) 放进堆里,按 w 从大到小排序。

  3. 如何处理玩家离店?
    池子里的玩家不能一直待着。当计算 dp[i] 时,堆顶的玩家虽然兴奋值最高,但他可能已经离店了(即他的 r_j < i)。这种玩家是“过期”的,不能选择。
    所以,在取堆顶玩家之前,我们需要做一个检查:如果堆顶玩家的离店时间 r_j 小于当前时间 i,说明他已经走了,就把他从堆里弹出。 我们重复这个过程,直到堆顶的玩家是在店的,或者堆为空。

这个“用到了再检查,过期了再删除”的技巧,通常被称为“懒删除”,非常高效。

5. 算法流程总结

现在,我们可以梳理出完整的、高效的算法流程了:

  1. 预处理:创建一个数组 qj(可以理解为很多个篮子),qj[t] 这个篮子存放所有“最早能结束游戏的时间点”为 t 的玩家信息(离店时间和兴奋值)。

    • 遍历所有 n 个玩家,如果一个玩家 j 能玩得起至少一局游戏(r_j - l_j + 1 >= k),就把他的 {r_j, w_j} 存入 qj[l_j + k - 1]
  2. 初始化

    • dp 数组全部初始化为0。
    • 创建一个空的优先队列 s(大顶堆,用来存 {w_j, r_j})。
  3. 主循环(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)
  4. 最终答案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),顺利通过此题。

希望这份报告能帮助你彻底理解这道题的精妙之处!

posted @ 2025-07-17 15:00  surprise_ying  阅读(18)  评论(0)    收藏  举报