加载中...
1473-粉刷房子 III(Paint House III)
发表于:2021-12-03 | 分类: 困难
字数统计: 953 | 阅读时长: 4分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/paint-house-iii

英文原文

There is a row of m houses in a small city, each house must be painted with one of the n colors (labeled from 1 to n), some houses that have been painted last summer should not be painted again.

A neighborhood is a maximal group of continuous houses that are painted with the same color.

  • For example: houses = [1,2,2,3,3,2,1,1] contains 5 neighborhoods [{1}, {2,2}, {3,3}, {2}, {1,1}].

Given an array houses, an m x n matrix cost and an integer target where:

  • houses[i]: is the color of the house i, and 0 if the house is not painted yet.
  • cost[i][j]: is the cost of paint the house i with the color j + 1.

Return the minimum cost of painting all the remaining houses in such a way that there are exactly target neighborhoods. If it is not possible, return -1.

 

Example 1:

Input: houses = [0,0,0,0,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
Output: 9
Explanation: Paint houses of this way [1,2,2,1,1]
This array contains target = 3 neighborhoods, [{1}, {2,2}, {1,1}].
Cost of paint all houses (1 + 1 + 1 + 1 + 5) = 9.

Example 2:

Input: houses = [0,2,1,2,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3
Output: 11
Explanation: Some houses are already painted, Paint the houses of this way [2,2,1,2,2]
This array contains target = 3 neighborhoods, [{2,2}, {1}, {2,2}]. 
Cost of paint the first and last house (10 + 1) = 11.

Example 3:

Input: houses = [0,0,0,0,0], cost = [[1,10],[10,1],[1,10],[10,1],[1,10]], m = 5, n = 2, target = 5
Output: 5

Example 4:

Input: houses = [3,1,2,3], cost = [[1,1,1],[1,1,1],[1,1,1],[1,1,1]], m = 4, n = 3, target = 3
Output: -1
Explanation: Houses are already painted with a total of 4 neighborhoods [{3},{1},{2},{3}] different of target = 3.

 

Constraints:

  • 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

中文题目

在一个小城市里,有 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

通过代码

高赞题解

动态规划

定义 $f[i][j][k]$ 为考虑前 $i$ 间房子,且第 $i$ 间房子的颜色编号为 $j$,前 $i$ 间房子形成的分区数量为 $k$ 的所有方案中的「最小上色成本」。

我们不失一般性的考虑 $f[i][j][k]$ 该如何转移,由于某些房子本身就已经上色,上色的房子是不允许被粉刷的。

我们可以根据第 $i$ 间房子是否已经被上色,进行分情况讨论:

  • 第 $i$ 间房子已经被上色,即 $houses[i] != 0$,此时只有满足 $j == houses[i]$ 的状态才是有意义的,其余状态均为 INF
    同时根据「第 $i$ 间房子的颜色 $j$」与「第 $i - 1$ 间房子的颜色 $p$」是否相同,会决定第 $i$ 间房子是否形成一个新的分区。这同样需要进行分情况讨论。
    整理后的转移方程为:

$$ f[i][j][k]=\begin{cases}
min(f[i - 1][j][k], f[i - 1][p][k - 1]) &j == houses[i], p != j\
INF & j != houses[i]
\end{cases}$$

  • 第 $i$ 间房子尚未被上色,即 $houses[i] == 0$,此时房子可以被粉刷成任意颜色。不会有无效状态的情况。
    同样,根据「第 $i$ 间房子的颜色 $j$」与「第 $i - 1$ 间房子的颜色 $p$」是否相同,会决定第 $i$ 间房子是否形成一个新的分区。
    转移方程为:

$$ f[i][j][k] = min(f[i - 1][j][k], f[i - 1][p][k - 1]) + cost[i][j - 1], p != j $$

一些编码细节:

  • 下标转换:这是个人习惯,无论做什么题,我都喜欢将下标转换为从 $1$ 开始,目的是为了「节省负值下标的分情况讨论」、「将无效状态限制在 $0$ 下标内」或者「充当哨兵」等等。
  • 0x3f3f3f3f 作为 INF:因为目标是求最小值,我们应当使用一个较大值充当正无穷,来关联无效状态。同时为了确保不会出现「在正无穷基础上累加导致丢失正无穷含义」的歧义情况,我们可以使用一个有「累加空间」的值作为「正无穷」(这个问题刚好最近在 这里 专门讲过)。

代码(感谢 @🍭可乐可乐吗QAQ@『‖— ˇ —‖』 两位同学提供的其他语言版本):

[]
class Solution { int INF = 0x3f3f3f3f; public int minCost(int[] hs, int[][] cost, int m, int n, int t) { int[][][] f = new int[m + 1][n + 1][t + 1]; // 不存在分区数量为 0 的状态 for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { f[i][j][0] = INF; } } for (int i = 1; i <= m; i++) { int color = hs[i - 1]; for (int j = 1; j <= n; j++) { for (int k = 1; k <= t; k++) { // 形成分区数量大于房子数量,状态无效 if (k > i) { f[i][j][k] = INF; continue; } // 第 i 间房间已经上色 if (color != 0) { if (j == color) { // 只有与「本来的颜色」相同的状态才允许被转移 int tmp = INF; // 先从所有「第 i 间房形成新分区」方案中选最优(即与上一房间颜色不同) for (int p = 1; p <= n; p++) { if (p != j) { tmp = Math.min(tmp, f[i - 1][p][k - 1]); } } // 再结合「第 i 间房不形成新分区」方案中选最优(即与上一房间颜色相同) f[i][j][k] = Math.min(f[i - 1][j][k], tmp); } else { // 其余状态无效 f[i][j][k] = INF; } // 第 i 间房间尚未上色 } else { int u = cost[i - 1][j - 1]; int tmp = INF; // 先从所有「第 i 间房形成新分区」方案中选最优(即与上一房间颜色不同) for (int p = 1; p <= n; p++) { if (p != j) { tmp = Math.min(tmp, f[i - 1][p][k - 1]); } } // 再结合「第 i 间房不形成新分区」方案中选最优(即与上一房间颜色相同) // 并将「上色成本」添加进去 f[i][j][k] = Math.min(tmp, f[i - 1][j][k]) + u; } } } } // 从「考虑所有房间,并且形成分区数量为 t」的所有方案中找答案 int ans = INF; for (int i = 1; i <= n; i++) { ans = Math.min(ans, f[m][i][t]); } return ans == INF ? -1 : ans; } }
[]
const int INF = 0x3f3f3f3f; //INF + INF 不会爆int int f[105][25][105]; class Solution { public: int minCost(vector<int>& houses, vector<vector<int>>& cost, int m, int n, int target) { //无效的状态 for(int i = 0; i <= m; i++){ for(int j = 0; j <= n; j++){ f[i][j][0] = INF; } } for(int i = 1; i <= m; i++){ int color = houses[i - 1]; for(int j = 1; j <= n;j++){ for(int k = 1; k <= target; k++){ if(k > i) { f[i][j][k] = INF; continue; } //第i个房间已经上色 if(color != 0){ if(j == color){ int cur = INF; //与上一个房间颜色不同 for(int p = 1; p <= n; p++){ if(p != j){ //颜色不同 cur = min(cur,f[i - 1][p][k - 1]); } } //与上一个房间颜色相同 f[i][j][k] = min(cur,f[i - 1][j][k]); } else f[i][j][k] = INF; //其他为无效状态 } else{ //第i个房间未上色 int u = cost[i - 1][j - 1]; //考虑第i个房间是否形成新的分区 //与上一个房间颜色不同,形成新分区 int cur = INF; for(int p = 1; p <= n; p++){ if(p != j) cur = min(cur,f[i - 1][p][k - 1] + u); } //与上一个房间颜色相同,不形成新的分区 f[i][j][k] = min(cur,f[i - 1][j][k] + u); } } } } int ans = INF; for(int i = 1; i <= n; i++){ ans = min(ans,f[m][i][target]); } return ans == INF ? -1 : ans; } };
[]
class Solution: def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int: INF = float("inf") f = [[[0]*(target+1) for _ in range(n+1)] for _ in range(m+1)] for i in range(m+1): for j in range(n+1): f[i][j][0] = INF for i in range(1,m+1): color = houses[i-1] for j in range(1,n+1): for k in range(1,target+1): if k>i: f[i][j][k] = INF continue if color!=0: #已经上色 if j==color: tmp = INF for p in range(1,n+1): if p!=j: tmp = min(tmp,f[i-1][p][k-1]) # 有新分区里的最小 f[i][j][k] = min(f[i-1][j][k],tmp) # 没有新分区 or 有新分区 else: f[i][j][k] = INF # 房间没上色 else: u = cost[i-1][j-1] tmp = INF # 形成新分区的最优情况 for p in range(1,n+1): if p!=j: tmp = min(tmp,f[i-1][p][k-1]) # 算上没有新分区 f[i][j][k] = min(f[i-1][j][k],tmp)+u res = INF for i in range(1,n+1): res = min(res,f[m][i][target]) return -1 if res == INF else res
  • 时间复杂度:共有 $m * n * t$ 个状态需要被转移,每次转移需要枚举「所依赖的状态」的颜色,复杂度为 $O(n)$。整体复杂度为 $O(m * n^2 * t)$
  • 空间复杂度:$O(m * n * t)$

状态定义的由来

对于有一定 DP 刷题量的同学来说,上述的「状态定义」应该很好理解。

根据经验,我们可以很容易确定「房间编号维度 $i$」和「分区数量维度 $k$」需要纳入考虑,同时为了在转移过程中,我们能够清楚知道从哪些状态转移过来需要增加「分区数量」,哪些状态转移过来不需要增加,因此需要多引入「最后一间房间颜色 $j$」维度。

至于对 DP 不熟悉的同学,可以从写「爆搜」开始入手。

这里的“写”不一定真的要动手去实现一个完整的「爆搜」方案,只需要合理设计出来 DFS 函数签名即可。

但为了更好理解,我写了一个完整版的供你参考。

代码:

[]
class Solution { int INF = 0x3f3f3f3f; int ans = INF; int[] hs; int[][] cost; int m, n, t; public int minCost(int[] _hs, int[][] _cost, int _m, int _n, int _t) { m = _m; n = _n; t = _t; hs = _hs; cost = _cost; dfs(0, -1, 0, 0); return ans == INF ? -1 : ans; } // u : 当前处理到的房间编号 // last : 当前处理的房间颜色 // cnt : 当前形成的分区数量 // sum : 当前的上色成本 void dfs(int u, int last, int cnt, int sum) { if (sum >= ans || cnt > t) return; if (u == m) { if (cnt == t) { ans = Math.min(ans, sum); } return; } int color = hs[u]; if (color == 0) { for (int i = 1; i <= n; i++) { int nCnt = u - 1 < 0 ? 1 : last == i ? cnt : cnt + 1; dfs(u + 1, i, nCnt, sum + cost[u][i - 1]); } } else { int nCnt = u - 1 < 0 ? 1 : last == color ? cnt : cnt + 1; dfs(u + 1, color, nCnt, sum); } } }
  • 时间复杂度:n 为颜色数量,m 为房间数量。不考虑剪枝效果,每个房间都可以粉刷 n 种颜色,复杂度为指数级别的 $O(n^m)$
  • 空间复杂度:忽略递归带来的额外空间开销。复杂度为 $O(1)$

可以发现,DFS 的可变参数有四个,其中 sum 是用于更新最终答案 ans 的,其应该作为动规值,其余三个参数,作为动规数组的三个维度。

至此,我们可以确定动态规划的「状态定义」,关于如何利用这种「技巧」来得到一个可靠的「状态定义」最早在 这里 讲过。


记忆化搜索

看到评论区有同学贴了「记忆化搜索」的版本,那么就作为补充增加到题解吧 ~

注意记忆化容器应当与我们的「动规数组」结构保存一致。

代码(感谢 @🍭可乐可乐吗QAQ@Benhao 同学提供的其他语言版本):

[]
class Solution { int INF = 0x3f3f3f3f; int m, n, t; int[] hs; int[][] cost; boolean[][][] vis; int[][][] cache; public int minCost(int[] _hs, int[][] _cost, int _m, int _n, int _t) { m = _m; n = _n; t = _t; hs = _hs; cost = _cost; vis = new boolean[m + 1][n + 1][t + 1]; cache = new int[m + 1][n + 1][t + 1]; int ans = dfs(0, 0, 0, 0); return ans == INF ? -1 : ans; } int dfs(int u, int last, int cnt, int sum){ if(cnt > t) return INF; if(vis[u][last][cnt]) return cache[u][last][cnt]; if (u == m) return cnt == t ? 0 : INF; int ans = INF; int color = hs[u]; if(color == 0){ for(int i = 1; i <= n; i++){ int nCnt = u == 0 ? 1 : last == i ? cnt : cnt + 1; int cur = dfs(u + 1, i, nCnt, sum + cost[u][i - 1]); ans = Math.min(ans, cur + cost[u][i - 1]); } } else{ int nCnt = u == 0 ? 1 : last == color ? cnt : cnt + 1; int cur = dfs(u + 1, color, nCnt, sum); ans = Math.min(ans, cur); } vis[u][last][cnt] = true; cache[u][last][cnt] = ans; return ans; } }
[]
int f[105][25][105]; const int INF = 0x3f3f3f3f; class Solution { public: int minCost(vector<int>& houses, vector<vector<int>>& cost, int m, int n, int target) { memset(f,-1,sizeof f); /* u: 当前处理到的房间编号 pre: 当前处理的房间颜色 cnt: 当前分区的数量 cur: 当前的上色成本 */ //记忆化搜索 function<int(int,int,int,int)> dfs = [&](int u,int pre,int cnt,int cur){ if(cnt > target) return INF; if(u >= m) return cnt == target ? 0 : INF; if(f[u][pre][cnt] != -1) return f[u][pre][cnt]; int color = houses[u]; int ans = INF; if(color == 0){ for(int i = 1; i <= n; i++){ int new_cnt = u == 0 ? 1 : pre == i ? cnt : cnt + 1; int cur = dfs(u + 1,i,new_cnt,cur + cost[u][i - 1]); if(cur == INF) continue; ans = min(ans,cur + cost[u][i - 1]); } } else{ int new_cnt = u == 0 ? 1 : pre == color ? cnt : cnt + 1; int cur = dfs(u + 1,color,new_cnt,cur); ans = min(ans,cur); } return f[u][pre][cnt] = ans; }; int ans = dfs(0,0,0,0); return ans == INF ? -1 : ans; } };
[]
class Solution: def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int: @lru_cache(None) def dfs(idx, color, t): if t < 0 or t > m - idx: return float("inf") if idx == m: return 0 curr = float("inf") if houses[idx]: if houses[idx] != color: curr = min(curr, dfs(idx + 1, houses[idx], t - 1)) else: curr = min(curr, dfs(idx + 1, houses[idx], t)) else: for i in range(1, n + 1): if i != color: curr = min(curr, dfs(idx + 1, i, t - 1) + cost[idx][i - 1]) else: curr = min(curr, dfs(idx + 1, i, t) + cost[idx][i - 1]) return curr res = dfs(0, 0, target) if res == float("inf"): return -1 return res
  • 时间复杂度:共有 $m * n * t$ 个状态需要被转移,每次转移需要枚举「所依赖的状态」的颜色,复杂度为 $O(n)$。整体复杂度为 $O(m * n^2 * t)$
  • 空间复杂度:$O(m * n * t)$

最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ (“▔□▔)/

也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解

统计信息

通过次数 提交次数 AC比率
15638 23226 67.3%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言
上一篇:
1472-设计浏览器历史记录(Design Browser History)
下一篇:
1492-n 的第 k 个因子(The kth Factor of n)
本文目录
本文目录