二刷 LeetCode 动态规划经典题,才真正体会到这类题目的核心魅力 ------ 从暴力递归到动态规划,再到空间优化,每一步都是对 "状态定义" 和 "状态转移" 的深度理解。今天复盘两道中等难度的题目:198. 打家劫舍 和 279. 完全平方数,分享我的解题思路、Java 实现以及优化过程。
一、198. 打家劫舍:入门级 DP 题,优化到极致的空间复杂度
题目回顾
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
核心思路
这是一道典型的一维动态规划入门题,核心在于定义状态并写出转移方程。
- 状态定义 :
dp[i]表示前i间房屋能偷窃到的最高金额。 - 状态转移方程 : 对于第
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])
- 偷 :那么第
- 边界条件 :
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 的完全平方数的最少数量。
完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
核心思路
这道题可以转化为一个 "完全背包问题":
- 背包容量为
n - 物品是所有小于等于
n的完全平方数(如 1, 4, 9, ...) - 每个物品可以无限次使用
- 目标是用最少的物品数量装满背包
- 状态定义 :
dp[i]表示和为i的最少完全平方数的个数。 - 状态转移方程 : 对于每个
i,我们遍历所有小于等于i的完全平方数j*j,则:dp[i] = min(dp[i - j*j] + 1),其中j*j <= i - 边界条件 :
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];
}
}
三、二刷感悟:动态规划的核心在于 "状态"
通过二刷这两道题,我对动态规划有了更深的理解:
- 状态定义是灵魂:清晰的状态定义是写出转移方程的前提。
- 优化是必由之路 :从
O(n)空间优化到O(1),是对问题本质的进一步理解。 - 题型归类:很多 DP 题都可以归类为背包、线性 DP、区间 DP 等,掌握一类题的通用解法能事半功倍。
四、总结
- 198. 打家劫舍:入门级线性 DP,重点是状态定义和空间优化。
- 279. 完全平方数:完全背包问题的变种,重点是问题转化和状态转移。