填充书架 图解题解
这道题到底在问什么
- 输入
- 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 步)
最优解:一步一步想明白
- 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,不能贪心地让每一层尽量塞满?+
因为当前层塞满,可能逼着一本很高的书混进来、把这一层撑高;把它留到下一层,整体反而更矮。当前层局部塞满不等于全局最矮,必须枚举「最后一层从哪本书开始」并复用前面子问题的最优解,这正是最优子结构加重叠子问题,标准 DP。
时间复杂度为什么是 O(n²)?+
外层有 n 个 f[i] 要算,内层对每个 f[i] 最坏要从第 i 本一直往左并到第 1 本(书都很薄、宽度放得下),也就是 O(n);两层相乘是 O(n²)。如果书普遍偏厚,内层很快就超宽 break,实际跑得比这快。
f 数组为什么要开 n+1 长、并且 f[0]=0?+
f[0] 代表「一本书都没放」的高度 0,是递推的起点。转移里 f[i] 会用到 f[j-1],当最后一层从第 1 本书开始时 j=1,要取 f[0],所以留这个哨兵位最自然,边界也不用特判。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 填充书架 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。