【Leetcode每日一题】2021-05-03 1473 粉刷房子III - 困难 (Python)

来源:力扣

在一个小城市里,有 m 个房子排成一排,你需要给每个房子涂上 n 种颜色之一(颜色编号为 1n )。有的房子去年夏天已经涂过颜色了,所以这些房子不需要被重新涂色。

我们将连续相同颜色尽可能多的房子称为一个街区。(比方说 houses = [1,2,2,3,3,2,1,1] ,它包含 5 个街区 [{1}, {2,2}, {3,3}, {2}, {1,1}] 。)

给你一个数组 houses ,一个 m * n 的矩阵 cost 和一个整数 target ,其中:

houses[i]:是第 i 个房子的颜色,0 表示这个房子还没有被涂色。
cost[i][j]:是将第 i 个房子涂成颜色 j+1 的花费。
请你返回房子涂色方案的最小总花费,使得每个房子都被涂色后,恰好组成 target 个街区。如果没有可用的涂色方案,请返回 -1


示例 1:
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
输出:9
解释:房子涂色方案为 [1,2,2,1,1]
此方案包含 target = 3 个街区,分别是 [{1}, {2,2}, {1,1}]
涂色的总花费为 (1 + 1 + 1 + 1 + 5) = 9

示例 2:
输入:houses = [0,2,1,2,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
输出:11
解释:有的房子已经被涂色了,在此基础上涂色方案为 [2,2,1,2,2]
此方案包含 target = 3 个街区,分别是 [{2,2}, {1}, {2,2}]
给第一个和最后一个房子涂色的花费为 (10 + 1) = 11

示例 3:
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[1,10],[10,1],[1,10]], m = 5, n = 2, target = 5
输出:5

示例 4:
输入:houses = [3,1,2,3], cost = [[1,1,1],[1,1,1],[1,1,1],[1,1,1]], m = 4, n = 3, target = 3
输出:-1
解释:房子已经被涂色并组成了 4 个街区,分别是 [{3},{1},{2},{3}] ,无法形成 target = 3 个街区。

提示:
m == houses.length == cost.length
n == cost[i].length
1 <= m <= 100
1 <= n <= 20
1 <= target <= m
0 <= houses[i] <= n
1 <= cost[i][j] <= 10^4

标签:动态规划

动态规划

题目中有一点说得不太明确,题目中说已经粉刷过的房子不需要再刷,题目本意应该是去年夏天刷过的房子不准再刷,说不需要感觉还是可以再被刷成其他颜色,所以说有一点不太明确。但是两种意思都挺有意思的,而且代码层面差别不大,这里我就把两种情况的个人解答都写出来了。
其实两种题目的动态规划解题思路大致是一样的:
  - 首先我们可以看出房子i的状态只跟房子i-1的状态有关,状态可以按target来分,即每个房子可以有target种状态,每种状态还可以分为n种颜色,所以这里要用三维DP$\rightarrow$ dp[m][target][n]
  - 确定完状态下面就要推状态转移方程了。假设现在状态为dp[i][j][k],即房子i处于j街区时颜色为k的最小cost。首先按街区分为两种,$j = 0$和$j > 0$:
    $\rightarrow j = 0$: 说明房子i还处于第一街区,所以它的颜色必须和前一个房子i-1相同,所以$$dp[i][j][k] = dp[i-1][j][k] + cost[i][k]$$
    $\rightarrow j > 0$: 此时房子已经不属于第一街区了,所以它的上一个房子i-1可以和i颜色相同(即i-1i属于同一街区j),也可以不同(即i-1属于i的上一个街区j-1),所以我们只需从所有可能性中选出cost最小的那一个所以$$dp[i][j][k] = min([dp[i-1][j][k]] + [dp[i-1][j-1][c]\ for\ c\ in\ range(n)\ if\ c\ !=\ k]) + cost[i][k]$$
  - 我们下一步再确定一下dp表的初始值,从题目中我们看到$1 <= cost[i][j] <= 10^4, 1 <= m <= 100$我们可以知道cost的值最大为$10^4*100=10^6$,所以我们设置一个最大值MAX_COST为$10^6$,我们直接用MAX_COST填满dp表。第一个房子肯定是属于第一个街区的,所以dp[0][0] = cost[0],但是这里对于两种不同的题目会有所不同,下面在具体的题目下面会说明。

其实两种题目主要的区别是怎么利用房子已有的颜色来更新cost表,如果是不准再刷,则我们可以先把当前房子i的所有颜色的cost都设为最大值,然后把房子i已有的颜色k对应的cost[i][k]设为0,即不要改变颜色。如果是可刷可不刷,那么我们直接把房子i已有的颜色对应的cost[i][k]设为0即可。

1. 已经粉刷过的房子可以刷成其他颜色,也可以不刷

时间复杂度为$\mathcal{O}(m\times n^2\times target)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution:
def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int:
'''
dp[m][target][n]
dp[i][j][k]表示第i个houses为第j个街区时的颜色为k是最小的cost
因为1 <= cost[i][j] <= 10^4, 1 <= m <= 100,所以cost不会超过10^6
'''
MAX_COST = 10**6 + 1
dp = [[[MAX_COST]*n for _ in range(target)] for _ in range(m)]

