二刷动态规划经典题:从打家劫舍到完全平方数,Java 实现复盘与优化

二刷 LeetCode 动态规划经典题,才真正体会到这类题目的核心魅力 ------ 从暴力递归到动态规划,再到空间优化,每一步都是对 "状态定义" 和 "状态转移" 的深度理解。今天复盘两道中等难度的题目:198. 打家劫舍 和 279. 完全平方数,分享我的解题思路、Java 实现以及优化过程。


一、198. 打家劫舍:入门级 DP 题,优化到极致的空间复杂度

题目回顾

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

核心思路

这是一道典型的一维动态规划入门题,核心在于定义状态并写出转移方程。

  1. 状态定义dp[i] 表示前 i 间房屋能偷窃到的最高金额。
  2. 状态转移方程 : 对于第 i 间房屋,有两种选择:
    • :那么第 i-1 间房屋不能偷,所以 dp[i] = dp[i-2] + nums[i]
    • 不偷 :那么最高金额就是前 i-1 间房屋的最高金额,即 dp[i] = dp[i-1] 所以,dp[i] = max(dp[i-1], dp[i-2] + nums[i])
  3. 边界条件
    • dp[0] = nums[0]
    • dp[1] = max(nums[0], nums[1])

Java 实现(基础版)

java

运行

复制代码
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        
        int n = nums.length;
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[n-1];
    }
}

空间优化版(O (1) 空间)

观察状态转移方程,dp[i] 只依赖于 dp[i-1]dp[i-2],因此我们不需要维护整个数组,只需用两个变量保存前两次的状态即可。

java

运行

复制代码
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        
        int prevPrev = nums[0];
        int prev = Math.max(nums[0], nums[1]);
        
        for (int i = 2; i < nums.length; i++) {
            int current = Math.max(prev, prevPrev + nums[i]);
            prevPrev = prev;
            prev = current;
        }
        return prev;
    }
}

二、279. 完全平方数:DP 与背包问题的结合

题目回顾

给你一个整数 n ,返回和为 n 的完全平方数的最少数量。

完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

核心思路

这道题可以转化为一个 "完全背包问题":

  • 背包容量为 n
  • 物品是所有小于等于 n 的完全平方数(如 1, 4, 9, ...)
  • 每个物品可以无限次使用
  • 目标是用最少的物品数量装满背包
  1. 状态定义dp[i] 表示和为 i 的最少完全平方数的个数。
  2. 状态转移方程 : 对于每个 i,我们遍历所有小于等于 i 的完全平方数 j*j,则: dp[i] = min(dp[i - j*j] + 1),其中 j*j <= i
  3. 边界条件dp[0] = 0,表示和为 0 时,需要 0 个完全平方数。

Java 实现(动态规划版)

java

运行

复制代码
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        // 初始化,最多用 n 个 1 相加
        for (int i = 1; i <= n; i++) {
            dp[i] = i;
        }
        
        for (int i = 1; i <= n; i++) {
            // 遍历所有小于等于 i 的完全平方数
            for (int j = 1; j * j <= i; j++) {
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }
}

三、二刷感悟:动态规划的核心在于 "状态"

通过二刷这两道题,我对动态规划有了更深的理解:

  1. 状态定义是灵魂:清晰的状态定义是写出转移方程的前提。
  2. 优化是必由之路 :从 O(n) 空间优化到 O(1),是对问题本质的进一步理解。
  3. 题型归类:很多 DP 题都可以归类为背包、线性 DP、区间 DP 等,掌握一类题的通用解法能事半功倍。

四、总结

  • 198. 打家劫舍:入门级线性 DP,重点是状态定义和空间优化。
  • 279. 完全平方数:完全背包问题的变种,重点是问题转化和状态转移。
相关推荐
阿Y加油吧1 小时前
二刷 LeetCode:爬楼梯与杨辉三角,Java 实现复盘
java·算法·leetcode
凌波粒1 小时前
LeetCode--101. 对称二叉树(二叉树)
算法·leetcode·职场和发展
_深海凉_1 小时前
LeetCode热题100-二叉树的最大深度
算法·leetcode·职场和发展
_深海凉_2 小时前
LeetCode热题100-二叉树的直径
算法·leetcode·职场和发展
水蓝烟雨2 小时前
3373. 连接两棵树后最大目标节点数目 II
算法·leetcode
YL200404262 小时前
044二叉搜索树中第K小的元素
数据结构·leetcode
_深海凉_3 小时前
LeetCode热题100-对称二叉树
算法·leetcode·职场和发展
运筹vivo@3 小时前
两数之和(leetcode)
算法·leetcode·职场和发展
Mr_pyx3 小时前
LeetCode Hot 100 - 最长递增子序列完全题解
算法·leetcode·职场和发展