0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1096,日期见页头。两边不一致时信原站。
(原题带一张 n = 3 的示意图,这里只能转文字;图在原站上。)
题目描述
给定 A、B、C 三根足够长的细柱,在 A 柱上放有 2n 个中间有孔的圆盘,共有 n 个不同的尺寸, 每个尺寸都有两个相同的圆盘,注意这两个圆盘是不加区分的。
现要将这些圆盘移到 C 柱上,在移动过程中可放在 B 柱上暂存。要求:
- 每次只能移动一个圆盘;
- A、B、C 三根细柱上的圆盘都要保持上小下大的顺序。
任务:设 A(n) 为 2n 个圆盘完成上述任务所需的最少移动次数,对于输入的 n,输出 A(n)。
输入格式:一个正整数 n,表示在 A 柱上放有 2n 个圆盘。
输出格式:一个正整数,为完成上述任务所需的最少移动次数 A(n)。
数据范围
- 对于 50% 的数据,
1 ≤ n ≤ 25 - 对于 100% 的数据,
1 ≤ n ≤ 200
提示:设法建立 A(n) 与 A(n-1) 的递推关系式。
1先看清楚:这道题有三关
第一关:公式对吗 「2n 个盘」不等于「2n 层汉诺塔」—— 同尺寸那两个是不加区分的
第二关:装得下吗 n = 200 时答案有 61 位,long long 最多 19 位
第三关:怎么算 只用得上「乘 2」和「减 2」,都是小学竖式
★ 第一关样例就能挡住,第二关样例挡不住 —— 这个对比本身就是这道题最值钱的地方。
输入
2
输出
6
n = 2(也就是 4 个盘,两种尺寸各两个)的答案是 6。
另一组样例是 n = 1 时答案 2。上面那段输出是仓库里的 p1096.cpp 真跑出来的。
2第 ① 版:直接套第 2 章的汉诺塔公式(这一版是错的)
刚学完第 2 章的汉诺塔,看到「2n 个圆盘」,手比脑子快:
ans = 2^(2n) - 1; // 普通汉诺塔:n 个盘要 2^n - 1 步
// 洛谷 P1096 Hanoi 双塔问题 —— 第 ① 版:把 2n 个盘当成普通汉诺塔(**这一版是错的**)//// 输入:n(A 柱上有 2n 个盘子,共 n 种尺寸,每种两个)// 输出:本该是最少移动次数//// 这份为什么存在:刚学完第 2 章的汉诺塔,看到「2n 个圆盘」,// 手比脑子快 —— 直接套公式 2^(盘数) − 1 = 2^(2n) − 1。//// ⚠ 错在哪:**这两个同尺寸的圆盘是不加区分的**(题面第一句就写了)。// 普通汉诺塔要求「上小下大」且每个盘互不相同;这里同尺寸的两个盘叠在一起,// 搬的时候**永远是一对一对地动**,根本没有「先搬哪一个」的选择。// ⇒ 把它们当成两个不同的盘,等于凭空多算了一大堆根本不存在的步骤。//// ★ 好消息是:**样例一跑就露馅** —— n = 1 时它输出 3,而答案是 2。// 这一版真正的教训不是公式记错了,是:**样例是免费的第一道防线,交之前一定跑一遍。**
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0;
// 2^(2n) − 1:把 2n 个盘当成互不相同的普通汉诺塔 unsigned long long ans = 1; // unsigned:让溢出也是可复现的(见 p1096LL.cpp) for (int i = 0; i < 2 * n; i++) ans *= 2; cout << ans - 1 << "\n"; return 0;}点「运行 ▶」看结果
每个尺寸都有两个相同的圆盘,注意这两个圆盘是不加区分的。
普通汉诺塔要求「上小下大」且每个盘互不相同;而这里同尺寸的两个盘叠在一起, 搬的时候永远是一对一对地动 —— 根本不存在「先搬这一对里的哪一个」这种选择。
把它们当成两个不同的盘,等于凭空多算了一大堆根本不存在的方案。
★ 但这一版真正的教训不是公式记错了,是:
样例是免费的第一道防线,交之前一定跑一遍。
n = 1 一秒钟就能打死它 —— 而很多人是提交之后才知道。
3第 ② 版:把公式推对(但 long long 装不下)
题面的提示已经把路指出来了:建立 A(n) 与 A(n-1) 的递推关系。
把 2n 个盘看成 n 对,每一对永远一起动。于是它就是一个 n 层的普通汉诺塔, 只不过「搬一层」现在要花 2 步:
A(n) = 2 * A(n-1) + 2 A(1) = 2
~~~~~~~~~~~ ~~~
上面 n-1 对 最下面那一对
搬过去再搬回来 搬 2 步
展开:A(n) = 2 * (2^n - 1) = 2^(n+1) - 2
核对:n = 1 -> 2^2 - 2 = 2 ✓
n = 2 -> 2^3 - 2 = 6 ✓
// 洛谷 P1096 Hanoi 双塔问题 —— 第 ② 版:公式推对了,但 long long 装不下//// 输入:n(1 <= n <= 200)// 输出:最少移动次数//// 先把公式推对(题面的提示就是「设法建立 A(n) 与 A(n-1) 的递推关系式」)://// 把 2n 个盘看成 **n 对**,每一对永远一起动。于是它就是一个 n 层的普通汉诺塔,// 只不过「搬一层」现在要花 2 步(那一对的两个盘各搬一次)。//// A(n) = 2 * A(n-1) + 2 A(1) = 2//// 展开就是 A(n) = 2 * (2^n − 1) = 2^(n+1) − 2//// 核对样例:n = 1 -> 2^2 − 2 = 2 ✓ n = 2 -> 2^3 − 2 = 6 ✓//// ⚠ 公式全对,样例全过,交上去照样 WA —— 因为 n 最大是 **200**:// 64 位有符号 (long long) 最多撑到 n = 62(2^63 − 2,离上限只差 1)// 64 位无符号 (unsigned) 最多撑到 n = 63// 题目要的 n = 200,答案是一个 **61 位**的数// ⚠ 「刚好还塞得进」这种边界最阴 —— 你随手测的 n 很可能全在 62 以内,一路绿灯。// ⇒ 和 P1255 数楼梯是同一种失败:**跑得动,但装不下。**//// ⚠ 一处刻意的写法:这里用的是 **unsigned long long**。真实的错误是用 long long 写的,// 但**有符号溢出在 C++ 里是未定义行为**,换个编译器给的数可能就不一样 ——// 那样这一页里「它输出了什么」就没法当成一个可复现的事实来讲。// unsigned 的溢出对 2^64 取模,是有定义的。(第 44 章立的规矩:演示错误也要让错可复现。)
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0;
unsigned long long ans = 1; // ⚠ 用 unsigned 见文件头最后一段 for (int i = 0; i <= n; i++) ans *= 2; // 2^(n+1) cout << ans - 2 << "\n"; return 0;}点「运行 ▶」看结果
64 位有符号 (long long) 最多撑到 n = 62 (2^63 - 2,离上限只差 1)
64 位无符号 (unsigned) 最多撑到 n = 63
题目要的 n = 200,答案有 61 位⚠ 这份演示代码用的是 unsigned long long:有符号溢出是未定义行为,
让它自己撞一次得到的数换台机器可能就不一样。无符号溢出有定义(对 2^64 取模),
这样「它错成什么样」才是可复现的 —— 结论完全一样:装不下。
⇒ 你随手测的 n 很可能全在 62 以内,一路绿灯。
★ 和 P1255 数楼梯 是同一种失败:跑得动,但装不下。
而这两道题的共同解法也一样:动手前先估一估答案的位数。
2^201 大概是 60 位 —— 这个估算三秒钟。
4第 ③ 版:公式 + 高精度(正解)
和第 ② 版的差别只有一处:把 long long 换成「一个数组存一个大数」。
这道题只需要两种运算,都是小学竖式:
乘 2(做 n+1 次,就得到 2^(n+1))
逐位乘 2,满十进一
减 2(最后做一次)
从个位减,不够就向上一位借
// 洛谷 P1096 Hanoi 双塔问题 —— 第 ③ 版:公式 + 高精度(正解)//// 输入:n(1 <= n <= 200)// 输出:最少移动次数 A(n) = 2^(n+1) − 2(n = 200 时是一个 61 位的数)//// 和第 ② 版的差别只有一处:**把 long long 换成「一个数组存一个大数」**。// 要做的运算只有两种,都是小学竖式:// · 乘 2:逐位乘 2,满十进一(做 n+1 次,就得到 2^(n+1))// · 减 2:从个位减,不够就向上一位借//// ⚠ 减 2 之后可能出现**前导零**(比如 100 − 2 = 098),必须把高位的 0 去掉再输出,// 否则会打出 "098" 这种东西。这道题 n >= 1 时答案至少是 2,不会整个变成 0,// 但去前导零的循环要留一位保底 —— 养成这个习惯,换道题就用得上。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0;
vector<int> a{1}; // 低位在前;现在表示 1
for (int t = 0; t <= n; t++) { // 乘 2,共 n+1 次 => 2^(n+1) int carry = 0; for (size_t i = 0; i < a.size(); i++) { int s = a[i] * 2 + carry; a[i] = s % 10; carry = s / 10; } while (carry) { a.push_back(carry % 10); carry /= 10; } }
a[0] -= 2; // 减 2,从个位开始借位 for (size_t i = 0; a[i] < 0; i++) { a[i] += 10; a[i + 1] -= 1; }
while (a.size() > 1 && a.back() == 0) a.pop_back(); // 去前导零,留一位保底
for (size_t i = a.size(); i-- > 0; ) cout << a[i]; cout << "\n"; return 0;}点「运行 ▶」看结果
100 − 2 = 098 —— 如果不把高位那个 0 去掉,打出来就是 098。
while (a.size() > 1 && a.back() == 0) a.pop_back(); // 留一位保底⚠ 那个 a.size() > 1 不能省:否则答案真是 0 的时候,整个数组会被删空,什么都打不出来。
这道题 n ≥ 1 时答案至少是 2,碰不到 —— 但换道题就会碰到,而那时你已经忘了这回事。
★ 这类「边界只在别的题上出现」的坑,最好的办法是每次都写全, 不要每次都重新判断「这道题需不需要」。
5回头看:三道高精度题串在一起
P1255 数楼梯 f(n) = f(n-1) + f(n-2) 需要:高精度加法
P1096 双塔 A(n) = 2*A(n-1) + 2 需要:高精度乘 2、减 2两道题的递推关系都是一眼看穿的,难点都不在「想出来」,而在「装得下」。
⇒ 于是它们一起教了同一件事: 做题的时候,「答案能有多大」和「怎么算出答案」是两个必须分开问的问题。 第二个想明白了,第一个还可能把你打死。
★ 第 44 章整章讲高精度。但你现在已经会最要紧的那两个运算了 —— 加法和乘一个小数,覆盖了普及组绝大多数高精度题。