填充书架 图解题解
这道题到底在问什么
- 输入
- books=[[1,1],[2,3],[2,3],[1,1],[1,1],[1,1],[1,2]], shelfWidth=4
- 输出
- 6
- 输入
- 本节演示 books=[[1,3],[2,1],[2,3],[1,4],[2,2]], shelfWidth=4
- 输出
- 9
先想最直接的笨办法
套路就一句:枚举「最后一层装哪几本连续的书」。最后一层选定后,前面那些书就是规模更小的同一个问题 f[j-1],加上这一层的层高即可。下面把 f 表一格一格填出来。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1105 填充书架:书按顺序上架、每层厚度和不超 shelfWidth、层高取该层最高的书,求总高最小。一维 DP,f[i]=前 i 本最小总高,枚举最后一层从第几本起来转移,时间 O(n²)、空间 O(n)。
书按顺序上架,怎么分层才能让书架最矮
给一排书 books,每本是 [厚度、高度],书架宽度 shelfWidth。书按给定顺序摆、不能打乱;同层厚度之和不超过 shelfWidth,放不下就另起一层。层高是这层最高那本的高度,总高是各层相加,求最小总高。演示 books=[[1,3],[2,1],[2,3],[1,4],[2,2]]、shelfWidth=4,答案 9。
为什么不能把每种分层方式都试一遍
每两本相邻的书之间都能选「断开」或「连一层」,n 本就有 2ⁿ⁻¹ 种分法,逐一算指数级炸开,且大量分法共享前缀被反复重算。既然「前 i 本怎么摆最矮」能独立回答,算一次存下来复用,就是动态规划(把『前 i 本书怎么摆最矮』算一次存下、后面直接取)。
状态定义:f[i] 存什么,f[0] 为什么是 0
定义 f[i] 为「前 i 本摆完能做到的最小总高」(把答案记进第 i 格,要用直接取)。下标 i 数的是书的本数、不是位置。f[0]=0:一本没摆、高度为 0,给后面递推(拿前面算好的推出后面的)当地基。答案是 f[n]。
转移(从前面算好的格子推出这一格):枚举最后一层从哪本起
算 f[i] 只盯最后一层装哪几本连续的书。设它从第 j 本到第 i 本,前 j-1 本取最优 f[j-1],这层层高是第 j 到 i 本里最高那本,于是 f[i] = f[j-1] + 这层最大高度。先给保底 f[i] = f[i-1] + 它的高度。
再让最后一层从第 i 本往左扩,累加新并那本的厚度。一旦超过 shelfWidth 就直接停(厚度只增不减、再往左更宽,一超即停)。没超就用 max 把层高更新成段内最高的一本,拿 f[j-1] + 层高刷新 f[i] 取更小。
拿这 5 本书把 f 表从左填到右
厚度 1、2、2、1、2,高度 3、1、3、4、2,shelfWidth=4,f[0]=0。
f[1]=f[0]+3=3。f[2]:保底 4;并第 1、2 本厚 3、层高 3,得 f[0]+3=3,f[2]=3。f[3]:保底 6;并第 2、3 本厚 4、层高 3 得 6 不更小,再并第 1 本厚 5 超宽停,f[3]=6。
f[4]:保底 10;并第 3、4 本厚 3、层高 max(4,3)=4,得 f[2]+4=7,f[4]=7。f[5]:保底 9;并第 4、5 本厚 3、层高 max(2,4)=4 得 10 不更小,f[5]=9 即答案,分层 [书1,书2]、[书3,书4]、[书5]。
漏了取 max、层高用成刚并进那本,总高为什么会算矮
外层 i 走 n 本、内层最坏从第 i 本往左扫到第 1 本,每格 O(n),合起来时间 O(n²)(大 O 记号,描述规模变大时操作数大概怎么涨);书厚时内层很快 break。空间只额外开长度 n+1 的数组 f,是 O(n)。
边界:全部塞一层时层高是最高那本;只有一本时答案是它的高度;厚度恰好等于 shelfWidth 也算放得下,判断用「超过才停」而非「大于等于就停」;超宽别写成「跳过往左」,该 break 不 break 会算进非法分层。
漏了 max、用刚并那本的高度,就把某层算矮。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3套路就一句:枚举「最后一层装哪几本连续的书」。最后一层选定后,前面那些书就是规模更小的同一个问题 f[j-1],加上这一层的层高即可。下面把 f 表一格一格填出来。
- 4先把数据摆出来。上面 5 个格子是 5 本书的高度,分别是 3、1、3、4、2;它们的厚度写在下面那行,依次是 1、2、2、1、2;书架宽度是 4。记住每本书是 [厚度, 高度] 一对值,厚度决定能不能挤进同一层,高度决定那层多高。
- 5搭一张表,只有一行,列从左到右是 f[0] 到 f[5]。最左边 f[0] 先填 0:一本书都没摆,高度自然是 0。它是后面所有递推的地基。我们从 f[1] 往右一格一格填。
- 6算 f[1]。先给一个保底方案:让第 1 本书(厚 1、高 3)自己单独占最后一层。那总高就是「前 0 本的最优」f[0]=0 再加上它自己的高 3,等于 3。先把这个值记进 f[1],下面看能不能把它跟前面的书拼一层、拼出更矮的。
- 7所有放法都比过了,f[1] 最终是 3,意思是把前 1 本书摆完,书架最矮能做到 3。这格定死,继续往右算下一格。
- 8算 f[2]。先给一个保底方案:让第 2 本书(厚 2、高 1)自己单独占最后一层。那总高就是「前 1 本的最优」f[1]=3 再加上它自己的高 1,等于 4。先把这个值记进 f[2],下面看能不能把它跟前面的书拼一层、拼出更矮的。
- 9试着把第 1 到第 2 本书放在同一层。它们厚度之和是 3,没超过 4,能放;这一层的高度取这几本里最高的,是 3。那么总高 = 前 0 本的最优 f[0]=0 加上这层高 3,等于 3。它比刚才记的还小,把 f[2] 刷新成 3。
- 10所有放法都比过了,f[2] 最终是 3,意思是把前 2 本书摆完,书架最矮能做到 3。这格定死,继续往右算下一格。
- 11算 f[3]。先给一个保底方案:让第 3 本书(厚 2、高 3)自己单独占最后一层。那总高就是「前 2 本的最优」f[2]=3 再加上它自己的高 3,等于 6。先把这个值记进 f[3],下面看能不能把它跟前面的书拼一层、拼出更矮的。
- 12试着把第 2 到第 3 本书放在同一层。它们厚度之和是 4,没超过 4,能放;这一层的高度取这几本里最高的,是 3。那么总高 = 前 1 本的最优 f[1]=3 加上这层高 3,等于 6。它没有更小,f[3] 还是 6,不动。
- 13想把更前面的第 1 本书也并进这最后一层,可这层厚度加起来变成 5,超过了书架宽度 4,这本书挤不进来。再往左只会更宽,所以不用看了,f[3] 的扩展到此为止。
- 14所有放法都比过了,f[3] 最终是 6,意思是把前 3 本书摆完,书架最矮能做到 6。这格定死,继续往右算下一格。
- 15算 f[4]。先给一个保底方案:让第 4 本书(厚 1、高 4)自己单独占最后一层。那总高就是「前 3 本的最优」f[3]=6 再加上它自己的高 4,等于 10。先把这个值记进 f[4],下面看能不能把它跟前面的书拼一层、拼出更矮的。
- 16试着把第 3 到第 4 本书放在同一层。它们厚度之和是 3,没超过 4,能放;这一层的高度取这几本里最高的,是 4。那么总高 = 前 2 本的最优 f[2]=3 加上这层高 4,等于 7。它比刚才记的还小,把 f[4] 刷新成 7。
- 17想把更前面的第 2 本书也并进这最后一层,可这层厚度加起来变成 5,超过了书架宽度 4,这本书挤不进来。再往左只会更宽,所以不用看了,f[4] 的扩展到此为止。
- 18所有放法都比过了,f[4] 最终是 7,意思是把前 4 本书摆完,书架最矮能做到 7。这格定死,继续往右算下一格。
- 19算 f[5]。先给一个保底方案:让第 5 本书(厚 2、高 2)自己单独占最后一层。那总高就是「前 4 本的最优」f[4]=7 再加上它自己的高 2,等于 9。先把这个值记进 f[5],下面看能不能把它跟前面的书拼一层、拼出更矮的。
- 20试着把第 4 到第 5 本书放在同一层。它们厚度之和是 3,没超过 4,能放;这一层的高度取这几本里最高的,是 4。那么总高 = 前 3 本的最优 f[3]=6 加上这层高 4,等于 10。它没有更小,f[5] 还是 9,不动。
- 21想把更前面的第 3 本书也并进这最后一层,可这层厚度加起来变成 5,超过了书架宽度 4,这本书挤不进来。再往左只会更宽,所以不用看了,f[5] 的扩展到此为止。
- 22所有放法都比过了,f[5] 最终是 9,意思是把前 5 本书摆完,书架最矮能做到 9。这格定死,表已经填完,下一步读出答案。
- 23整张表填完了。最右边 f[5] 就是把 5 本书全部摆完的最小总高度,等于 9,这正是题目要的答案。回头看一眼这张表是怎么一步步长出来的:每一格都靠它左边某一格加上最后一层的层高得来。
- 24顺着每格当初选的方案倒推一遍分层:f[5]=9 是让书5 单独成一层、接在 f[4] 后面;f[4]=7 是书3、书4 同一层(高 4)、接在 f[2] 后面;f[2]=3 是书1、书2 同一层(高 3)、接在 f[0] 后面。于是三层分别是 [书1,书2]、[书3,书4]、[书5]。
- 25把最优分层标回原始的书上:绿色的书1、书2 是第一层,厚度 1+2=3 没超 4,层高取两本里高的 3;蓝色的书3、书4 是第二层,厚 2+1=3,层高 4;红色的书5 自己一层,层高 2。三层加起来 3+4+2=9,和 DP 表算出的答案严丝合缝。
⚠️ 容易写错的地方
✗ 错:以为每本书各占一层最矮
✓ 对:把矮的书挤进同一层往往更省,但受宽度上限约束,要 DP 权衡
同层只按最高那本计高,几本矮书拼一层可以省下重复的高度
✗ 错:层高算成这一层厚度和或高度和
✓ 对:层高 = 这一层里最高那本书的高度
书是并排摆的,层高由最高的书撑起,跟厚度无关、也不是各书高度相加
✗ 错:内层并书忘了在超宽时 break
✓ 对:累计厚度一旦 > shelfWidth 立即停止往左扩
再往左只会更宽、永远放不下,继续算会用上放不下的非法分层
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minHeightShelves(self, books: List[List[int]], shelfWidth: int) -> int:
n = len(books)
f = [0] * (n + 1)
for i, (w, h) in enumerate(books, 1):
f[i] = f[i - 1] + h
for j in range(i - 1, 0, -1):
w += books[j - 1][0]
if w > shelfWidth:
break
h = max(h, books[j - 1][1])
f[i] = min(f[i], f[j - 1] + h)
return f[n]C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minHeightShelves(vector<vector<int>>& books, int shelfWidth) {
int n = books.size();
int f[n + 1];
f[0] = 0;
for (int i = 1; i <= n; ++i) {
int w = books[i - 1][0], h = books[i - 1][1];
f[i] = f[i - 1] + h;
for (int j = i - 1; j > 0; --j) {
w += books[j - 1][0];
if (w > shelfWidth) {
break;
}
h = max(h, books[j - 1][1]);
f[i] = min(f[i], f[j - 1] + h);
}
}
return f[n];
}
};Java
import java.util.*;
class Solution {
public int minHeightShelves(int[][] books, int shelfWidth) {
int n = books.length;
int[] f = new int[n + 1];
for (int i = 1; i <= n; ++i) {
int w = books[i - 1][0], h = books[i - 1][1];
f[i] = f[i - 1] + h;
for (int j = i - 1; j > 0; --j) {
w += books[j - 1][0];
if (w > shelfWidth) {
break;
}
h = Math.max(h, books[j - 1][1]);
f[i] = Math.min(f[i], f[j - 1] + h);
}
}
return f[n];
}
}复杂度
时间
O(n²)
n 是书的本数。外层 n 个 f[i],内层最坏从 i 一直往左扫到 1(书都很薄、宽度放得下),每个 f[i] 花 O(n),合起来 O(n²);书厚时内层很快超宽 break,实际更快
空间
O(n)
只用一个长度 n+1 的一维数组 f,峰值就是这条数组;没有递归栈,与本数线性相关
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 填充书架 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不能贪心地让每层尽量塞满再另起一层?+
贪心「每层塞到不能再塞才换层」看着省层数,却可能把一本很高的书硬塞进本可以矮的一层,把整层拔高。某本高书是留在当前层、还是让它另起一层更划算,取决于它前后的书怎么分,单看眼前能不能塞下决定不了。所以要枚举最后一层从第几本开始,让 DP 比遍所有合法的分界,才不会被「先塞满」带偏。
f[i] 里的 i 到底数什么,数组为什么要开成 n+1 长?+
i 数的是「前几本书」,不是数组下标位置,f[i] 表示前 i 本摆完的最小总高。多开一格是为了让 f[0] 表示「一本都没摆、高度 0」,它给最后一层从第 1 本开始的情况当基准:转移里 f[j-1] 当 j 取到 1 时正好落在 f[0]。少开这一格,第一层的转移就没有地基可接。
内层为什么可以一超宽就 break,而不是跳过这本继续往左?+
最后一层是「一段连续的书」,从第 i 本往左扩时厚度只会累加、只增不减。当前这本已经让这层厚度超过 shelfWidth,再往左并入更多书只会更宽,不可能再变合法。所以一超就能干脆停,继续往左试是白费功夫。这也是复杂度里「内层最坏 O(n)、但书厚时实际快得多」的由来。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 填充书架 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。