记录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 步
所以:
dp[i]=dp[i−1]+dp[i−2]+dp[i−3]dp[i]=dp[i−1]+dp[i−2]+dp[i−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] = 1dp[2] = 2dp[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]=7dp[5]=13dp[6]=24dp[7]=44dp[8]=81dp[9]=149dp[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]=1或dp[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,则从 dp[1] 开始)
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
cppdp[0][*] = 0; // 0 个物品,价值为 0 dp[*][0] = 0; // 容量为 0,装不下完全背包:类似,但注意初始化是否允许"恰好装满"
🔑 关键:
dp[0][v]的含义要明确(选 0 个物品时的最大价值)
4. 区间 DP(如石子合并)
边界 :长度为 1 的区间cpp
cppfor (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,可不设 dp[0]
✅ 技巧 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在初始化或转移时显式处理:
cppif (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 开始)
四、总结:处理边界四步法
- 理解状态定义 :
dp[i]到底表示什么?- 找出最小问题:n=0,1,2... 时答案是多少?
- 手动验证:用样例或小数据检查边界值
- 统一处理非法状态:障碍、禁区、不可达等设为 0 或 -∞
🎯 一句话记住:
边界条件 = 递推无法覆盖的最小情况 + 非法状态的显式处理
掌握这一点,DP 的正确率会大幅提升!