动态规划
文章目录
核心 是状态定义和状态转移方程
70. 爬楼梯

f[i] 表示爬到第 i 阶楼梯总共有多少种方法
java
class Solution {
public int climbStairs(int n) {
int[] f = new int[n+1];
f[0] = f[1] = 1;
for(int i = 2;i <= n;i++){
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
}
118. 杨辉三角
java
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> ans = new ArrayList<>();
for(int i = 0;i < numRows;i++){
List<Integer> row = new ArrayList<>();
for(int j = 0;j <= i;j++){
if(j == 0 || j == i){
row.add(1);
}else{
row.add(ans.get(i-1).get(j-1) + ans.get(i-1).get(j));
}
}
ans.add(row);
}
return ans;
}
}
198. 打家劫舍

java
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
int length = nums.length;
if(length == 1)return nums[0];
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < length ;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[length-1];
}
}
279. 完全平方数
f[i] 表示:凑成数字 i 最少需要几个完全平方数
f i = m i n ( f i − j 2 ) + 1 fi=min(fi−j\^2)+1 fi=min(fi−j2)+1
-
外层循环
从小到大逐个得出每个数字的最少平方和个数
-
内层循环
尝试所有不超过i的完全平方数,找出最优的那个
java
class Solution {
public int numSquares(int n) {
int[] f = new int[n+1];
for(int i = 1;i <= n;i++){
int tmp = Integer.MAX_VALUE;
for(int j = 1;j*j<=i;j++){
tmp = Math.min(f[i-j*j],tmp);
}
f[i] = tmp + 1;
}
return f[n];
}
}
322. 零钱兑换
初识化所有格子为无穷大,表示还没找到解

该算法采用自底向上的动态规划 策略,核心逻辑是建立一个数组 dp,其中 dp[i] 代表凑成金额 i 所需的最少硬币数。它从金额 1 开始逐步推导至目标金额 amount,对于每一个金额 i,尝试减去每一种硬币的面值 coin,利用之前计算好的子问题结果 dp[i - coin] 加上当前这一枚硬币(即 1 + dp[i - coin])来更新当前金额的最小硬币数。最终,如果 dp[amount] 仍为初始的无穷大值则说明无法凑出,否则该值即为所求的最少硬币数量。
java
class Solution {
public int coinChange(int[] coins, int amount) {
//如果金额为0或为负数,直接返回0
if(amount < 1){
return 0;
}
//dp[i]表示凑成金额i所需的最少硬币数
//使用amount+1作为无穷大的占位符
int[] dp = new int[amount + 1];
Arrays.fill(dp,amount + 1);
dp[0] = 0;
//外层循环:从1遍历到目标金额amount
for(int i = 1;i <= amount;i++){
//内层循环:尝试每一种硬币
for(int coin : coins){
// 只有当硬币面值小于等于当前金额时,才可能使用该硬币
if(coin <= i){
dp[i] = Math.min(dp[i],1 + dp[i - coin]);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
139.单词拆分
核心思想是将字符串看作一个序列,通过判断"前缀是否可被拆分"以及"剩余后缀是否在字典中"来逐步推导整个字符串是否合法
将输入的 List 转换为 HashSet。这是为了优化查找效率。在 List 中查找一个单词的时间复杂度是 O(N) ,而在 HashSet 中查找的时间复杂度接近 O(1)
dp[i] 表示字符串 s 的前 i 个字符(即索引 0 到 i-1 的子串 s[0...i-1])是否可以被字典中的单词拼接而成
外层循环:遍历字符串长度
内层循环:寻找分割点
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
for(int i=0;i<=s.length();i++){
for(int j=0;j<i;j++){
if(dp[j]&&set.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
300. 最长递增子序列
核心逻辑是:对于数组中的每一个数字,都尝试把它接在之前所有比它小的数字后面,看看能不能形成更长的递增序列
dp[i] 的含义是:以 nums[i] 这个元素结尾的最长严格递增子序列的长度
外层循环:遍历数组中的每一个元素numsi
内层循环:遍历i之前所有的元素
状态转移 :dp[i] = Math.max(dp[i], dp[j] + 1)
- 尝试把
nums[i]接在以nums[j]结尾的序列后面。 - 新的长度是
dp[j] + 1
java
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0){
return 0;
}
int[] dp = new int[nums.length];
dp[0] = 1;
int ans = 1;
for(int i = 1;i < nums.length;i++){
dp[i] = 1;
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
ans = Math.max(ans,dp[i]);
}
return ans;
}
}
152. 乘积最大子数组

要解决这个问题,必须同时维护两个状态:
maxF:以当前元素结尾的最大乘积。minF:以当前元素结尾的最小乘积(为了处理负负得正的情况)
java
class Solution {
public int maxProduct(int[] nums) {
if(nums == null||nums.length == 0)return 0;
int maxF = nums[0];
int minF = nums[0];
int ans = nums[0];
for(int i=1;i < nums.length;i++){
if(nums[i] > 0){
maxF *= nums[i];
minF *= nums[i];
}else if(nums[i] < 0){
int tmp = maxF;
maxF = minF * nums[i];
minF = tmp * nums[i];
}else{
//当前为0,全部归零
maxF = 0;
minF = 0;
}
// 如果前面的累积不如当前数本身,就从当前数重新开始
maxF = Math.max(nums[i],maxF);
minF = Math.min(nums[i],minF);
ans = Math.max(ans,maxF);
}
return ans;
}
}
416. 分割等和子集
子集不要求连续,前缀和只能处理连续片段,所以这个方法从根上就行不通
正确解法:
能否从数组中选出若干个数,使它们的和恰好等于总和的一半
java
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num : nums){
sum += num;
}
//总和为奇数,不可能平分
if(sum % 2 == 1)return false;
int target = sum / 2;
//dp[i]表示能否凑出和为i的子集
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for(int num : nums){//外层循环:遍历每个数字
for(int i = target;i >= num;i--){//内层循环:从target倒退num 防止num重复使用
dp[i] = dp[i] || dp[i - num];//不选或选当前数字
}
}
return dp[target];
}
}
32. 最长有效括号
栈 + 配对标记
从左到右遍历字符串 s,对于右括号,找左侧最近的未配对的左括号,然后把这一对括号标记为「已配对」。
为方便找到左侧最近的未配对的左括号,我们可以用一个栈保存遍历过的左括号的下标:
遇到左括号,把其下标入栈。
遇到右括号,且栈非空,那么弹出栈顶。这样遍历到下一个右括号时,栈顶总是最近的未配对的左括号的下标。
最后,最长连续已配对标记的长度,就是最长有效括号的长度
java
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
boolean[] isValid = new boolean[n];
Deque<Integer> st = new ArrayDeque<>();
//标记哪些括号是配对的
for(int i = 0;i < n;i++){
if(s.charAt(i) == '('){
st.push(i);//(入栈
}else if(!st.isEmpty()){
isValid[i] = isValid[st.pop()] = true;//匹配到(,(出栈,把两个括号都标记为已配对
}
}
int ans = 0;
int tmp = 0;
//找到被标记的最长连续
for(boolean b : isValid){
if(b){
tmp++;
ans = Math.max(ans,tmp);
}else{
tmp = 0;
}
}
return ans;
}
}