hot100-动态规划
70. 爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
118. 杨辉三角
给定一个非负整数 numRows
**,生成「杨辉三角」的前 numRows
行。在「杨辉三角」中,每个数是它左上方和右上方的数的和。
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路:和爬楼梯基本一致,dp[i]表示偷窃前i个房屋的最大金额。对于下标为i房屋,第i+1个房屋,有两种选择,偷或不偷。①如果不偷则最大值为dp[i];如果偷则第i个房屋不能偷,即dp[i-1]+nums[i],二者取最大值。
class Solution { public int rob(int[] nums) { int n = nums.length; int[] dp = new int[n + 1]; dp[0] = 0; dp[1] = nums[0]; for (int i = 1; i < n; i++) { dp[i + 1] = Math.max(dp[i], dp[i - 1] + nums[i]); } return dp[n]; } }
class Solution {
public List<List<Integer>> generate(int numRows) {
// 创建一个二维列表 res,用于存储杨辉三角的每一行
List<List<Integer>> res = new ArrayList<>();
// 创建一个数组 nums,用于存储上一行的数字(初始为空)
Integer[] nums = new Integer[numRows];
// 外层循环:逐行生成杨辉三角,从第 0 行到第 numRows-1 行
for (int i = 0; i < numRows; i++) {
// 创建一个数组 nums2,用于存储当前行的数字
Integer[] nums2 = new Integer[i + 1];
// 设置当前行的第一个数字为 1
nums2[0] = 1;
// 设置当前行的最后一个数字为 1
nums2[i] = 1;
// 内层循环:计算当前行中间的数字(从第 1 个到第 i-1 个)
for (int j = 1; j < i; j++) {
// 当前行的第 j 个数字等于上一行的第 j-1 个数字和第 j 个数字之和
nums2[j] = nums[j - 1] + nums[j];
}
// 更新 nums,使其指向当前行的数组 nums2
nums = nums2;
// 将当前行的数字列表添加到结果列表 res 中
res.add(Arrays.asList(nums));
}
// 返回生成的杨辉三角
return res;
}
}
279. 完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
思路:dp[i]表示和为i的完全平方数的最少数量,i是由不同的数组成,因此需要计算出小于i的所有数的完全平方数的最少数量,当遍历到i时,从1枚举所有平方小于等于i的数j,取所有dp[i-j*j]+1
中的最小值即可
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1]; // dp[i] 表示凑成整数 i 所需的最小完全平方数数量
Arrays.fill(dp, Integer.MAX_VALUE); // 初始化为一个较大的值
dp[0] = 0; // 凑成 0 不需要任何完全平方数
// 遍历每个整数 i
for (int i = 1; i <= n; i++) {
// 对于每个 i,尝试用所有可能的完全平方数 j * j 来凑成它
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
322. 零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
思路:dp[i]表示兑换金额为i需要的硬币数量,i只能由coins中的数组合而成,因此只需要直到i-coins[j]金额所需要的最少硬币数量即可,而j需要进行一次遍历
class Solution {
public int coinChange(int[] coins, int amount) {
if(amount == 0){
return 0;
}
int n = coins.length;
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 初始化为一个较大的值
dp[0] = 0; // 金额为0时,不需要硬币
for(int i = 0; i < n ; i++){
//正序遍历:完全背包每个硬币可以选择多次
for(int j = coins[i]; j <= amount; j++){
//选择硬币数目最小的情况
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
// 如果 dp[amount] 仍然是初始值,说明无法凑出目标金额
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
139. 单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
思路:dp[i] 表示字符串 s 长度为 i 的字串能否由字典组成,每增加一个长度就要检查所有满足条件的字典中的单词
300. 最长递增子序列
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // 初始化:空字符串可以被拆分
// 使用 HashSet 存储字典单词,便于快速查找
HashSet<String> set = new HashSet<>(wordDict);
// 遍历每个位置 i
for (int i = 1; i <= n; i++) {
// 尝试从 i 向前查找长度为 j 的子字符串
for (int j = 1; j <= i; j++) {
// 如果前 i - j 个字符可以被拆分,并且子字符串在字典中
if (dp[i - j] && set.contains(s.substring(i - j, i))) {
dp[i] = true; // 更新 dp[i]
break; // 找到一个有效拆分即可
}
}
}
return dp[n]; // 返回最终结果
}
}
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
思路:
-
明确状态定义
动态规划的核心是状态定义。对于LIS问题,我们定义:
-
dp[i]
表示以nums[i]
结尾的最长递增子序列的长度。
-
状态转移方程
-
对于每个
i
,遍历所有j
(0 ≤ j < i
),如果nums[i] > nums[j]
,则说明nums[i]
可以接在nums[j]
后面形成一个更长的递增子序列。 -
因此,状态转移方程为:
dp[i] = Math.max(dp[i], dp[j] + 1)
这个方程的含义是:
-
如果
nums[i]
可以接在nums[j]
后面,那么以nums[i]
结尾的递增子序列长度至少为dp[j] + 1
。 -
我们需要遍历所有可能的
j
,取其中的最大值作为dp[i]
的值。
-
初始化:每个元素自身可以构成长度为1的递增子序列,因此:
dp[i] = 1
(对所有i
)。
-
遍历顺序
动态规划的遍历顺序必须确保在计算dp[i]
时,所有相关的dp[j]
(j < i
)已经计算完成。对于LIS问题:
-
外层循环从
i = 1
到n-1
(数组长度为n
)。 -
内层循环从
j = 0
到i-1
。
-
维护全局最优解
在动态规划过程中,我们不仅要计算每个dp[i]
,还需要维护一个全局最优解。对于LIS问题:
-
在每次更新
dp[i]
时,同步更新全局最长递增子序列的长度result
:result = Math.max(result, dp[i])
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if(nums.length == 0){
return 0;
}
// 初始化
int[] dp = new int[len];
for(int i = 0; i < len; i++){
dp[i] = 1;
}
int result = 1;
for(int i = 1; i < len; i++){
for(int j = 0; j < i; j++){
// 如果nums[i]可以接在nums[j]后面,那么以nums[i]结尾的递增子序列长度至少为dp[j] + 1
if (nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 在每次更新dp[i]时,同步更新全局最长递增子序列的长度result
result = Math.max(result, dp[i]);
}
return result;
}
}
152. 乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
思路:由于负数的存在,可能会导致最小值变成最大值,因此在动态规划时同时维护最小值和最大值。
class Solution {
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE, imax = 1, imin = 1;
for(int i = 0; i < nums.length; i++){
// 如果当前数字是负数,最大值和最小值会互换
// 因为负数乘以最小值可能变成最大值,反之亦然
if(nums[i] < 0){
int temp = imax;
imax = imin;
imin = temp;
}
// 更新当前最大乘积
// 取当前数字和当前数字乘以之前的最大乘积之间的较大值
imax = Math.max(imax * nums[i], nums[i]);
// 更新当前最小乘积
// 取当前数字和当前数字乘以之前的最小乘积之间的较小值
imin = Math.min(imin * nums[i], nums[i]);
// 更新全局最大乘积
// 只需要比较当前的最大乘积(imax),因为最大乘积不会出现在最小乘积中
max = Math.max(max, imax);
}
return max;
}
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:要有两个相等的子集,说明数组总和肯定为偶数,如果为偶数,则目标为找到一个子集,其和为sum / 2.
动态规划,dp[j]
表示容量为j
的背包能够达到的最大和,初始时,dp[j]
全部设为0。
动态规划状态转移:
-
遍历数组中的每个数字
nums[i]
。 -
对于每个数字,从后往前更新
dp
数组,避免重复使用同一个数字。 -
对于每个容量
j
,更新dp[j]
为以下两种情况的较大值:-
不选择当前数字
nums[i]
,即dp[j]
保持不变。 -
选择当前数字
nums[i]
,即dp[j]
更新为dp[j - nums[i]] + nums[i]
。
-
提前返回:如果在某次更新后dp[target]
已经等于target
,说明已经找到一个和为target
的子集,直接返回true
。
最终检查:如果遍历结束后dp[target]
等于target
,说明可以找到一个和为target
的子集,返回true
;否则返回false
。
public class Solution {
public boolean canPartition(int[] nums) {
if (nums == null || nums.length == 0) return false;
int n = nums.length;
// 计算数组的总和
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和为奇数,无法平分成两个和相等的子集,直接返回 false
if (sum % 2 != 0) return false;
// 目标是找到一个子集,其和为总和的一半
int target = sum / 2;
// 初始化动态规划数组
// dp[j] 表示容量为 j 的背包能够达到的最大和
int[] dp = new int[target + 1];
// 遍历数组中的每个数字
for (int i = 0; i < n; i++) {
// 从后往前更新 dp 数组,避免重复使用同一个数字
for (int j = target; j >= nums[i]; j--) {
// 更新 dp[j],表示当前容量下能达到的最大和
// 选择当前数字 nums[i] 或不选择当前数字,取两者中的较大值
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
// 如果在某次更新后 dp[target] 已经达到目标值,直接返回 true
if (dp[target] == target) return true;
}
// 最后检查 dp[target] 是否等于目标值 target
// 如果等于 target,说明可以找到一个子集,其和为总和的一半
return dp[target] == target;
}
}
32. 最长有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
思路:
哨兵值-1
用于处理边界情况,例如字符串以)
开头的情况。
栈的作用:
-
栈中存储的是索引,而不是括号本身。
-
每次遇到
(
时,将索引压入栈中。 -
每次遇到
)
时,尝试弹出栈顶元素进行匹配。 -
如果栈为空,说明当前
)
没有匹配的(
,需要压入当前索引作为新的哨兵值。 -
如果栈不为空,栈顶元素是最近一个未匹配的索引或哨兵值,可以通过
i - stack.peek()
计算当前有效括号子串的长度。
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0; // 用于存储最长有效括号子串的长度
Deque<Integer> stack = new LinkedList<>(); // 使用栈存储索引
stack.push(-1); // 初始化栈,压入哨兵值 -1,用于处理边界情况
// 遍历字符串中的每个字符
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
// 如果当前字符是左括号 '('
// 将其索引压入栈中
stack.push(i);
} else {
// 如果当前字符是右括号 ')'
// 尝试弹出栈顶元素进行匹配
stack.pop();
if (stack.isEmpty()) {
// 如果栈为空,说明当前右括号 ')' 没有匹配的左括号 '('
// 将当前索引压入栈中,作为新的哨兵值
stack.push(i);
} else {
// 如果栈不为空,说明当前右括号 ')' 有匹配的左括号 '('
// 栈顶元素是最近一个未匹配的索引或哨兵值
// 计算当前有效括号子串的长度:当前索引 i - 栈顶索引 stack.peek()
maxans = Math.max(maxans, i - stack.peek());
}
}
}
// 返回最长有效括号子串的长度
return maxans;
}
}
标记数组+栈
相匹配到的括号在数组中对应的位置设置为1,计算最大连续1的长度
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
Deque<Integer> stack = new LinkedList<>();
int[] res = new int[n]; // 用于标记匹配的括号位置
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '(') {
stack.push(i); // 遇到左括号,压入索引
} else if (!stack.isEmpty()) {
// 遇到右括号且栈不为空,标记匹配的括号
res[i] = 1;
res[stack.pop()] = 1;
}
}
// 计算最长的连续1的长度
int maxans = 0, count = 0;
for (int i = 0; i < n; i++) {
if (res[i] == 1) {
count++; // 连续的匹配括号
} else {
maxans = Math.max(maxans, count); // 更新最大长度
count = 0; // 重置计数
}
}
maxans = Math.max(maxans, count); // 最后一次更新
return maxans;
}
}