[20210504]LeetCode每日一题 - 1473. 粉刷房子 III

注:本篇题目较难,因此虽然是0504的题目,但放到了0505发布

题目描述

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

题目地址:地址


初步分析

根据题目的描述,初步分析其必定得使用动态规划才可以解出此题 (暴力法想都不要想了)
但题目中有三个变量(房子个数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] !== jdp(0, j, 0) = ∞
  • hourses[i] !== -1 && hourses[i] === jdp(0, j, 0) = 0
  • hourses[i] === -1dp(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版貌似只有我提交了)


参考

以上内容参考自:


最后

如果我在哪里写的有问题,欢迎指出,共同进步,谢谢阅读~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值