K 次调整数组大小浪费的最小总空间 图解题解
这道题到底在问什么
- 输入
- nums=[10,20,15,30,20], k=2
- 输出
- 15 (容量取 [10,20,20,30,30])
- 输入
- nums=[10,20], k=0
- 输出
- 10 (容量全设 20)
先想最直接的笨办法
记牢这套两步走:先用 g 表把任意一段的浪费一次算清,再用 f 表枚举「最后一段切在哪」,把问题拆成前面少一段的子问题。下面先算 g 表,再填 f 表。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1959 K 次调整数组大小浪费的最小总空间:调 k 次即切成 k+1 段,每段容量取段内最大值。g 表先算清任一段的浪费,f[i][j] 再枚举最后一段起点做划分 DP,时间 O(n²·k)、空间 O(n²+n·k)。
容量能改 k 次,浪费到底怎么算?
nums[t] 是第 t 个时刻(下标从 0 数起)要装的元素数,容量不能小于它,多出的就是浪费;最多调 k 次,初始设定不计入,求总浪费最小。题面例子 nums=[10,20,15,30,20] 最多调 2 次,答案 15。
两次调整之间容量不变,调 k 次等于把时间轴切成 k+1 段。
把每种切法都试一遍,账要算多大?
n 个时刻有 n-1 个空隙,切成 k+1 段的方案数是 C(n-1,k)(从 n-1 个空隙里挑 k 个的选法数),n=200 时有几十位,试不完。
重复也躲不掉:不同切法常共用前缀,同一笔账算了又算。这笔账只由前缀决定,存下来复用——这就是动态规划(简称 DP,把「前 i 个时刻切 j 段的最小浪费」记进表、后面直接取)。
一段的容量为什么必须取段内最大值?
一段之内容量不动、又得每刻盖得住,只能取段内最大的 nums,再大只多浪费。这段浪费于是定死:最大值乘段长,减段内元素和。
开一张 g 表:g[i][j] 存 [i..j] 这段的浪费;固定起点 i、右挪结尾 j,顺手维护最大值与和,每格 O(1) 填出(大 O 记号,描述规模变大时操作次数怎么涨)。
最后一段切在哪,凭什么拆成子问题?
这是划分 DP——把一排东西切成几段、逐段结账。定义 f[i][j]:前 i 个时刻切成 j 段的最小总浪费。最后一段总从某个下标 h 到 i-1,代价 g[h][i-1];砍掉它,剩下正是「前 h 个时刻切 j-1 段」。
转移(由小问题推这一格的式子):对每个 h 取 f[h][j-1]+g[h][i-1] 的最小值。f[0][0]=0 是 base case(最简单、不用再拆的情况),其余铺无穷大当暂无方案。答案在 f[n][k+1]。
[10,20,15,30,20]调 2 次,15 是怎么切出来的
先填 g:对角线单独成段全 0;g[0][1]=20×2-30=10,g[1][2]=5,g[2][3]=15,g[3][4]=10,g[0][2]=15,g[2][4]=25,g[0][3]=45,g[0][4]=55。
再填 f,段数 3。1 段列抄 g 第 0 行:0、10、15、45、55。2 段列:f[3][2]=min(0+5,10+0)=5,f[4][2]=min(0+25,10+15,15+0)=15,f[5][2]=min(0+35,10+25,15+10,45+0)=25。3 段列:f[3][3]=0、f[4][3]=5;f[5][3] 的最后一段从下标 2、3、4 起,各给 0+25=25(f[2][2]=g[1][1] 一段自身=0)、5+10=15、15+0=15,取 15 即题面答案。
忘了把 k 加 1,15 为什么会算成 25?
段数是 k+1 不是 k;拿 k 当段数,本例只切 2 段,答案停在 f[5][2]=25。k 取 0 时只许一段、容量取全局最大。f 的初值铺 0 而不是无穷大,会让数不够分段的非法格混进最小值。
g 表 O(n²),f 表 n×(k+1) 格、每格枚举 O(n) 起点,时间 O(n²·k),空间 O(n²+n·k)。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记牢这套两步走:先用 g 表把任意一段的浪费一次算清,再用 f 表枚举「最后一段切在哪」,把问题拆成前面少一段的子问题。下面先算 g 表,再填 f 表。
- 4先看原始数据 [10,20,15,30,20],它是每个时刻要装的元素数。容量必须盖住元素,盖多了就浪费。如果一段时间用同一个容量,那容量必须等于这段里最大的 nums,多出来的都是浪费。
- 5先看一个极端:一次都不调整,整条时间线只能用一个容量。那容量得盖住全局最大的 30,五个时刻容量都是 30,总浪费是 30 乘 5 减去元素总和 95,等于 55。这就是不切段的代价。我们要用切段把它压下来。
- 6搭第一张表 g:行是段的起点 i,列是段的结尾 j。某格 g[i][j] 记的是「把 [i..j] 这段用一个容量装下」的浪费,也就是这段最大值乘段长,再减掉段内元素和。左下角灰掉的「无」是起点比结尾还靠后的非法格。先填对角线。
- 7对角线上每一格都是「一个数单独成段」。容量正好等于这个数本身,一点不多,浪费全是 0。五个 0 一次填好。接下来往右上方走,看多个数挤在一段会浪费多少。
- 8g[0][1] 是区间 [10,20] 合成一段。容量得取最大的 20,两个时刻都用 20,总容量 20 乘 2 等于 40,减去这段元素和 30,浪费 10。填进去。
- 9g[1][2] 是区间 [20,15]。容量取最大的 20,20 乘 2 等于 40,减去和 35,浪费只有 5。这一格待会儿最优解会用到,记一下。
- 10g[3][4] 是区间 [30,20]。容量取最大的 30,30 乘 2 等于 60,减去和 50,浪费 10。这也是最优解要用的一段,先备着。
- 11照同样的公式把上三角剩下的格子全补上。比如 [10,20,15,30] 这段最大 30,30 乘 4 减 75 等于 45;整条 [10,20,15,30,20] 最大 30,30 乘 5 减 95 等于 55,正好对上刚才不切段的代价。g 表填满,任意一段的浪费现在都能一眼查到。
- 12搭第二张表 f:行是「用了前 i 个数」,列是「切成 j 段」。f[i][j] 记这种情况下的最小总浪费。起手只有 f[0][0] 等于 0,意思是零个数零段浪费为零;标「无」的格是数不够分那么多段的非法情况。我们从最左的 1 段列往右填。
- 13先填 1 段列。只切一段,意味着前 i 个数全挤在一起,浪费就是 g 表里 [0..i-1] 那一段,直接抄过来。一格一格填。
- 14一段的浪费就是 g 第 0 行原样搬下来:前 1 个数 0,前 2 个 10,前 3 个 15,前 4 个 45,五个全塞一段 55。这一列就是「后面只许一段」的地基,分多段时要靠它接力。
- 15进入 2 段列。现在允许切两段:枚举最后一段从哪个位置 h 起头,前半段前 h 个数已经在 1 段列算好了,最后一段 [h..i-1] 的浪费从 g 表查。两者相加取最小。前 1 个数分不出两段,标「无」。
- 16前 2 个数 [10,20] 切两段,只有一种切法:各自一段。f[1][1] 是 0,最后一段 [20] 单独一段浪费也是 0,合起来 0。每个数独占一段,一点不浪费。
- 17前 3 个数 [10,20,15] 切两段,两个切点都试。切在第 1 个后面:前段 0 加最后一段 [20,15] 浪费 5,得 5;切在第 2 个后面:前段 10 加 [15] 浪费 0,得 10。取小的 5。把 20 和 15 放一段更省。
- 18前 4 个数 [10,20,15,30] 切两段。逐个切点比下来,最省的是在第 3 个后面切:前段 [10,20,15] 一段浪费 15,最后一段 [30] 浪费 0,合起来 15。把突起的 30 单独拎出来最划算。
- 19前 5 个数全用上、切两段。最优在第 3 个后面切:前段 [10,20,15] 浪费 15,最后一段 [30,20] 浪费 10,合起来 25。2 段列填满,注意它比 1 段列的 55 已经省了一半多。
- 20最后一列 3 段,也就是题目要的 k 加 1 段。转移一模一样,只是最后一段前面接的是 2 段列的结果。前 1、前 2 个数不够分 3 段,标「无」。我们要的答案 f[5][3] 就在这一列最底下。
- 21前 3 个数分 3 段,只能每个数各占一段。前 2 个数分 2 段是 0,最后一段 [15] 浪费 0,合起来 0。三个数各自成段,毫无浪费。
- 22前 4 个数分 3 段。最省的是在第 3 个后面切:前 3 个数分 2 段是 5,最后一段 [30] 浪费 0,合起来 5。前面把 20、15 凑一段那点浪费,是眼下躲不掉的。
- 23到正主了:五个数全用上、切成 3 段。逐个枚举最后一段的起点下标 h:从下标 h=2 开始,前段 0 加 [15,30,20] 浪费 25,得 25;从下标 h=3 开始,前段 5 加 [30,20] 浪费 10,得 15;从下标 h=4 开始,前段 15 加 [20] 浪费 0,也是 15。最小是 15。
- 24取最小值 15 填进 f[5][3]。这就是「五个时刻切成 3 段」的最小总浪费,也就是题目要的答案。两张表都填完了。
- 25顺着每格当初选的切点回溯:f[5][3] 的最后一段是 [30,20],跳到 f[3][2];f[3][2] 的最后一段是 [20,15],跳到 f[1][1];f[1][1] 就是头一段 [10]。三段正好是 [10]、[20,15]、[30,20]。
- 26回到原数组看最终分法:绿色 [10] 一段容量 10,蓝色 [20,15] 一段容量 20,红色 [30,20] 一段容量 30。五个时刻容量排成 [10,20,20,30,30],浪费只在下标 2 多 5、下标 4 多 10,一共 15,和表算出的严丝合缝。突起的大值各自靠后成段,是最省的切法。
⚠️ 容易写错的地方
✗ 错:把「调整 k 次」理解成 k 段
✓ 对:调整 k 次是把时间线切成 k 加 1 段
初始容量不算一次调整,所以 k 次调整会产生 k 加 1 个不同容量的区间,段数要记得加 1
✗ 错:每段容量取平均或首元素
✓ 对:每段容量必须取该段最大 nums
容量要盖住段内每一刻,低于最大值就装不下,取最大值才是合法且最省的选择
✗ 错:每次重新扫一段求最大值和和
✓ 对:扩展结尾 j 时顺手维护 mx 和 s
固定起点向右扩时,最大值和元素和都能 O(1) 增量更新,省掉一层循环
✗ 错:无穷大用 int 上限还去相加
✓ 对:用 0x3f3f3f3f 这类留了余量的大数
直接拿 INT_MAX 相加会溢出变负,0x3f3f3f3f 加个 g 也不会越界,是竞赛常用技巧
完整代码(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 minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
k += 1
n = len(nums)
g = [[0] * n for _ in range(n)]
for i in range(n):
s = mx = 0
for j in range(i, n):
s += nums[j]
mx = max(mx, nums[j])
g[i][j] = mx * (j - i + 1) - s
f = [[inf] * (k + 1) for _ in range(n + 1)]
f[0][0] = 0
for i in range(1, n + 1):
for j in range(1, k + 1):
for h in range(i):
f[i][j] = min(f[i][j], f[h][j - 1] + g[h][i - 1])
return f[-1][-1]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 minSpaceWastedKResizing(vector<int>& nums, int k) {
++k;
int n = nums.size();
vector<vector<int>> g(n, vector<int>(n));
for (int i = 0; i < n; ++i) {
int s = 0, mx = 0;
for (int j = i; j < n; ++j) {
mx = max(mx, nums[j]);
s += nums[j];
g[i][j] = mx * (j - i + 1) - s;
}
}
int inf = 0x3f3f3f3f;
vector<vector<int>> f(n + 1, vector<int>(k + 1, inf));
f[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
for (int h = 0; h < i; ++h) {
f[i][j] = min(f[i][j], f[h][j - 1] + g[h][i - 1]);
}
}
}
return f[n][k];
}
};Java
import java.util.*;
class Solution {
public int minSpaceWastedKResizing(int[] nums, int k) {
++k;
int n = nums.length;
int[][] g = new int[n][n];
for (int i = 0; i < n; ++i) {
int s = 0, mx = 0;
for (int j = i; j < n; ++j) {
s += nums[j];
mx = Math.max(mx, nums[j]);
g[i][j] = mx * (j - i + 1) - s;
}
}
int[][] f = new int[n + 1][k + 1];
int inf = 0x3f3f3f3f;
for (int i = 0; i < f.length; ++i) {
Arrays.fill(f[i], inf);
}
f[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
for (int h = 0; h < i; ++h) {
f[i][j] = Math.min(f[i][j], f[h][j - 1] + g[h][i - 1]);
}
}
}
return f[n][k];
}
}复杂度
时间
O(n² · k)
预处理 g 表是 O(n²);填 f 表状态有 n 乘 k 个,每个状态再枚举最后一段起点 O(n),合起来 O(n² 乘 k),k 这里指 k 加 1 段
空间
O(n² + n · k)
按峰值算:g 表占 n 乘 n,f 表占 n 乘 k。都是二维表,不随规模平方叠加,峰值就是两张表之和
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 K 次调整数组大小浪费的最小总空间 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
题目说最多调 k 次,代码却固定切成 k+1 段,会不会漏掉少调几次更优的方案?+
不会。把任何一段再切一刀,两个新段各取各的最大值,都不超过原段的最大值,浪费只会持平或变小,所以切满 k+1 段永远不吃亏。题目数据里 k 不会超过 n-1,段数也就不会多于时刻数,f[n][k+1] 一定有合法值,它就是全局最优。
不先预处理 g 表,转移时现场算段浪费行不行?+
行,但账要算清。现场从头累这一段的最大值与和是 O(段长),f 的每次转移都要付一遍,总复杂度涨到 O(n³·k) 量级。也可以在固定 i、把起点 h 从 i-1 往左挪的同时倒着维护最大值与和,不开 g 表也保持 O(n²·k);g 表版胜在预处理和转移两步分明,更好讲清楚。
这套「前 i 个切成 j 段」的填表还能搬到哪些题?+
LeetCode 410 分割数组的最大值、813 最大平均值和的分组、1278 分割回文串 III 都是同一副骨架:状态记「前缀 × 段数」,转移枚举最后一段起点,只换每段的代价——本题是浪费,410 是段和取最大,813 是段平均值求和。骨架练熟,换个代价函数就能上手。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 K 次调整数组大小浪费的最小总空间 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。