力扣 LCR 091. 粉刷房子 —— 动态规划入门详解

引言

动态规划是算法面试中的"拦路虎",许多初学者不知从何下手。今天讲解的「力扣 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?

2. DP 数组的定义

3. DP 数组的构造(以示例为例)

4. 状态转移方程

三、Java 代码实现

四、代码优化(空间压缩)

五、易错点总结(特别重要)

⚠️ 注意点 1:DP 数组的构造维度

⚠️ 注意点 2:返回值不是 dp[n-1][2]

⚠️ 注意点 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

dp[i][j]:表示粉刷完前 i 个房子(0 ~ i),且第 i 个房子刷成颜色 j 时的最小总花费。

其中:

  • i表示房子编号,范围0 ~ n-1

  • j表示颜色,0=红色,1=蓝色,2=绿色

3. DP 数组的构造(以示例为例)

输入:

costs = [[17,2,17], [16,16,5], [14,3,19]]

我们手动构造出dp数组:

房子 \ 颜色红色蓝色绿色
0号房子17217
1号房子18337
2号房子211037

推导过程:

  • 初始化第一行
    第 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号房子)的最小花费是:

min(21, 10, 37) = 10

4. 状态转移方程

dp[i][j] = costs[i][j] + min(dp[i-1][k]) 其中 k ≠ j

也就是说:当前房子刷颜色j的总花费 = 当前房子刷颜色j的成本 + 上一个房子刷另外两种颜色的较小值。


三、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)

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)


总结

这道题是动态规划入门的经典题目,核心思想是:

  1. 定义dp[i][j]表示第i个房子刷颜色j时的最小花费

  2. 状态转移只依赖于前一个房子的两种颜色

  3. 最后取最后一个房子的三种颜色中的最小值

掌握了这道题,后续遇到“打家劫舍”、“股票买卖”等经典 DP 问题,思路也会更加清晰。


希望这篇文章能帮助你更好地理解动态规划!如果有问题,欢迎留言讨论 🚀