题目概述
You are given two 0-indexed integer arrays, cost
and time
, of size n
representing the costs and the time taken to paint n
different walls respectively. There are two painters available:
A paid painter that paints the
i
th wall intime[i]
units of time and takescost[i]
units of money.A free painter that paints any wall in
1
unit of time at a cost of0
. But the free painter can only be used if the paid painter is already occupied.
Return the minimum amount of money required to paint the n
walls.
Constraints:
1 <= cost.length <= 500
cost.length == time.length
1 <= cost[i] <= 106
1 <= time[i] <= 500
思路 & 代码实现
方法1:Top-Down Dynamic Programming
按照动态规划的解决问题思路,我们需要先定义子问题的状态。不妨假设油漆工按照索引由小到大的顺序工作,如此便可以自然得出第一个状态参数是当前面对的这面墙的索引值。除此之外显然还需要一个量用于判定整个漆墙工作是否完成,那便是已漆墙的个数。这里可以用i
, painted
两个变量指代。
根据题意可以推理出对于每一面墙来说,存在两种被漆的可能:
- 被付费油漆工漆
- 被免费油漆工漆
而免费油漆工的漆墙次数其实取决于付费油漆工,或者更准确地说,免费油漆工的漆墙次数不会超过付费油漆工的漆墙次数。而如果希望成本最小化,在付费油漆工漆墙的这段时间内,免费油漆工必定会一直工作。所以我们只需要关注付费油漆工的行为即可。而对于付费油漆工来说,每遇到一面墙他都可以做出两种选择:漆,或不漆。假设墙的总数为n
,根据这两种不同的行为,我们可以推导出如下两种不同的状态变化:
- 选择漆第
i
面墙,那么在面对第i + 1
面墙时,已漆的墙数为painted + 1 + time[i]
, 当前成本增加cost[i]
- 选择不漆第
i
面墙, 那么在面对第i + 1
面墙时,已漆的墙数为painted
, 当前成本不变
按照自顶向下的解决问题思路,定义递归函数dp
,传入其中的参数为i
,painted
。递归终止的条件有以下两种:
painted >= n
, 这代表所有的墙都已被漆完,无需再进行任何工作,此时的后续成本为0
。i >= n
, 注意这个条件要在上一个条件之后,此时所处的索引已经位于范围之外,这表明尚有墙未被漆完,选择不漆的墙数过多了。故此种情况不能满足题目要求要被舍弃,而因为最终答案是要求最低成本,所以为保证此种情况被舍弃,返回inf
。
同时为避免重复计算,需要记忆已经计算的结果,python中可选择@cache
装饰器。代码实现如下:
Time complexity: O(n^2)
Space complexity: O(n^2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
@cache
def dp(i, painted):
if painted >= n:
return 0
if i >= n:
return inf
# choose to paint current wall
paint = cost[i] + dp(i + 1, painted + 1 + time[i])
# choose to ignore current wall
not_paint = dp(i + 1, painted)
return min(paint, not_paint)
return dp(0, 0)
顺利通过。
方法2: Bottom-Up Dynamic Programming
自底向上的动态规划采用迭代的方式,用dp
数组记录每一个子问题的状态。与自顶向下的记忆化搜索不同,迭代则需要从最小的子问题开始依次向上求解,直到得出最终问题的答案。虽然在子问题结构上解决问题的方向不同,但是状态转移的方式是一样的,所以可以参考方法1中的思路。
首先,方法1中的dp
函数传入了两个参数i
和painted
,那么与之相对应的dp
数组将存在两个维度,分别表示方法1中的两个参数。接下来定义dp[i][j]
表示的状态:漆到第i面墙,且已漆j面墙时,接下来为完成任务所需的最低成本。接下来考虑初始状态,对应的是dp
函数中不符合条件返回的部分,也即墙的索引值超出范围时尚未漆完全部的墙的情况。用代码实现如下:
1
2
3
dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
for painted in range(n):
dp[n][painted] = inf
这里dp
数组的长度为n+1
而不是n
的原因是需要超出索引范围的情况作为初始状态(与dp
函数的返回条件对应)。而因为dp[n][n]
表示的含义是刚好在最后一面墙时完成所有漆墙任务,所以符合题意,后续的最低成本为0。接下来思考状态转移,假设当前的状态为dp[i][painted]
,也即面对第i
面墙时已漆painted
面墙的状态。那么还是分为两种情况:
- 不漆第
i
面墙,则后续最低成本为面对第i + 1
面墙,且已漆painted
面墙时的后续最低成本 - 漆第
i
面墙,则后续最低成本为面对第i + 1
面墙,且已漆painted + 1 + time[i]
面墙时的后续最低成本加上漆当前这面墙的成本cost[i]
最后在两种情况中选择所需成本最少的那一个。解释一下第二种情况:painted + 1 + time[i]
中1
表示付费油漆工漆的一面墙,time[i]
表示的是在付费油漆工漆这面墙时免费油漆工可以漆的墙数。加起来以后表示的是上一个状态已经漆的墙数。这里需要注意一个细节:此结果有可能大于n
,所以需要判断一下,如果超过了就修改成n
。状态转移的代码实现如下:
1
dp[i][painted] = min(dp[i+1][painted], dp[i+1][painted+1+time[i]] + cost[i])
最后需要明确按照当前的状态定义,最终所需的结果应该是保存在dp[0][0]
中的,也即面对第0
面墙,且尚未漆任何墙时为完成任务所需的最低成本。因此需要注意i
与painted
都应按照从大到小的顺序进行循环。最终代码实现如下:
Time complexity: O(n^2)
Space complexity: O(n^2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
# dp[i][j], i th wall, j painted, minimum cost
dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
for painted in range(n):
dp[n][painted] = inf
for i in range(n-1, -1, -1):
for painted in range(n, -1, -1):
dp[i][painted] = min(dp[i+1][painted], dp[i+1][min(n, painted+1+time[i])] + cost[i])
return dp[0][0]
顺利通过。
方法3:Space-Optimized Dynamic Programming
可以注意到方法2中dp[i][*]
的状态转移只取决于dp[i+1][*]
,所以可以将i
这一维度压缩,将空间复杂度优化到 $O(n)$。具体实现方式如下:
Time complexity: O(n^2)
Space complexity: O(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
prev = [inf for _ in range(n+1)]
prev[-1] = 0
cur = [0 for _ in range(n+1)]
for i in range(n-1, -1, -1):
for painted in range(n, -1, -1):
cur[painted] = min(prev[painted], prev[min(n, painted+1+time[i])] + cost[i])
prev = cur[:]
return prev[0]
这里prev
数组对应的是原先的dp[i + 1]
,cur
数组对应的是dp[i]
。
细节:prev = cur[:]
处不能写成prev = cur
,因为这样prev
并不是复制了cur
这个数组,而是获得了cur
这个数组的引用。这会导致后续修改cur
数组的同时prev
也被修改,而cur
数组中的元素更新需要依赖原本的prev
数组中保存的信息,所以最终会导致答案出错。
顺利通过。