0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1255,日期见页头。两边不一致时信原站。
题目描述
楼梯有 N 阶,上楼可以一步上一阶,也可以一步上二阶。编一个程序,计算共有多少种不同的走法。
输入格式:一个数字,楼梯数。
输出格式:输出走的方式总数。
数据范围
- 对于 60% 的数据,
N ≤ 50 - 对于 100% 的数据,
1 ≤ N ≤ 5000
1先看清楚:这道题有两关,不是一关
这道题在题单里出现了两次(第 1 章和第 2 章各一次),因为它正好卡在两个不同的地方:
第一关:跑得动吗 -> 朴素递归是指数级的,n = 45 就已经等不起
第二关:装得下吗 -> n = 5000 的答案有 1045 位,long long 差得远
★ 绝大多数人只会撞上第一关,然后以为自己做完了。 第二关是这道题真正的门槛, 而且它不报错、不超时,只是悄悄给出一个错的数。
递推关系本身一眼就能看出来 —— 盯住最后一步:
最后一步要么迈一阶(前面走了 n-1 阶),要么迈两阶(前面走了 n-2 阶)
f(n) = f(n-1) + f(n-2) f(1) = 1, f(2) = 2
输入
4
输出
5
N = 4:走法是 1+1+1+1、1+1+2、1+2+1、2+1+1、2+2,共 5 种。
上面那段输出是仓库里的 p1255.cpp 真跑出来的。
2第 ① 版:照着式子直接递归(对,但跑不完)
// 洛谷 P1255 数楼梯 —— 第 ① 版:照题意直接写递归(对,但跑不完)//// 输入:N(1 <= N <= 5000)// 输出:走法总数//// 这份为什么存在:题意翻译成递归只有一行 ——// 最后一步要么迈一阶、要么迈两阶,所以 f(n) = f(n-1) + f(n-2)。// 这就是斐波那契,也是第 1 章正文里那个「同一个子问题被算了无数遍」的活标本。//// ⚠ 慢到什么程度:**调用次数正好是 2 * f(n-1) - 1**(p1255Count.cpp 逐行比给你看)。// ⚠ 这个式子是**跑出来才对上的**:一开始猜的是 2 * f(n) - 1,一比就差了一整阶。// 「看着显然」的式子也要跑一遍 —— 这本书踩过好几次。// 总之调用次数和答案是同一个量级:f(45) 已经十亿,而题目要的是 f(5000) —— 那个数有 1045 位。// ⇒ 它不是「慢一点」,是**这辈子都跑不完**。//// ★ 第 1 章那句「先别急着优化,记住这个感觉」说的就是它。// 下一版加一个数组就好了;再下一版才轮到这道题真正的难点:数太大,long long 装不下。
#include <bits/stdc++.h>using namespace std;
long long f(int n) { if (n == 1) return 1; // 一阶:只有一种走法 if (n == 2) return 2; // 两阶:1+1 或 2 return f(n - 1) + f(n - 2);}
int main() { int n; if (!(cin >> n)) return 0; cout << f(n) << "\n"; return 0;}点「运行 ▶」看结果
调用次数正好是 2 × f(n-1) − 1(第 ⑤ 步那张表逐行比给你看)。
⚠ 这个式子是跑出来才对上的:一开始我猜的是 2 × f(n) − 1,一比就发现差了一整阶。
「看着显然」的式子也要跑一遍。
⇒ 结论不变:工作量跟着答案一起爆炸。而这道题的答案本身就是天文数字, 所以这一版不是「慢一点」,是这辈子都跑不完。
★ 第 1 章那句「先别急着优化,记住这个感觉」说的就是它。
3第 ② 版:改成递推,快的问题当场解决(但答案错了)
从小往大推一遍,一个循环就完事:
f[1] = 1; f[2] = 2;
for (int i = 3; i <= n; i++) f[i] = f[i - 1] + f[i - 2];
// 洛谷 P1255 数楼梯 —— 第 ② 版:改成递推,快得飞起(**但答案是错的**)//// 输入:N// 输出:走法总数//// 这份为什么存在:把递归倒过来从小往大推,一个循环就完事,// **快的问题当场解决**(5000 阶眨眼就跑完)。很多人写到这儿就交了。//// ⚠ 一处刻意的写法:这里用的是 **unsigned long long**,不是 long long。// 真实的错误当然是用 long long 写的,但**有符号溢出在 C++ 里是未定义行为** ——// 换个编译器、换个优化档,它吐出来的数就可能不一样,那样这一页就没法拿数字说话了。// unsigned 的溢出**有明确定义**(对 2^64 取模),于是「错的那个数」也是可复现的。// ⇒ 演示「这样写是错的」时,要让那个错本身是确定的。(第 44 章立的规矩。)//// ⚠⚠ 然后就 WA 了,而且**样例是过的** —— 这是这道题最坑人的地方。// N = 4 输出 5,N = 40 也对,到第 92 阶(无符号)/ 第 91 阶(有符号)为止都还对,// 再往后它会给出一个**看起来很正常、其实完全错误的数**(溢出之后无声无息)。// 题目要的是 N = 5000,那个答案有 **1045 位**。//// ★ 记住这条:**「跑得动」和「答案对」是两回事,而溢出属于后者。**// 对拍也救不了你 —— 两份都用 long long 的话,它们会一起错,而且错得一模一样。// ⇒ 唯一的办法是**动手前先估一估答案能有多大**(第 44 章整章在讲这件事)。//// p1255Count.cpp 会把「从第几阶开始溢出」精确地找出来。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0;
vector<unsigned long long> f(max(n, 2) + 1); f[1] = 1; f[2] = 2; for (int i = 3; i <= n; i++) f[i] = f[i - 1] + f[i - 2];
cout << f[n] << "\n"; return 0;}点「运行 ▶」看结果
N = 4 对、N = 40 对、样例全过,然后从某一阶开始,它给出的每一个数都是错的:
64 位有符号 (long long) 最多撑到第 91 阶
64 位无符号 (unsigned) 最多撑到第 92 阶
题目要的 第 5000 阶,答案有 1045 位这两条界是用 __int128 当裁判算出来的(第 ⑤ 步那张表)—— 注意是「算」不是「让它溢出去看看」:有符号溢出是未定义行为, 让它自己撞一次得到的数,换台机器可能就不一样,不能当证据。
⚠ 也因此这份演示代码用的是 unsigned long long 而不是 long long:
无符号溢出有明确定义(对 2^64 取模),这样「它错成什么样」才是可复现的。
真实的错误当然是用 long long 写的,两者的结论完全一样 —— 装不下。
★★ 这一条要背下来:「跑得动」和「答案对」是两回事,而溢出属于后者。
⚠ 而且对拍救不了你:两份都用 long long 的话,它们会一起错,而且错得一模一样。
⇒ 唯一的办法是动手前先估一估答案能有多大。这一步只要三秒钟。
4第 ③ 版:高精度加法(正解)
和第 ② 版的差别只有一处:把 long long 换成「一个数组存一个大数」。
递推那两行的形状一模一样,还是 f[i] = f[i-1] + f[i-2]。
高精度加法就是竖式加法,小学怎么算的就怎么写:
低位在前存法(下标 0 是个位)
f[i-1] : [3][2][1] 表示 123
f[i-2] : [9][8] 表示 89
逐位加 : 3+9=12 -> 写 2 进 1
2+8+1=11 -> 写 1 进 1
1+0+1=2 -> 写 2
结果 : [2][1][2] 表示 212 ✓
// 洛谷 P1255 数楼梯 —— 第 ③ 版:递推 + 高精度加法(正解)//// 输入:N(1 <= N <= 5000)// 输出:走法总数(N = 5000 时是一个 1045 位的数)//// 和第 ② 版的差别只有一处:**把 long long 换成「一个数组存一个大数」**。// 递推那两行的形状一模一样,还是 f[i] = f[i-1] + f[i-2]。//// 高精度加法就是竖式加法,小学怎么算的就怎么写:// · 每一位存一个十进制数字,**低位在前**(下标 0 是个位)—— 这样进位是「往后加」,不用挪数组// · 逐位相加,超过 10 就往上一位进 1//// ⚠ 低位在前是约定,不是随便挑的:加法、乘法都是从低位往高位算,// 存成「低位在前」之后下标和数位就一一对应了,最后输出时倒着打一遍即可。// (第 44 章整章讲这件事,这里只用得上最简单的加法。)
#include <bits/stdc++.h>using namespace std;
/** 大数:低位在前,每个元素是一个十进制数字 */using Big = vector<int>;
Big fromInt(int x) { Big a; if (x == 0) a.push_back(0); while (x > 0) { a.push_back(x % 10); x /= 10; } return a;}
/** 竖式加法:逐位相加,满十进一 */Big add(const Big& a, const Big& b) { Big c; int carry = 0; for (size_t i = 0; i < a.size() || i < b.size() || carry; i++) { int s = carry; if (i < a.size()) s += a[i]; if (i < b.size()) s += b[i]; c.push_back(s % 10); carry = s / 10; } return c;}
int main() { int n; if (!(cin >> n)) return 0;
vector<Big> f(max(n, 2) + 1); f[1] = fromInt(1); f[2] = fromInt(2); for (int i = 3; i <= n; i++) f[i] = add(f[i - 1], f[i - 2]);
const Big& ans = f[n]; for (size_t i = ans.size(); i-- > 0; ) cout << ans[i]; // 低位在前,倒着输出 cout << "\n"; return 0;}点「运行 ▶」看结果
这不是随手挑的:加法和乘法都是从低位往高位算的。
存成低位在前之后,「个位」永远是下标 0、「十位」永远是下标 1 —— 进位就是「往后一个下标加 1」,数组不用挪动。 存成高位在前的话,每进一次位都可能要把整个数组往后搬,写起来痛苦得多。
⇒ 代价只有一个:输出时要倒着打一遍。这笔交易非常划算。
(第 44 章整章讲高精度,这里只用得上最简单的加法。)
5两把尺子:一把量时间,一把量数值
// P1255 的三个版本各卡在哪 —— 两把可复现的尺子//// 输入:无// 输出:两张表// 表一:朴素递归的调用次数(并验证它恒等于 2 × f(n) − 1)// 表二:long long 从第几阶开始装不下,以及答案有多少位//// ★ 这份代码要说明的是两件**性质不同**的失败:// · 第 ① 版失败在**时间**上 —— 调用次数是答案的两倍,而答案本身就大得没边;// · 第 ② 版失败在**数值**上 —— 它跑得飞快,只是从某一阶开始悄悄给出错的数。// 前者你会看到 TLE,后者你只会看到 WA,而且样例还是过的。//// ⚠ 表二用 __int128 当裁判:它能装到 1.7 × 10^38,足够看清 long long 在哪一步开始跑偏。// (第 44 章 verify.cpp 用的是同一招:换一把更长的尺子去量原来那把。)
#include <bits/stdc++.h>using namespace std;
long long calls = 0;long long fib(int n) { calls++; if (n == 1) return 1; if (n == 2) return 2; return fib(n - 1) + fib(n - 2);}
/** 大数位数:只算位数,不算值 */int digitsOf(int n) { vector<int> a{1}, b{2}, c; if (n == 1) return 1; if (n == 2) return 1; for (int i = 3; i <= n; i++) { c.clear(); int carry = 0; for (size_t k = 0; k < a.size() || k < b.size() || carry; k++) { int s = carry; if (k < a.size()) s += a[k]; if (k < b.size()) s += b[k]; c.push_back(s % 10); carry = s / 10; } a = b; b = c; } return (int)b.size();}
int main() { // 先把 f 递推出来(第 ③ 列要用 f[n-1]) vector<long long> f(45); f[1] = 1; f[2] = 2; for (int i = 3; i < 45; i++) f[i] = f[i - 1] + f[i - 2];
cout << "表一:朴素递归要调用多少次\n\n"; cout << " n 答案 f(n) 调用次数 2*f(n-1)-1\n"; cout << "----- ------------- ----------- -----------\n"; for (int n : {4, 10, 20, 30, 40}) { calls = 0; long long v = fib(n); cout << setw(5) << n << setw(16) << v << setw(14) << calls << setw(14) << 2 * f[n - 1] - 1 << "\n"; } cout << "\n后两列永远相等:调用次数 = 2 * f(n-1) - 1。\n"; cout << "也就是说它和答案是同一个量级 —— 所以「答案大」就等于「跑不完」,优化循环救不了。\n"; cout << "⚠ 这个式子是跑出来才对上的:先猜的是 2*f(n)-1,一比就发现差了一整阶。\n";
cout << "\n表二:64 位整数最多能撑到第几阶\n\n"; // ⚠ 用 __int128 递推真值,再和两条上限比 —— 全程没有溢出,结论是确定的。 // (直接让 long long 溢出去看它变成什么,是**未定义行为**,不能拿来当证据。) const __int128 LL_MAX = (__int128)9223372036854775807LL; const __int128 ULL_MAX = ((__int128)1 << 64) - 1; __int128 A = 1, B = 2; // f(1), f(2) int lastLL = 2, lastULL = 2; // ⚠ 只推到第 100 阶就够了 —— 再往下推,连 __int128 自己都会溢出, // 那样这把「裁判尺」本身就不可信了。(量东西之前先确认尺子够长。) for (int i = 3; i <= 100; i++) { __int128 C = A + B; if (C <= LL_MAX) lastLL = i; if (C <= ULL_MAX) lastULL = i; A = B; B = C; } cout << "64 位有符号 (long long) 最多到第 " << lastLL << " 阶\n"; cout << "64 位无符号 (unsigned) 最多到第 " << lastULL << " 阶\n"; cout << "题目要的 第 5000 阶,答案有 " << digitsOf(5000) << " 位\n"; cout << "\n⇒ 差的不是「大一点」,是差着 " << digitsOf(5000) / 19 << " 个 long long 那么长。\n"; return 0;}点「运行 ▶」看结果
表一:朴素递归要调用多少次
n 答案 f(n) 调用次数 2*f(n-1)-1
----- ------------- ----------- -----------
4 5 5 5
10 89 109 109
20 10946 13529 13529
30 1346269 1664079 1664079
40 165580141 204668309 204668309
表二:64 位整数最多能撑到第几阶
64 位有符号 (long long) 最多到第 91 阶
64 位无符号 (unsigned) 最多到第 92 阶
题目要的 第 5000 阶,答案有 1045 位
第 ① 版 失败在时间上 -> 你会看到 TLE,一眼就知道出事了
第 ② 版 失败在数值上 -> 你只会看到 WA,而且样例还是过的第二种危险得多,因为它不给你任何提示。
⇒ 对付它只有一招:动手前先估一估答案的位数。 斐波那契每加一阶大约多 0.209 位,5000 阶就是一千多位 —— 这个估算三秒钟, 比事后调一小时值钱得多。
★ 用 __int128 当裁判去量 long long 够不够长,是这本书反复用的一招
(第 44 章的 verify.cpp 同款):换一把更长的尺子,去量原来那把。
⚠ 而且量之前要先确认这把裁判尺自己够长:__int128 最多约 38 位,
所以那段代码只推到第 100 阶就停了 —— 再往下推,裁判自己也会溢出,
那时候比出来的结论是假的。尺子不够长的时候,读数是没有意义的。
6回头看:这道题为什么在题单里出现两次
- 第 1 章列它,是为了让你亲手撞一次「同一个子问题被算了无数遍」—— 那种「明明式子这么简单,怎么就跑不完」的感觉,只有自己撞过才记得住。
- 第 2 章列它,是因为分解思维走完之后,剩下的就是数值本身太大这个新问题。
⇒ 于是这道题正好把两件事分开摆给你看:递归的形状对不对,和数值装不装得下 —— 它们互相独立,各自会用完全不同的方式咬你。
★ 顺带一提:第 ① 版那个「指数级递归」的正规解法是记忆化 (见 P1028 第 ③ 步),但这道题连记忆化都不用 —— 直接递推更省事。 选最省事的那条路,别为了用上刚学的技巧硬套。