💻 动态数组最小浪费空间 —— 动态规划经典问题解析
📌 题目描述
在实现动态数组时,我们常常面临空间与性能的权衡。某一时刻数组的大小必须足够容纳所有元素,但也不能浪费太多内存。本题模拟的就是这样一个真实场景:
给你一个整数数组 nums
,其中 nums[i]
表示第 i
时刻数组中元素的数量。
此外,允许你最多调整数组大小 k
次(每次可以调整成任意大小,但大小必须 ≥ 当前元素数量)。
每个时刻 t
的数组空间大小必须 ≥ nums[t]
。
浪费空间 定义为该时刻数组大小减去实际元素数量:size_t - nums[t]
。
你的任务是:在最多调整数组大小 k
次的前提下,最小化整个过程中的总浪费空间。
输入输出示例
示例 1:
输入: nums = [10, 20], k = 0
输出: 10
解释: 初始大小为20,总浪费空间为(20-10) + (20-20) = 10。
示例 2:
输入: nums = [10,20,30], k = 1
输出: 10
解释: 初始大小为20,在时刻2调整为30,总浪费为10。
示例 3:
输入: nums = [10,20,15,30,20], k = 2
输出: 15
解释: 尽量每一段固定大小,总浪费为15。
约束条件
1 <= nums.length <= 200
1 <= nums[i] <= 10^6
0 <= k <= nums.length - 1
🧠 解题思路分析
核心思想:分段 + 动态规划
数组最多可以调整大小 k
次,因此可以将 nums
分为最多 k + 1
个连续的段。每一段中,我们选用一个固定的数组大小,必须 ≥ 段内所有元素的最大值。
在某一段 [i..j]
中:
- 最优的数组大小是这段中的最大值
max(nums[i..j])
- 这段的总浪费 =
段长度 * 段最大值 - 段内元素和
转化为 DP 问题
我们定义:
dp[i][j]
:前i
个时刻,用j
段(即调整j-1
次)时的最小浪费空间。- 初始化:
dp[0][0] = 0
(0 个元素,0 段,浪费为 0)
状态转移:
dp[i][j] = min(dp[t][j-1] + cost[t][i-1]) for all j-1 ≤ t < i
其中 cost[t][i-1]
表示将 nums[t..i-1]
当作一段时的浪费空间(已预处理)。
🧮 解题方法(Python 实现)
from typing import List
import math
class Solution:
def minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
n = len(nums)
# 前缀和:用于快速求区间和
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
# cost[i][j]: 将 nums[i..j] 作为一个段的最小浪费
cost = [[0] * n for _ in range(n)]
for i in range(n):
max_val = nums[i]
for j in range(i, n):
max_val = max(max_val, nums[j])
cost[i][j] = max_val * (j - i + 1) - (prefix[j + 1] - prefix[i])
# dp[i][j]: 前 i 个数,用 j 段的最小浪费
dp = [[math.inf] * (k + 2) for _ in range(n + 1)]
dp[0][0] = 0
for i in range(1, n + 1): # 前 i 个数
dp[i][1] = cost[0][i - 1] # 只用一段的情况
max_parts = min(k + 1, i)
for j in range(2, max_parts + 1): # 分成 j 段
for t in range(j - 1, i): # 分段点 t
dp[i][j] = min(dp[i][j], dp[t][j - 1] + cost[t][i - 1])
return dp[n][k + 1]
🔍 示例说明与过程还原
以输入 nums = [10,20,15,30,20], k = 2
为例:
我们希望将数组分为 3 段(最多调整 2 次):
一种最优划分方案:
- 段 1:
[10]
大小为10,浪费为0 - 段 2:
[20,15]
大小为20,浪费 =(20-20) + (20-15) = 5
- 段 3:
[30,20]
大小为30,浪费 =(30-30) + (30-20) = 10
总浪费 = 0 + 5 + 10 = 15
⏱ 复杂度分析
时间复杂度
O(n^2)
:构造cost[i][j]
O(n^2 * k)
:三重循环进行状态转移
总复杂度为 O(n^2 * k)
,在 n ≤ 200
时完全可以接受。
空间复杂度
O(n^2)
:用于存储cost
表O(nk)
:用于 DP 状态表
📈 分析与比较
方法 | 时间复杂度 | 空间复杂度 | 适用范围 |
---|---|---|---|
暴力递归 | 指数级(不可用) | 高 | 不适合大数据 |
本文 DP 方法 | O(n^2 * k) | O(n^2) | n ≤ 200 的场景 |
进一步优化(如单调队列优化) | O(nk) (理想) | 中等 | 面试挑战题可考虑 |
🧾 总结
这道题结合了「动态规划」、「分段最优化」、「前缀和」与「区间最大值预处理」等多个算法思想,是一道很适合锻炼 DP 思维的题目。