力扣 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?](#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)


总结

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

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

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

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

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


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

相关推荐
CV-Climber4 小时前
检索技术的实际应用
人工智能·算法
hhzz4 小时前
Tiger AI Platform平台中增加人脸识别功能
图像处理·人工智能·算法·计算机视觉·大模型
从零开始的代码生活_5 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法
TsingtaoAI5 小时前
3D高斯泼溅技术发展及其在具身智能领域的应用综述
人工智能·算法·ai·具身智能·高斯泼溅
atunet6 小时前
关于图算法中的连通分量检测与最小割问题7
算法
心运软件6 小时前
银行客户流失预测(Python 完整实战)
算法
邪神与厨二病6 小时前
牛客周赛 Round 153
python·算法
qizayaoshuap8 小时前
# ❌ 井字棋 — 鸿蒙ArkTS Minimax AI算法与博弈系统设计
人工智能·算法·华为·harmonyos