P2800 又上锁妖塔 题解复盘

P2800 又上锁妖塔 题解复盘

模块:线性动态规划 / 状态机 DP

目标:在仙术有冷却限制的情况下,求到达第 n 层的最短时间

基本信息

项目 内容
题目编号、来源 P2800 洛谷 / 又上锁妖塔
训练层级 B 变形题
知识版块 线性 DP、状态机 DP、最短时间

解题前 · 关键信号识别

维度 分析
目标、约束、底层结构 目标 :到达第 n 层时总耗时最小。 约束 :可以爬一层,也可以用仙术跳一层或两层;仙术免费,但每次跳完后必须至少爬一层才能再次使用。 底层结构:仅知道"当前在第几层"不够,因为还需要知道"当前是否能继续使用仙术",因此需要额外记录状态。
数据规模 n≤10^6,必须使用接近 O(n) 的算法。
候选算法和依据 算法:状态机 DP。 依据:同样到达第 i 层,因为"能不能继续跳"不同,后续决策不同,因此定义 dp[i][0/1]
复杂度预判 时间复杂度 O(n),空间复杂度 O(n),还可以进一步压缩空间。

解题后 · 外化复盘

维度 内容
实现结构 / 核心思路 定义两个状态:dp[i][0] 表示到第 i 层后可以继续使用仙术的最小时间;dp[i][1] 表示到第 i 层后不能继续使用仙术的最小时间。爬一层后一定恢复体力,因此转移到状态 0;使用仙术跳一层或两层后进入状态 1
错因回溯 1. 一开始只想到 dp[i],没有记录能否使用仙术。 2. 容易把跳跃也加上 v[i],但题目明确跳跃不耗时。 3. i=1 时直接访问 dp[i-2] 会越界,需要单独判断。 4. DP 初始状态不能默认全为 0,否则 min 会把不存在的状态当成合法状态。
边界和易错点 1. 初始在地面,即第 0 层,并且可以使用仙术,所以 dp[0][0]=0。 2. 其他状态初始化为 INF。 3. 跳一层来自 dp[i-1][0],跳两层来自 dp[i-2][0]。 4. 最后到达第 n 层时,无论能不能继续跳都已经完成任务,因此答案取两个状态最小值。
下次看到什么信号,我应该想到这个方法 看到"同一个位置存在不同内部状态""技能冷却""是否有体力""是否持有某种东西"等条件,想到:状态机 DP,使用 dp[i][state] 记录额外状态。

AC 完整代码

cpp 复制代码
#include<iostream>
#include<queue>
#include<algorithm>
#include<cmath>
#include<iomanip>
using namespace std;

int dp[1000005][2];
const int INF=1e9;

int main(){
    int n;
    cin>>n;

    int v[n+1];

    for(int i=1;i<=n;i++){
        cin>>v[i];
    }

    for(int i=0;i<=n;i++){
        dp[i][0]=INF;
        dp[i][1]=INF;
    }

    dp[0][0]=0;

    for(int i=1;i<=n;i++){

        // 跳一层到第 i 层
        dp[i][1]=min(
            dp[i][1],
            dp[i-1][0]
        );

        // 爬一层到第 i 层
        dp[i][0]=min(
            dp[i-1][1],
            dp[i-1][0]
        )+v[i];

        // 跳两层到第 i 层
        if(i>=2){
            dp[i][1]=min(
                dp[i][1],
                dp[i-2][0]
            );
        }
    }

    cout<<min(dp[n][0],dp[n][1]);

    return 0;
}

本题知识点总结

1. 为什么不能只定义 dp[i]

如果只写:

cpp 复制代码
dp[i]

表示到达第 i 层的最短时间,会丢失一个非常重要的信息:

text 复制代码
当前还能不能使用仙术?

例如,同样到达第 5 层:

text 复制代码
情况1:刚刚是爬上来的
→ 可以继续跳

情况2:刚刚是跳上来的
→ 不能继续跳

后续决策不同,因此这两个状态不能合并。


2. 状态定义

cpp 复制代码
dp[i][0]

表示:

到达第 i 层,并且当前可以使用仙术时的最短时间。

cpp 复制代码
dp[i][1]

表示:

到达第 i 层,并且当前不能使用仙术时的最短时间。


3. 爬一层的转移

无论之前:

text 复制代码
能跳

还是:

text 复制代码
不能跳

只要爬过一层,就满足了"休息至少一层"的要求。

所以:

cpp 复制代码
dp[i][0]
=
min(
    dp[i-1][0],
    dp[i-1][1]
)
+
v[i];

这里的:

cpp 复制代码
+v[i]

表示爬第 i 层需要花费的时间。


4. 跳一层的转移

仙术只有在当前"可以跳"的状态下才能使用。

所以:

cpp 复制代码
dp[i][1]
=
min(
    dp[i][1],
    dp[i-1][0]
);

跳跃免费,因此不需要:

cpp 复制代码
+v[i]

5. 跳两层的转移

同样必须从能使用仙术的状态过来:

cpp 复制代码
dp[i][1]
=
min(
    dp[i][1],
    dp[i-2][0]
);

但是只有:

text 复制代码
i>=2

时才存在 i-2 层,因此需要判断边界。


6. 为什么 DP 要初始化为 INF

如果数组默认都是 0:

cpp 复制代码
dp[i][0]=0;
dp[i][1]=0;

那么:

cpp 复制代码
min(...)

会错误地认为某些根本不存在的路径耗时为 0。

所以:

cpp 复制代码
dp[i][0]=INF;
dp[i][1]=INF;

表示这些状态一开始不可达。

唯一合法初始状态:

cpp 复制代码
dp[0][0]=0;

表示:

初始在地面,并且仙术可以使用。


7. 为什么最后取两个状态最小值

到达第 n 层时:

text 复制代码
可能是爬上去的

对应:

cpp 复制代码
dp[n][0]

也可能:

text 复制代码
是跳上去的

对应:

cpp 复制代码
dp[n][1]

无论是哪种情况,都已经到达塔顶。

所以答案:

cpp 复制代码
min(dp[n][0],dp[n][1])

与普通线性 DP 对比

模型 状态
最大子段和 dp[i]
普通爬楼梯 dp[i]
P2800 又上锁妖塔 dp[i][0/1]

区别在于:

P2800 到达同一个位置后,还有"能否使用仙术"这个额外状态,因此需要多开一维。

状态机 DP 识别信号

以后看到:

text 复制代码
有没有钥匙
有没有体力
能不能使用技能
技能是否冷却
当前持有什么货币
当前是否持股
今天工作还是休息

都要考虑:

cpp 复制代码
dp[i][state]

一句话总结

当"当前位置"不足以决定后续转移时,就把影响未来的额外信息加入 DP 状态;本题就是用 0/1 记录当前能不能继续使用仙术。

相关推荐
evans在进步21 小时前
LeetCode 322:零钱兑换——Java 动态规划详解
java·leetcode·动态规划
我变成萤火虫1 天前
2026 ICPC沈阳邀请赛Vp补题
数据结构·c++·算法·贪心算法·stl·排序算法·动态规划
wangjialelele2 天前
动态规划DP经典题型总结(Java、C++):路径、子数组、子序列、背包问题详解
java·c++·算法·面试·动态规划·代理模式
evans在进步2 天前
LeetCode 198:打家劫舍——Java 动态规划详解
java·leetcode·动态规划
(❁´◡`❁)Jimmy(❁´◡`❁)3 天前
P1156 [USACO01OPEN] 垃圾陷阱
算法·动态规划
OuO-23 天前
笔试强训 Day 44:最小差值、kotori 和素因子、dd 爱科学 1.0
二分查找·动态规划·贪心·模拟·记忆化搜索·递归·状态表示优化
银-豆豆4 天前
数据结构与算法-动态规划、回溯与贪心
算法·动态规划
2601_960906724 天前
具备抗弯耐磨的性能特点
大数据·人工智能·物联网·决策树·动态规划
-dzk-4 天前
【动态规划】LC 118.杨辉三角
算法·动态规划