P10250 [GESP样题 六级] 下楼梯

记录80

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
long long dp[65];
int main(){
	int n;
	cin>>n;
	dp[1]=1,dp[2]=2,dp[3]=4;
	for(int i=4;i<=n;i++) dp[i]=dp[i-1]+dp[i-2]+dp[i-3];
	cout<<dp[n];
	return 0;
} 

题目传送门https://www.luogu.com.cn/problem/P10250


突破口

顽皮的小明发现,下楼梯时每步可以走 1 个台阶、2 个台阶或 3 个台阶。现在一共有 N 个台阶,你能帮小明算算有多少种方案吗?


思路

🔍 一、题目思路分析

1. 问题本质:爬楼梯变种(三步可达)
  • 小明在第 0 级台阶,要走到第 N
  • 每次可以走 1 步、2 步 或 3 步
  • 问:有多少种不同的走法?

这是经典的 动态规划(DP) 问题,是"斐波那契数列"的扩展。


2. 状态定义

dp[i] 表示走到第 i 级台阶的方案数。

3. 状态转移方程

要到达第 i 级,最后一步可能是:

  • i−1 走 1 步
  • i−2 走 2 步
  • i−3 走 3 步

所以:

dpi=dpi−1+dpi−2+dpi−3dpi=dpi−1+dpi−2+dpi−3

4. 初始条件(边界)
  • dp[0] = 1(站在地面,1 种方案:不动)
    但本题从 N≥1 开始,且样例给出:
    • N=1 → 1 种(1)
    • N=2 → 2 种(1+1, 2)
    • N=3 → 4 种(1+1+1, 1+2, 2+1, 3)

所以直接初始化:

  • dp[1] = 1
  • dp[2] = 2
  • dp[3] = 4

✅ 验证 N=4

  • dp[4] = dp[3] + dp[2] + dp[1] = 4 + 2 + 1 = 7 ✅(符合样例)

代码分析

✅ 代码逐行解释

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
long long dp[65];
  • 引入标准库
  • 定义 dp 数组,大小 65(因为 N ≤ 60,留余量)
  • 使用 long long:防止结果溢出(N=60 时答案很大)
cpp 复制代码
int main(){
    int n;
    cin >> n;
  • 读入台阶总数 n
cpp 复制代码
    dp[1] = 1, dp[2] = 2, dp[3] = 4;
  • 初始化前三个值(根据实际走法枚举得出)
cpp 复制代码
    for(int i = 4; i <= n; i++)
        dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
  • 从第 4 级开始,按递推公式计算
  • 时间复杂度:O(n),空间复杂度:O(n)
cpp 复制代码
    cout << dp[n];
    return 0;
}
  • 输出走到第 n 级的方案总数

🧪 样例验证

样例 1:n = 4
  • dp[4] = 4 + 2 + 1 = 7
样例 2:n = 10
  • 依次计算:
    • dp[4]=7
    • dp[5]=13
    • dp[6]=24
    • dp[7]=44
    • dp[8]=81
    • dp[9]=149
    • dp[10]=274

✅ 总结

要点 说明
模型 三阶线性递推(类斐波那契)
状态转移 dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
初始值 dp[1]=1, dp[2]=2, dp[3]=4
数据类型 long long 防止溢出(N=60 时约 10¹² 量级)
复杂度 O(n) 时间,O(n) 空间

补充

在动态规划(DP)中,边界条件(或称初始状态、base case)是整个递推的起点。如果边界处理错误,即使状态转移方程正确,结果也会全错。

下面从 通用原则 + 常见场景 + 实战技巧 三方面讲解如何正确处理 DP 的边界条件。


一、核心原则:"最小不可再分的子问题"

边界条件对应的是问题规模最小、无需递推就能直接得出答案的情况

✅ 问自己:

"当 n = 0、1、2 ... 时,答案是多少?能否手动枚举?"


二、常见 DP 类型与边界处理方法

1. 线性递推型(如爬楼梯、斐波那契)

  • 问题f(n) = f(n-1) + f(n-2) + ...
  • 边界:根据题意手动计算前几项
