注:本篇题目较难,因此虽然是0504的题目,但放到了0505发布
题目描述
在一个小城市里,有
m
个房子排成一排,你需要给每个房子涂上n
种颜色之一(颜色编号为1
到n
)。有的房子去年夏天已经涂过颜色了,所以这些房子不需要被重新涂色。
我们将连续相同颜色尽可能多的房子称为一个街区。(比方说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
题目地址:地址
初步分析
根据题目的描述,初步分析其必定得使用动态规划才可以解出此题 (暴力法想都不要想了)
但题目中有三个变量(房子个数m
, 颜色个数n
, 街区k
),因此这是一个三维动态规划题目 (os: 二维就很讨厌了,还来个三维)
在此之前,如果不知道或不熟悉动态规划的朋友们,可以先刷一道经典的二维动态规划题:1143. 最长公共子序列,这道题有助于你理解动态规划中比较重要的状态转移。
对应的分析我后期会补上= =
正式分析
就我个人理解,动态规划是从最后面向前考虑,因为如果是从前往后考虑,则会导致很多的重复计算,浪费时间(当然如果从前往后计算时,可以使用memorize
牺牲空间换取时间,在此不做考虑)
使用动态规划,一般定义变量为dp
。对于此题,我们假设dp(i , j , k)
表示将0 ~ i
间房子都涂色,并且当最末尾的第 i
个房子的颜色为 j
,并且它属于第 k
个街区时,需要的最少花费。
进行状态转移时,我们需要考虑i - 1
号房子的颜色,因为根据题目,颜色涉及到花费及街区划分,所以需要对此进行枚举判断
假设第i - 1
号房子的颜色为j0
,我们根据不同情况讨论其动态转移方程:
为计算方便,所有的数值(房子index
、颜色
、街区
)从0开始编号
- 如果
hourses[i] !== -1
,即其已经涂色,因其不能再次涂色,对于颜色枚举有如下判断:- 如果
hourses[i] !== j
,则此时dp(i, j, k) = ∞
,因为是花费,所以此时颜色与枚举的j
不同时,可以使用最大值来表示此方案不通,用js
可以使用Number.MAX_VALUE
表达∞
- 如果
hourses[i] === j
,则此时:- 如果
j === j0
,即第i
间房子与第i - 1
间房子同色且属于同一街区,则dp(i, j, k) = dp(i - 1, j , k)
,即此时第i
间房子花费为0
,将dp(i - 1, j, k)
的费用转移、继承过来 - 如果
j !== j0
,即第i
间房子与第i - 1
间房子不同色且不属于同一街区,则dp(i, j, k) = min(dp(i, j, k), dp(i - 1, j0, k - 1))
。即虽然第i
间房子花费为0
,但转移的花费是从dp(i, j, k), dp(i - 1, j0, k - 1)
中取最小值而来的,其中dp(i - 1, j0, k - 1)
代表第i - 1
间房屋,涂色为j0
,且街区是k - 1
号时的花费(因颜色不同,街区不同)
- 如果
- 如果
- 如果
hourses[i] === -1
,即其未涂色,当第i
间房子涂成j
号颜色时:- 如果
j === j0
,即第i
间房子与第i - 1
间房子同色且属于同一街区,则dp(i, j, k) = min(dp(i, j, k), dp(i - 1, j, k)) + cost[i][j]
- 如果
j !== j0
,即第i
间房子与第i - 1
间房子不同色且不属于同一街区,则dp(i, j, k) = min(dp(i, j, k), dp(i - 1, j0, k - 1)) + cost[i][j]
- 如果
分析上述方程,发现当:
hourses[i] !== -1 && hourses[i] === j
hourses[i] === -1
这两种情况下,转移方程一致,因此可以合并
另考虑边界情况:
- 当
k === 0
时,不能从包含k - 1
的状态转移而来,因街区只从0号
开始 - 当
i === 0
时,因第0
个房子前没有房子,且此时k = 0
此时的状态方程如下:
- 当
hourses[i] !== -1 && hourses[i] !== j
,dp(0, j, 0) = ∞
- 当
hourses[i] !== -1 && hourses[i] === j
,dp(0, j, 0) = 0
- 当
hourses[i] === -1
,dp(0, j, 0) = cost[i][j]
对上述情况补足:当 i === 0 && k !== 0
时,dp(0, j, k) = ∞
具体代码
function minCost(houses: number[], cost: number[][], m: number, n: number, target: number): number {
// 将所有房子的颜色以0开始编号,没有涂色的即为-1
houses = houses.map(item => --item)
// 初始化 动态转移3维数组 , 并将每个可能的花费初始化为最大值
const dp = new Array(m).fill(0)
.map(() => new Array(n).fill(0)
.map(() => new Array(target).fill(Number.MAX_VALUE))
)
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
// 如果房子已经涂色了,但颜色并不是 j ,则直接continue
if (houses[i] !== -1 && houses[i] !== j) {
continue
}
// 对街区(第三维)做枚举
for (let k = 0; k < target; k++) {
// 对 i - 1 号房子的颜色做遍历
for (let j0 = 0; j0 < n; j0++) {
if (j === j0) {
if (i === 0) {
if (k === 0) { // 如果 i - 1 房子颜色与 i 房子颜色一致,且是第一栋房子,且是第一个街区,则不花钱
dp[i][j][k] = 0
}
} else { // 如果 i - 1 房子颜色与 i 房子颜色一致,但不是第一栋房子,则所花费的钱是 i 与 i - 1 房子花钱最少的那个
dp[i][j][k] = Math.min(dp[i][j][k], dp[i - 1][j][k])
}
} else if (i > 0 && k > 0) { // 如果 i - 1 房子的颜色与 i 房子的颜色不同,且不是第一栋房子及第一个街区
dp[i][j][k] = Math.min(dp[i][j][k], dp[i - 1][j0][k - 1])
}
}
// 如果当前房子花费不是初始值,且当前房子未涂色时,花费的钱要加上(即状态转移)cost[i][j]的钱
if (dp[i][j][k] !== Number.MAX_VALUE && houses[i] === -1) {
dp[i][j][k] += cost[i][j]
}
}
}
}
let rs = Number.MAX_VALUE
// 遍历颜色值,获取所花费最少的钱
for (let j = 0; j < n; j++) {
rs = Math.min(rs, dp[m - 1][j][target - 1])
}
return rs === Number.MAX_VALUE ? -1 : rs
};
TS AC 256 ms, 在所有 TypeScript 提交中击败了 100.00% 的用户 (ts版貌似只有我提交了)
参考
以上内容参考自:
- 粉刷房子 III 官方解答 (吐槽一下,其实官方文档没有看懂,是看了下面这位朋友画的图才能理解含义)
- 【1473.粉刷房子Ⅲ】状态转移图示
最后
如果我在哪里写的有问题,欢迎指出,共同进步,谢谢阅读~