引言
动态规划是算法面试中的"拦路虎",许多初学者不知从何下手。今天讲解的「力扣 91. 粉刷房子」正是 DP 入门的绝佳练习题。它不像背包问题需要纠结"容量"维度,而是用最朴素的二维 DP 表格,清晰展示了状态定义、初始化、转移和返回的完整流程。无论你是算法新手还是面试备战者,这篇文章都会带你一步步拆解题目,让你真正理解 DP 的核心思想。让我们从一道题开始,打通动态规划的"任督二脉"

摘要
本文详细解析力扣 91「粉刷房子」的 DP 解法。给定 n×3 成本矩阵,求相邻颜色不同时的最小总花费。定义 dp[i][j] 为第 i 个房子刷颜色 j 的最小花费,转移方程 dp[i][j]=costs[i][j]+min(dp[i-1][k]) (k≠j)。通过示例手动推导 DP 表格,并提供二维数组和 O(1) 滚动数组两种代码实现。重点总结三个易错点:维度理解、返回值、三数取最小值。时间 O(n),空间可优化至 O(1)
目录
[1. 为什么用 DP?](#1. 为什么用 DP?)
[2. DP 数组的定义](#2. DP 数组的定义)
[3. DP 数组的构造(以示例为例)](#3. DP 数组的构造(以示例为例))
[4. 状态转移方程](#4. 状态转移方程)
[三、Java 代码实现](#三、Java 代码实现)
[⚠️ 注意点 1:DP 数组的构造维度](#⚠️ 注意点 1:DP 数组的构造维度)
[⚠️ 注意点 2:返回值不是 dpn-12](#⚠️ 注意点 2:返回值不是 dp[n-1][2])
[⚠️ 注意点 3:三个数取最小值的写法](#⚠️ 注意点 3:三个数取最小值的写法)
一、题目描述
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
示例 1:
输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
最少花费: 2 + 5 + 3 = 10。
示例 2:
输入: costs = [[7,6,2]]
输出: 2
二、动态规划思路
1. 为什么用 DP?
这道题满足最优子结构 :
第 i 个房子刷某种颜色的最小花费,只依赖于第 i-1 个房子刷其他两种颜色 的最小花费。
因此我们可以用动态规划,从前往后依次推导。
2. DP 数组的定义
我们定义一个二维数组 dp:
java
dp[i][j]:表示粉刷完前 i 个房子(0 ~ i),且第 i 个房子刷成颜色 j 时的最小总花费。
其中:
-
i表示房子编号,范围0 ~ n-1 -
j表示颜色,0=红色,1=蓝色,2=绿色
3. DP 数组的构造(以示例为例)
输入:
java
costs = [[17,2,17],
[16,16,5],
[14,3,19]]
我们手动构造出 dp 数组:
| 房子 \ 颜色 | 红色 | 蓝色 | 绿色 |
|---|---|---|---|
| 0号房子 | 17 | 2 | 17 |
| 1号房子 | 18 | 33 | 7 |
| 2号房子 | 21 | 10 | 37 |
推导过程:
-
初始化第一行 :
第 0 号房子刷任意颜色,花费就是它本身的成本。
→
dp[0] = [17, 2, 17] -
第二行(1号房子):
-
刷红色:
16 + min(dp[0][1], dp[0][2]) = 16 + min(2,17) = 18 -
刷蓝色:
16 + min(dp[0][0], dp[0][2]) = 16 + min(17,17) = 33 -
刷绿色:
5 + min(dp[0][0], dp[0][1]) = 5 + min(17,2) = 7
-
-
第三行(2号房子):
-
刷红色:
14 + min(dp[1][1], dp[1][2]) = 14 + min(33,7) = 21 -
刷蓝色:
3 + min(dp[1][0], dp[1][2]) = 3 + min(18,7) = 10 -
刷绿色:
19 + min(dp[1][0], dp[1][1]) = 19 + min(18,33) = 37
-
最终,最后一个房子(2号房子)的最小花费是:
java
min(21, 10, 37) = 10
4. 状态转移方程
java
dp[i][j] = costs[i][j] + min(dp[i-1][k]) 其中 k ≠ j
也就是说:当前房子刷颜色 j 的总花费 = 当前房子刷颜色 j 的成本 + 上一个房子刷另外两种颜色的较小值。
三、Java 代码实现
java
public class Main {
public static void main(String[] args) {
int[][] costs = {{17, 2, 17}, {16, 16, 5}, {14, 3, 19}};
System.out.println(minCost(costs)); // 输出 10
}
public static int minCost(int[][] costs) {
int M = costs.length; // 房子数量
int N = 3; // 颜色数量(红、蓝、绿)
// dp[i][j]:前 i 个房子,第 i 个房子刷颜色 j 的最小总花费
int[][] dp = new int[M][N];
// 1. 初始化第一行
for (int j = 0; j < N; j++) {
dp[0][j] = costs[0][j];
}
// 2. 从第二个房子开始递推
for (int i = 1; i < M; i++) {
for (int j = 0; j < N; j++) {
int prevMin;
if (j == 0) { // 当前刷红色,上一个只能是蓝色或绿色
prevMin = Math.min(dp[i-1][1], dp[i-1][2]);
} else if (j == 1) { // 当前刷蓝色,上一个只能是红色或绿色
prevMin = Math.min(dp[i-1][0], dp[i-1][2]);
} else { // 当前刷绿色,上一个只能是红色或蓝色
prevMin = Math.min(dp[i-1][0], dp[i-1][1]);
}
dp[i][j] = costs[i][j] + prevMin;
}
}
// 3. 返回最后一个房子的最小花费
return Math.min(dp[M-1][0], Math.min(dp[M-1][1], dp[M-1][2]));
}
}
运行结果:

四、代码优化(空间压缩)
因为 dp[i] 只依赖于 dp[i-1],我们可以用一维数组 滚动更新,降低空间复杂度到 O(1):
java
public static int minCost(int[][] costs) {
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 prev0 = dp[0], prev1 = dp[1], prev2 = dp[2];
dp[0] = costs[i][0] + Math.min(prev1, prev2);
dp[1] = costs[i][1] + Math.min(prev0, prev2);
dp[2] = costs[i][2] + Math.min(prev0, prev1);
}
return Math.min(dp[0], Math.min(dp[1], dp[2]));
}
五、易错点总结(特别重要)
⚠️ 注意点 1:DP 数组的构造维度
本题虽然只有一个"房子数量"维度,但因为每个状态有 3 种颜色选择,所以用二维数组 dp[n][3] 来记录。
不要误以为需要"物品 + 容量"两个维度,那是 01 背包的思路,这里没有容量限制。
⚠️ 注意点 2:返回值不是 dp[n-1][2]
很多同学想当然地认为最后一个元素就是答案,但这是错误的!
最后一个房子有三种可能颜色,应该取三种颜色中的最小值:
return Math.min(dp[M-1][0], Math.min(dp[M-1][1], dp[M-1][2]));
⚠️ 注意点 3:三个数取最小值的写法
Java 中 Math.min() 只支持两个参数,取三个数最小值要嵌套:
Math.min(a, Math.min(b, c))
六、复杂度分析
-
时间复杂度 :
O(n * 3) = O(n),只需要遍历每个房子一次 -
空间复杂度:
-
二维数组版:
O(n * 3) = O(n) -
一维滚动数组版:
O(1)
-
总结
这道题是动态规划入门的经典题目,核心思想是:
-
定义
dp[i][j]表示第i个房子刷颜色j时的最小花费 -
状态转移只依赖于前一个房子的两种颜色
-
最后取最后一个房子的三种颜色中的最小值
掌握了这道题,后续遇到"打家劫舍"、"股票买卖"等经典 DP 问题,思路也会更加清晰。
希望这篇文章能帮助你更好地理解动态规划!如果有问题,欢迎留言讨论 🚀