题目 边界设置 说明
爬楼梯(1/2步) dp[0]=1, dp[1]=1dp[1]=1, dp[2]=2 取决于是否定义 dp[0]
本题(1/2/3步) dp[1]=1, dp[2]=2, dp[3]=4 直接枚举小规模情况
斐波那契 dp[1]=1, dp[2]=1 题目定义

🔑 技巧:优先按题目输入范围的最小值来设边界(如 N≥1,则从 dp1 开始)


2. 网格路径型(如走格子)

  • 状态dp[i][j] 表示走到 (i,j) 的方案数
  • 边界:第一行、第一列(只能沿一个方向走)
cpp 复制代码
// 无障碍
for (int i=1; i<=n; i++) dp[i][1] = 1;
for (int j=1; j<=m; j++) dp[1][j] = 1;
  • 有障碍 :若某格是障碍,则 dp[i][j] = 0,且后续路径无法通过它

⚠️ 注意:起点是否合法?

如果 (1,1) 是障碍 → 整个答案为 0


3. 背包问题

  • 01 背包 :cpp

    cpp 复制代码
    dp[0][*] = 0;   // 0 个物品,价值为 0
    dp[*][0] = 0;   // 容量为 0,装不下
  • 完全背包:类似,但注意初始化是否允许"恰好装满"

🔑 关键:dp[0][v] 的含义要明确(选 0 个物品时的最大价值)


4. 区间 DP(如石子合并)

  • 边界 :长度为 1 的区间cpp

    cpp 复制代码
    for (int i=1; i<=n; i++) dp[i][i] = 0; // 合并一个堆,代价为 0

三、处理边界的实用技巧

✅ 技巧 1:从"空状态"开始思考

  • 例如:爬楼梯中,dp[0] = 1 表示"站在地面,有 1 种方式(什么都不做)"
  • 这能让递推更统一(dp[2] = dp[1] + dp[0]

但注意:题目是否允许 n=0? 若输入保证 n≥1,可不设 dp0


✅ 技巧 2:手动枚举小数据验证边界

  • n=1,2,3 手动算出答案
  • 看你的 dp[1], dp[2], dp[3] 是否匹配

例:本题中 N=1 → 1 种;N=2 → 2 种;N=3 → 4 种 → 直接赋值


✅ 技巧 3:数组下标对齐问题

  • 有些题用 dp[0] 表示第 1 个元素,有些用 dp[1]
  • 建议 :让 dp[i] 直接对应"第 i 个"(如台阶 i、第 i 个物品),避免混淆

✅ 技巧 4:警惕"非法状态"

  • 如本题中的"偶行列禁区",某些 dp[i][j] 应强制为 0

  • 在初始化或转移时显式处理:

    cpp 复制代码
    if (isForbidden(i, j)) dp[i][j] = 0;
    else dp[i][j] = dp[i-1][j] + dp[i][j-1];

✅ 技巧 5:使用足够大的数组,避免越界

  • 比如 N≤60,开 dp[65] 而不是 dp[61],防止 i-3 越界
  • 或者在循环中控制起始点(如从 i=4 开始)

四、总结:处理边界四步法

  1. 理解状态定义dp[i] 到底表示什么?
  2. 找出最小问题:n=0,1,2... 时答案是多少?
  3. 手动验证:用样例或小数据检查边界值
  4. 统一处理非法状态:障碍、禁区、不可达等设为 0 或 -∞

🎯 一句话记住:

边界条件 = 递推无法覆盖的最小情况 + 非法状态的显式处理

掌握这一点,DP 的正确率会大幅提升!

相关推荐
哥不想学算法25 分钟前
【C++】字符串字面量拼接
开发语言·c++
海石38 分钟前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石38 分钟前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry3 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding3 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL5 小时前
我的数据结构3——链表(link list)
数据结构·链表
a1117765 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜5 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry5 小时前
LeetCode 541. 反转字符串 II
算法
Jerry6 小时前
LeetCode 344. 反转字符串
算法