剑指 Offer 动态规划系列:矩形覆盖——用 n 个 2×1 小矩形铺满 2×n 大矩形的递推推导与 O(1) 空间解法(CS-Notes)
本篇基于 CS-Notes 剑指 Offer 题解中的 10.2 矩形覆盖 展开,讲解如何用 n 个 2×1 的小矩形无重叠地覆盖一个 2×n 的大矩形、总共有多少种方法。读完本文,你将掌握"枚举最后一步放法"这一建模套路、递推公式的严格推导过程,以及从朴素递归到滚动变量、时间 O(n) 空间 O(1) 的完整优化路径,并能把它与斐波那契数列、跳台阶等同族问题融会贯通。
题目描述
我们可以用 2×1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2×1 的小矩形无重叠地覆盖一个 2×n 的大矩形,总共有多少种方法?
即:大矩形的尺寸固定为 2 行 n 列,恰好要放入 n 个 2×1 的"骨牌"(每个骨牌可横放占 1×2,也可竖放占 2×1),要求恰好铺满且不重叠,问铺法总数。
从特例入手:确定递推的边界条件
做铺放类计数题,最稳妥的办法是先手写小规模的解,再找规律。
当 n = 1 时,2×1 的大矩形只能用 1 个小矩形竖着铺,只有 1 种方法:f(1) = 1。
当 n = 2 时,有两种覆盖方法:要么两个小矩形都竖着放,要么都横着放:f(2) = 2。
上图为 n = 2 时的全部两种铺法。这两个值就是后续递推的初始条件。
递推公式的推导:只关心最左边一列的放法
要覆盖 2×n 的大矩形,可以只看最左边 2 列区域的铺法,它只有两种可能:
- 在左上角竖着放一个 2×1 小矩形:它独自占据第 1 列,剩下的部分是一个 2×(n−1) 的矩形,铺法数为 f(n−1);
- 在左上角横着放一个 2×1 小矩形:它占据第 1、2 列的上半格,此时为了铺满第 1、2 列的下半格,左上角下方必须再横着放一个 2×1 小矩形(这是唯一可行的补法)。也就是说"左上角横放"必然成对出现,它独占前 2 列,剩下的部分是一个 2×(n−2) 的矩形,铺法数为 f(n−2)。
两种情况互斥且穷尽了左上角的所有放法,因此得到原 10.2 矩形覆盖 中给出的递推公式:
即:
- f(1) = 1
- f(2) = 2
- f(n) = f(n−1) + f(n−2),n > 2
按此递推,前几项为 f(1)=1、f(2)=2、f(3)=3、f(4)=5、f(5)=8……可以看出 f(n) 恰好是斐波那契数列整体右移一位,即 f(n) = Fib(n+1)(若约定 Fib(1)=1、Fib(2)=1)。这与仓库中 10.1 斐波那契数列 是同一族递推,只是初始条件不同。
解法一:朴素递归(反面教材)
直接按递推式写递归:
public int rectCover(int n) {
if (n <= 1)
return 1;
if (n == 2)
return 2;
return rectCover(n - 1) + rectCover(n - 2);
}
问题在于递归会重复求解大量子问题:rectCover(n−1) 与 rectCover(n−2) 都会各自调用 rectCover(n−2),以此类推,时间复杂度高达指数级的 O(2^n),n 稍大就会超时。这也正是 10.1 斐波那契数列 一文中强调的"递归重复计算子问题"的缺陷,动态规划的核心就是缓存子问题的解来消除这种重复。
解法二:自底向上动态规划,O(N) 空间
用数组把子问题的解缓存起来,自底向上填表即可:
public int rectCover(int n) {
if (n <= 2)
return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
时间复杂度 O(n),空间复杂度 O(n)。这与 10.1 斐波那契数列 中的 DP 数组版本结构完全一致。
解法三:滚动变量优化,O(1) 空间(仓库官方解法)
考虑到计算第 i 项只依赖第 i−1 项和第 i−2 项,无需保留整张 dp 表,用两个变量滚动更新前两项的值即可,从而把空间复杂度由 O(n) 降为 O(1)。这正是 10.2 矩形覆盖 给出的官方解法:
public int rectCover(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 3; i <= n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
逐行解析:
| 代码 | 作用 |
|---|---|
if (n <= 2) return n; |
处理边界:f(1)=1、f(2)=2,直接返回 |
int pre2 = 1, pre1 = 2; |
初始化前两项 f(1)=1、f(2)=2 |
result = pre2 + pre1; |
计算 f(i) = f(i−2) + f(i−1) |
pre2 = pre1; pre1 = result; |
窗口整体右移一位,为下一轮做准备 |
时间复杂度 O(n),空间复杂度 O(1)。可以验证:n=3 时循环执行一次,result = 1 + 2 = 3,与手算 f(3)=3 一致;n=4 时 result = 2 + 3 = 5,与 f(4)=5 一致。
同类问题对照:一个递推,三种题型
仓库把矩形覆盖归入剑指 Offer 的"动态规划"分类(见 剑指 Offer 题解 - 目录 中"动态规划"小节)。围绕同一个递推框架 f(n) = f(n−1) + f(n−2),本章还有两道变体值得对照学习:
- 10.1 斐波那契数列:直接求 Fib(n),初始条件为 f(0)=0、f(1)=1;同样的"数组版 → 滚动变量版"两级优化在这里也有完整演示;
- 10.3 跳台阶:青蛙一次跳 1 级或 2 级,其递推式与矩形覆盖完全相同(f(n) = f(n−1) + f(n−2),f(1)=1、f(2)=2),本质等价:把"最后一级台阶的跳法"对应到"最左边一列的铺法"即可;
- 10.4 变态跳台阶:青蛙可以跳 1~n 级,递推退化为 f(n) = 2·f(n−1) 的等比数列,是本章对"枚举最后一步"思路的进一步抽象。
可以这样记忆:矩形覆盖和跳台阶是同一递推的两个物理模型;面试时先写清边界条件 f(1)、f(2),再说明"最后一步只有两种选择",即可快速推出递推式。
常见追问与扩展
追问 1:n 很大时会不会溢出? 解法三返回 int,而 f(n) 按斐波那契速度增长,当 n 较大时会超出 int 范围。若题目给定 n 的上限,先估算 Fib(n+1) 的量级,必要时改用 long 或取模后的答案(例如输出对 10^9+7 取模的结果),以具体题目约束为准。
追问 2:如果把小矩形换成 3×1,还有多少种铺法? 这是同一思路的自然推广。此时最左边一列的放法有三种:竖着放 1 个 3×1;或横着放 2 个(上下各一,独占前两列,且此时前两列必须都横放,故成组计数);逐一枚举可得递推 f(n) = f(n−1) + 2·f(n−2)(f(1)=1、f(2)=3)。解法仍是填表 + 滚动变量,只是系数从 1 变成 2,建模方法完全一致。
追问 3:能不能直接给出闭式解? 由于 f(n) = Fib(n+1),理论上可以套用斐波那契通项公式,但面试场景下递推 + 滚动变量的 O(n)/O(1) 解法既简单又稳健,是最标准的答案。
小结
矩形覆盖题的解题路径可以归纳为三步:
- 枚举最后一步:只看最左边一列,铺法被穷尽为"竖放 1 块"和"横放 2 块"两种,得到 f(n) = f(n−1) + f(n−2);
- 定边界:f(1) = 1、f(2) = 2;
- 选实现:朴素递归(指数级,仅作反面教材)→ DP 数组(O(n) 空间)→ 滚动变量(O(1) 空间,推荐)。
掌握这一范式后,10.1 斐波那契数列、10.3 跳台阶、10.4 变态跳台阶 乃至更多"枚举最后一步"的计数题都能按同一套路快速求解。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