# 根据房子已有的颜色调整cost,颜色不变就可以把cost设为0
for i in range(m):
if houses[i] == 0: continue
cost[i][houses[i]-1] = 0

dp[0][0] = cost[0]

for i in range(1, m):
for j in range(target):
for k in range(n):
if j == 0:
dp[i][j][k] = dp[i-1][j][k] + cost[i][k]
else:
'''
当前house为第j的街区且颜色为k时就从下面的两种情况选一个最小值:
- 当上一个house也属于j街区时,那颜色也必须是k
- 当上一个house属于j-1街区时,那就从颜色不为k的dp[i-1][j-1]中选一种
'''
dp[i][j][k] = min([dp[i-1][j][k]] + [dp[i-1][j-1][l] for l in range(n) if l != k]) + cost[i][k]
ans = min(dp[-1][-1])

return ans if ans < MAX_COST else -1

2. 已经粉刷过的房子不准再刷成其他颜色

时间复杂度为$\mathcal{O}(m\times n^2\times target)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int:
'''
dp[m][target][n]
dp[i][j][k]表示第i个houses为第j个街区时的颜色为k是最小的cost
因为1 <= cost[i][j] <= 10^4, 1 <= m <= 100,所以cost不会超过10^6
'''
MAX_COST = 10**6 + 1
dp = [[[MAX_COST]*n for _ in range(target)] for _ in range(m)]

# 根据房子已有的颜色调整cost,颜色不变就可以把cost设为0, 而其他颜色都设为最大cost
for i in range(m):
if houses[i] == 0: continue
cost[i] = [MAX_COST]*n
cost[i][houses[i]-1] = 0

dp[0][0] = cost[0]

for i in range(1, m):
for j in range(target):
for k in range(n):
if j == 0:
dp[i][j][k] = dp[i-1][j][k] + cost[i][k]
else:
'''
当前house为第j的街区且颜色为k时就从下面的两种情况选一个最小值:
- 当上一个house也属于j街区时,那颜色也必须是k
- 当上一个house属于j-1街区时,那就从颜色不为k的dp[i-1][j-1]中选一种
'''
dp[i][j][k] = min([dp[i-1][j][k]] + [dp[i-1][j-1][l] for l in range(n) if l != k]) + cost[i][k]
ans = min(dp[-1][-1])

return ans if ans < MAX_COST else -1

下面我们把上面的代码再优化一下,我们把$j>0$中当前街区j和上个房子的街区j-1不同时查找所有颜色不等于k时最小cost的复杂度$\mathcal{O}(n)$降到$\mathcal{O}(1)$,大致想法是用数组best来存储每个房子每个街区所有颜色中cost的最小值及其颜色值以及第二小的cost值$\rightarrow$[first, index of first, second]best大小为best[m][target][3]。之所以只需要这三个值,因为我们只想找出最小值,如果最小值的颜色和当前颜色相同那么直接用第二小的值即可,如果颜色不同就直接用最大值,这样就把查找的时间复杂度从$\mathcal{O}(n)$降为$\mathcal{O}(1)$。
时间复杂度优化为$\mathcal{O}(m\times n\times target)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution:
def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int:
'''
dp[m][target][n]
dp[i][j][k]表示第i个houses为第j个街区时的颜色为k是最小的cost
因为1 <= cost[i][j] <= 10^4, 1 <= m <= 100,所以cost不会超过10^6
'''
MAX_COST = 10**6 + 1
dp = [[[MAX_COST]*n for _ in range(target)] for _ in range(m)]

# 根据房子已有的颜色调整cost,颜色不变就可以把cost设为0, 而其他颜色都设为最大cost
for i in range(m):
if houses[i] == 0: continue
cost[i] = [MAX_COST]*n
cost[i][houses[i]-1] = 0

best = [[[MAX_COST, -1, MAX_COST] for _ in range(target)] for _ in range(m)]
for i in range(m):
for j in range(target):
if i == 0 and j == 0:
dp[i][j] = cost[i]
for k in range(n):
if i != 0:
if j == 0:
dp[i][j][k] = dp[i-1][j][k] + cost[i][k]
else:
'''
当前house为第j的街区且颜色为k时就从下面的两种情况选一个最小值:
- 当上一个house也属于j街区时,那颜色也必须是k
- 当上一个house属于j-1街区时,那就从颜色不为k的dp[i-1][j-1]中选一种
'''
dp[i][j][k] = min(dp[i-1][j][k], best[i-1][j-1][0] if best[i-1][j-1][1] != k else best[i-1][j-1][2]) + cost[i][k]

if dp[i][j][k] < best[i][j][0]:
best[i][j][2] = best[i][j][0]
best[i][j][0] = dp[i][j][k]
best[i][j][1] = k
elif dp[i][j][k] < best[i][j][2]:
best[i][j][2] = dp[i][j][k]
ans = min(dp[-1][-1])

return ans if ans < MAX_COST else -1