LeetCode第256题:粉刷房子
题目描述
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的矩阵来表示的。
例如,costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。请你计算出粉刷完所有房子最少的花费成本。
难度
中等
题目链接
示例
示例 1:
输入: [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
最少花费: 2 + 5 + 3 = 10。
示例 2:
输入: [[7,6,2]]
输出: 2
提示
- 所有花费均为正整数。
costs.length <= 100
costs[i].length == 3
解题思路
方法:动态规划
这是一个经典的动态规划问题。我们需要为每个房子选择一种颜色,使得相邻房子的颜色不同,并且总花费最小。
关键点
- 每个房子有三种可能的颜色:红色(0)、蓝色(1)和绿色(2)
- 相邻房子的颜色不能相同
- 目标是找到最小的总花费
具体步骤
-
定义状态:
dp[i][j]
表示粉刷前 i 个房子且第 i 个房子使用颜色 j 的最小花费- j 的取值为 0, 1, 2,分别代表红色、蓝色和绿色
-
状态转移方程:
dp[i][0] = costs[i][0] + min(dp[i-1][1], dp[i-1][2])
dp[i][1] = costs[i][1] + min(dp[i-1][0], dp[i-1][2])
dp[i][2] = costs[i][2] + min(dp[i-1][0], dp[i-1][1])
-
初始状态:
dp[0][0] = costs[0][0]
dp[0][1] = costs[0][1]
dp[0][2] = costs[0][2]
-
最终结果:
min(dp[n-1][0], dp[n-1][1], dp[n-1][2])
-
优化:我们可以进一步优化空间复杂度,因为当前状态只依赖于前一个状态,所以可以只用三个变量来存储状态。
复杂度分析
- 时间复杂度:O(n),其中 n 是房子的数量。我们需要遍历每个房子一次。
- 空间复杂度:O(1),使用常数级别的额外空间。
图解思路
动态规划状态转移表
房子序号 | 选择红色 | 选择蓝色 | 选择绿色 | 解释 |
---|---|---|---|---|
0 | costs[0][0] | costs[0][1] | costs[0][2] | 初始状态,分别为第0个房子涂三种颜色的成本 |
1 | costs[1][0] + min(dp[0][1], dp[0][2]) | costs[1][1] + min(dp[0][0], dp[0][2]) | costs[1][2] + min(dp[0][0], dp[0][1]) | 第1个房子的选择依赖于第0个房子的最优选择 |
… | … | … | … | … |
n-1 | costs[n-1][0] + min(dp[n-2][1], dp[n-2][2]) | costs[n-1][1] + min(dp[n-2][0], dp[n-2][2]) | costs[n-1][2] + min(dp[n-2][0], dp[n-2][1]) | 最后一个房子的选择依赖于前一个房子的最优选择 |
示例分析表
以示例1为例,costs = [[17,2,17],[16,16,5],[14,3,19]]
:
房子序号 | 选择红色(0) | 选择蓝色(1) | 选择绿色(2) | 最优选择 |
---|---|---|---|---|
0 | 17 | 2 | 17 | 蓝色(1),成本为2 |
1 | 16 + min(2, 17) = 18 | 16 + min(17, 17) = 33 | 5 + min(17, 2) = 7 | 绿色(2),成本为7 |
2 | 14 + min(33, 7) = 21 | 3 + min(18, 7) = 10 | 19 + min(18, 33) = 37 | 蓝色(1),成本为10 |
最终结果为10,对应的选择是:房子0选蓝色,房子1选绿色,房子2选蓝色。
代码实现
C# 实现
public class Solution {
public int MinCost(int[][] costs) {
if (costs == null || costs.Length == 0) return 0;
int n = costs.Length;
// 使用滚动数组优化空间
int[] dp = new int[3];
dp[0] = costs[0][0];
dp[1] = costs[0][1];
dp[2] = costs[0][2];
for (int i = 1; i < n; i++) {
int[] temp = new int[3];
// 更新每种颜色的最小花费
temp[0] = costs[i][0] + Math.Min(dp[1], dp[2]);
temp[1] = costs[i][1] + Math.Min(dp[0], dp[2]);
temp[2] = costs[i][2] + Math.Min(dp[0], dp[1]);
// 更新dp数组
dp = temp;
}
// 返回最后一个房子的最小花费
return Math.Min(dp[0], Math.Min(dp[1], dp[2]));
}
}
Python 实现
class Solution:
def minCost(self, costs: List[List[int]]) -> int:
if not costs:
return 0
n = len(costs)
# 初始化dp数组
dp = costs[0].copy()
for i in range(1, n):
new_dp = [0] * 3
# 更新每种颜色的最小花费
new_dp[0] = costs[i][0] + min(dp[1], dp[2])
new_dp[1] = costs[i][1] + min(dp[0], dp[2])
new_dp[2] = costs[i][2] + min(dp[0], dp[1])
dp = new_dp
# 返回最后一个房子的最小花费
return min(dp)
C++ 实现
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
if (costs.empty()) return 0;
int n = costs.size();
// 使用滚动数组优化空间
vector<int> dp = costs[0];
for (int i = 1; i < n; i++) {
vector<int> temp(3);
// 更新每种颜色的最小花费
temp[0] = costs[i][0] + min(dp[1], dp[2]);
temp[1] = costs[i][1] + min(dp[0], dp[2]);
temp[2] = costs[i][2] + min(dp[0], dp[1]);
dp = temp;
}
// 返回最后一个房子的最小花费
return *min_element(dp.begin(), dp.end());
}
};
执行结果
C# 实现
- 执行用时:92 ms
- 内存消耗:39.4 MB
Python 实现
- 执行用时:40 ms
- 内存消耗:14.9 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:9.8 MB
性能对比
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 92 ms | 39.4 MB | 语法简洁,性能适中 |
Python | 40 ms | 14.9 MB | 实现最简单,代码量最少 |
C++ | 4 ms | 9.8 MB | 性能最佳,内存占用最小 |
代码亮点
- 🎯 使用动态规划算法高效解决房屋粉刷问题,时间复杂度为O(n)
- 💡 通过滚动数组优化空间复杂度至O(1),只需保存前一个状态
- 🔍 巧妙利用min函数避免了大量的if-else判断,使代码更简洁
- 🎨 状态转移方程清晰明了,逻辑直观易懂
常见错误分析
- 🚫 忘记处理输入数组为空的边界情况
- 🚫 状态转移方程写错,错误地使用相同颜色的前一个状态
- 🚫 忘记在循环内更新dp数组,导致状态无法正确传递
- 🚫 使用二维数组但没有必要,增加了空间复杂度
解法对比
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
动态规划(二维) | O(n) | O(n) | 思路直观,容易理解 | 空间使用较多 |
动态规划(一维) | O(n) | O(1) | 空间优化,效率高 | 代码稍复杂 |
递归(记忆化) | O(n) | O(n) | 编码简单 | 有栈溢出风险,效率低 |
贪心 | - | - | - | 不适用于此问题 |
相关题目
- LeetCode 265. 粉刷房子 II - 困难
- LeetCode 276. 栅栏涂色 - 中等
- LeetCode 198. 打家劫舍 - 中等
- LeetCode 213. 打家劫舍 II - 中等