题单 · 习题解析

洛谷 P1096 Hanoi 双塔问题

第一关样例就能挡住(公式套错),第二关样例挡不住(n=200 的答案有 61 位)

原题:洛谷 P1096出自 第 2 章 递归的分解思维:汉诺塔与斐波那契 的题单题面本地存档:2026-08-25
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1096,日期见页头。两边不一致时信原站。 (原题带一张 n = 3 的示意图,这里只能转文字;图在原站上。)

题目描述

给定 A、B、C 三根足够长的细柱,在 A 柱上放有 2n 个中间有孔的圆盘,共有 n 个不同的尺寸, 每个尺寸都有两个相同的圆盘,注意这两个圆盘是不加区分的

现要将这些圆盘移到 C 柱上,在移动过程中可放在 B 柱上暂存。要求:

  1. 每次只能移动一个圆盘;
  2. 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 步
p1096Wrong.cpp第 ① 版(错的)
输入 1。它给 3,而样例说答案是 2。一跑就露馅。
// 洛谷 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 错在题面的第一句话

每个尺寸都有两个相同的圆盘,注意这两个圆盘是不加区分的

普通汉诺塔要求「上小下大」且每个盘互不相同;而这里同尺寸的两个盘叠在一起, 搬的时候永远是一对一对地动 —— 根本不存在「先搬这一对里的哪一个」这种选择。

把它们当成两个不同的盘,等于凭空多算了一大堆根本不存在的方案

★ 但这一版真正的教训不是公式记错了,是:

样例是免费的第一道防线,交之前一定跑一遍。 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  ✓
p1096LL.cpp第 ② 版(公式对,但溢出)
先输入 1、2 核对样例(都对)。再输入 63 —— 还对(无符号刚好撑到这儿)。然后输入 64 和 200。
// 洛谷 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠⚠ 「刚好还塞得进」是最阴的一种边界
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.cpp第 ③ 版(正解)
输入 200,看那个 61 位的数。再输入 1、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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 减法之后一定要去前导零

100 − 2 = 098 —— 如果不把高位那个 0 去掉,打出来就是 098

while (a.size() > 1 && a.back() == 0) a.pop_back();   // 留一位保底

⚠ 那个 a.size() > 1 不能省:否则答案真是 0 的时候,整个数组会被删空,什么都打不出来。 这道题 n ≥ 1 时答案至少是 2,碰不到 —— 但换道题就会碰到,而那时你已经忘了这回事。

★ 这类「边界只在别的题上出现」的坑,最好的办法是每次都写全, 不要每次都重新判断「这道题需不需要」。

5回头看:三道高精度题串在一起

✓ 这道题和 P1255 是一对
P1255 数楼梯    f(n) = f(n-1) + f(n-2)     需要:高精度加法
P1096 双塔      A(n) = 2*A(n-1) + 2        需要:高精度乘 2、减 2

两道题的递推关系都是一眼看穿的,难点都不在「想出来」,而在「装得下」。

⇒ 于是它们一起教了同一件事: 做题的时候,「答案能有多大」和「怎么算出答案」是两个必须分开问的问题。 第二个想明白了,第一个还可能把你打死。

★ 第 44 章整章讲高精度。但你现在已经会最要紧的那两个运算了 —— 加法和乘一个小数,覆盖了普及组绝大多数高精度题。