动态规划问题专项练习(未编辑完成...

动态规划常见问题类型:

一、线性DP:

斐波那契数列

爬楼梯问题

最大子数组和

二、区间DP(这个其实可以直接放到字符串DP中的...):

1.石子合并
  • 转移表达式:dp[i][j] = min{dp[i][k] + dp[k+1][j] + cost(i, j)} (i ≤ k < j)

  • 具体转移表达式

    java 复制代码
    for (int k = i; k < j; k++) {
        int cost = dp[i][k] + dp[k+1][j] + (prefixSum[j+1] - prefixSum[i]);
        dp[i][j] = Math.min(dp[i][j], cost);
    }
  • 详细解释:

    1. 分割策略
      在区间 [i, j] 中选择分割点 k
      将区间分为 [i, k] [k+1, j]两部分
    2. 代价构成
      dp[i][k]: 合并左半部分 [i, k] 的代价
      dp[k+1][j]: 合并右半部分 [k+1, j] 的代价
      prefixSum[j+1] - prefixSum[i]: 合并完成后两堆石子合并的代价(即区间[i, j]的总和)
  • 详细代码

java 复制代码
public class StoneMerge {
    public static int mergeStones(int[] stones) {
        int n = stones.length;
        if (n <= 1) return 0;
        
        // 预处理:计算前缀和,用于快速计算区间和
        int[] prefixSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefixSum[i + 1] = prefixSum[i] + stones[i];
        }
        
        // dp[i][j] = 合并区间 [i, j] 的最小代价
        int[][] dp = new int[n][n];
        
        // 枚举区间长度
        for (int len = 2; len <= n; len++) {  // 长度从2开始
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1;
                dp[i][j] = Integer.MAX_VALUE;
                
                // 枚举分割点 k
                for (int k = i; k < j; k++) {
                    // 合并 [i, k] + 合并 [k+1, j] + 合并后的代价
                    int cost = dp[i][k] + dp[k + 1][j] + 
                              (prefixSum[j + 1] - prefixSum[i]);
                    dp[i][j] = Math.min(dp[i][j], cost);
                }
            }
        }
        
        return dp[0][n - 1];
    }
    
    public static void main(String[] args) {
        int[] stones = {3, 2, 4, 1};
        System.out.println("最小合并代价: " + mergeStones(stones)); // 20
    }
}
2.回文串分割

三、树形DP:

树的最大独立集

树的重心

四、背包问题:资源优化问题,关注约束下的最值

0-1背包 - 像买限量商品

  • 每种物品只有一个
  • 解法:二维DP,外循环物品,内循环背包容量(倒序)

完全背包 - 像自助餐厅打菜

  • 每种食物可以无限取
  • 解法:一维DP,外循环物品,内循环背包容量(正序)

多重背包 - 像超市购物有数量限制

  • 每种商品有固定数量
  • 解法:二进制优化或转为0-1背包
  • 例题:

    解法:
java 复制代码
import java.util.*;

//结果可以用TreeSet来存放,
//
class Solution {
    public int getDifferentWeights (int[] weights, int[] counts, int maxWeights) {
        boolean[] allPosibility = new boolean[maxWeights + 1];
        allPosibility[0] = true;


        
        //处理每个砝码
        for (int i = 0; i < weights.length; i++) {
            int weight = weights[i];
            int count = counts[i];

            //处理每种重量可能,从大往小判断可以避免再重复判断
            for (int j = maxWeights; j >= 0; j--) {
                //先判断该种可能是否已经可以走
                if(allPosibility[j]) {
                    //可以走再使用当前砝码不同数量去与最大重量比较,可以就可行
                    for (int k = 1; k <= count; k++) {
                        if ((j + weight * k) <= maxWeights) {
                            allPosibility[j + weight * k] = true;
                        } else {break;}
                    }
                }
                
            }
        }

        int result = 0;

        for (boolean b : allPosibility) {
            if (b) {
                result ++;
            }
        }

        return result;

        

    }
}

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
       
       int n = in.nextInt();
       
       int[] weights = new int[n];
       int[] counts = new int[n];

       int maxWeights = 0;

        //存储好砝码数据,同时得到最大重量
        for(int i = 0; i < n; i++) {
        weights[i] = in.nextInt();
       }
       for(int i = 0; i < n; i++) {
        counts[i] = in.nextInt();
       }

       for(int i = 0; i < n; i++) {
        maxWeights += weights[i] * counts[i];
       }

       Solution solution = new Solution();
       System.out.println(solution.getDifferentWeights(weights, counts, maxWeights));

       in.close();
    }
}

分组背包 - 像套餐选择

  • 每组物品中只能选一个
  • 解法:外循环组,中循环背包容量(倒序),内循环组内物品
  • 例题:
    解题:
java 复制代码
import java.util.*;

class Solution {
    public void getMax(ArrayList<ArrayList<int[]>> list, int[][] parents, int budget, int m) {
        ArrayList<ArrayList<int[]>> groups = new ArrayList<>();//存放每个主件的可能性list
        //ArrayList<int[]> options = new ArrayList<>();//存放每一个主件的可能性int数组

        for(int j = 1; j <= m; j ++) {
            ArrayList<int[]> options = new ArrayList<>();//存放每一个主件的可能性int数组,不能放外面否则所有主件的选项都被累积到了同一个列表中
            if(parents[j][0] != 0) {//如果是主件
            // 选项0:只买主件
                options.add(new int[] {parents[j][0],parents[j][0] * parents[j][1]});
            }


            ArrayList<int[]> aAttachment = list.get(j);
            // 选项1:主件+附件1
            if(aAttachment.size() >= 1){
                int[] aArray1 = aAttachment.get(0);
                options.add(new int[] {
                    parents[j][0] + aArray1[0],
                    parents[j][0] * parents[j][1] + aArray1[0] * aArray1[1]
                });
            }

						 // 选项2:主件+附件2
            if (aAttachment.size() >= 2) {
                int[] aArray2 = aAttachment.get(1);
                options.add(new int[] {
                    parents[j][0] + aArray2[0],
                    parents[j][0] * parents[j][1] + aArray2[0] * aArray2[1]
                });
            }

					// 选项3:主件+附件1+附件2
            if (aAttachment.size() >= 2) {
                int[] aArray1 = aAttachment.get(0);
                int[] aArray2 = aAttachment.get(1);
                options.add(new int[] {
                    parents[j][0] + aArray2[0] + aArray1[0],
                    parents[j][0] * parents[j][1] + aArray2[0] * aArray2[1] + aArray1[0] * aArray1[1]
                });
            }

            groups.add(options);

            

            

        }

        int[] dp = new int[budget + 1];

        for(ArrayList<int[]> group : groups) {
            if(group.isEmpty()) continue;
            for(int w = budget; w >= 0; w--) {
                for(int[] option : group){
                    int price = option[0];
                    int satisfaction = option[1];
                    if(price <= w) {
                    dp[w] = Math.max(dp[w], dp[w - price] + satisfaction);
                }
                }
                
            }
        }
        System.out.println(dp[budget]);





    }
}

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int budget = in.nextInt();//预算
        int count = in.nextInt();//物品件数

        ArrayList<ArrayList<int[]>> attachment = new ArrayList<>();
        int[][] mainGoods = new int[count+1][2];
        for(int i = 0; i <= count; i++) {
            attachment.add(new ArrayList<>());
        }
        

        for (int i = 1; i <= count; i++) { // 注意 while 处理多个 case

            int price = in.nextInt();
            int content = in.nextInt();
            int parent = in.nextInt();
            //添加主件
            if(parent == 0) {
                mainGoods[i][0] = price;
                mainGoods[i][1] = content;
            }else { //添加附件,不用存parent 直接看它所属的list的index就代表是哪一个主件的
                attachment.get(parent).add(new int[]{price, content});
            }
            
            //需要max来记录,需要target来判断价格是否超出,需要判断主附是否满足
        }

        Solution solution = new Solution();
        solution.getMax(attachment, mainGoods, budget, count);

        in.close();
    }
}

解法套路总结:

状态定义:dp[i][w] 表示前i个物品在容量w下的最优解

状态转移:考虑拿或不拿当前物品的决策

初始化:根据题目要求初始化边界条件

结果:通常是dp[n][capacity]

关键区别在于对物品数量的约束不同,导致遍历顺序状态转移略有差异。

五、字符串DP:序列分析问题,关注字符串的结构性质

解法步骤:

看到题目时:
先分析题目类型(匹配/子序列/分割/转换)

再选择合适的DP模型:
涉及两个字符串 → 优先考虑双序列型
涉及一个字符串的性质 → 单序列型或区间型
涉及区间操作 → 区间型

具体说明:

  1. 典型问题分类(先判断题目类型):
  • A.匹配类:KMP、字符串匹配、正则表达式
  • B.子序列类:LCS(最长公共子序列也可以是匹配类)、LIS(最长递增子序列)、不同路径变形
  • C.分割类:单词拆分、回文分割、有效IP、解码方法数石子合并
  • D.转换类:编辑距离、最小窗口、字符串重构、字符串变换、模式转换(只能用双序列模型来解决)
    • 识别标志
      1. 题目关键词:"最少...次数"、"最小...代价"、"...转换为..."、"编辑距离"
      2. 问题结构:有明确的源和目标、有可列举的操作集、求最优转换路径
  1. 字符串DP模型(选什么模型来解决这个题):
  • 双序列型:用两个维度 的状态解决两个字符串 的问题
    • A. 匹配类: 最长公共子序列、字符串相似度、正则表达式
    • D. 转换类: 编辑距离、字符串变换
  • 单序列型:用一个维度 的状态解决一个字符串 的问题
    • C. 分割类: 单词拆分、数字解码
    • A. 匹配类: KMP部分状态、单字符串模式识别
    • B. 子序列类: 最长回文子串、(最长)递增子序列
  • 区间型:用二维状态 解决字符串区间 的问题
    • B. 子序列类: (最长)回文子串(序列)、括号匹配
    • C. 分割类: 区间分割、矩阵链乘变形、 回文串分割
    • A. 匹配类: 区间内匹配统计
  1. 通用模型思路:

确定状态定义: 明确dp数组的含义

初始化边界: 处理长度为0或1的情况

状态转移: 根据问题特点写出转移方程

返回结果:根据dp数组返回最终答案

  1. 具体示例
  • 编辑距离:是转换类问题,用双序列型
    状态: dp[i][j] 表示s1前i个到s2前j个的最小编辑距离
    转移: 考虑s1[i-1]s2[j-1]的匹配情况

    java 复制代码
    	if(s1[i-1]==s2[j-1]) dp[i][j]=dp[i-1][j-1]
    	else dp[i][j] = min(删除, 插入, 替换) + 1 
  • 解码方法数: 是分割类问题,用单序列模型
    状态: dp[i] 表示字符串前i个字符的解码方法数
    转移: 考虑当前字符单独解码或与前一个字符组合解码

    java 复制代码
    	dp[i] = dp[i-1] (当 s[i-1] 可以单独解码时) 
      		+ dp[i-2] (当 s[i-2:i] 可以组合解码时)
  • 最长回文子串:子序列问题,考虑用区间模型
    状态: dp[i][j] 表示区间 [i, j] 的某种性质
    转移: 通常枚举区间长度,然后枚举起点,从小区间推导大区间

    java 复制代码
    	dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
  1. 注意要点:
  • 注意索引对应关系: dp[i][j]对应s1[i-1]s2[j-1]
  • 边界处理要仔细: 空字符串的情况
  • 转移条件要完整: 相等不相等等各种情况
  • 结果返回要准确: 通常是dp[m][n]max/min
具体试题:
1.最长公共子序列(LCS)
2.编辑距离
  • 状态表达式:

    java 复制代码
    if (s1[i-1] == s2[j-1])
    	dp[i][j] = dp[i-1][j-1]           // 字符相同,无需操作
    else
        dp[i][j] = min(
            dp[i-1][j] + 1,               // 删除 s1[i-1]
            dp[i][j-1] + 1,               // 插入 s2[j-1] 
            dp[i-1][j-1] + 1              // 替换 s1[i-1] 为 s2[j-1]
        )
  • 详细表达式

    java 复制代码
    // 初始化边界条件
    for (int i = 0; i <= m; i++) dp[i][0] = i;  // s1前i个变为空串需i次删除
    for (int j = 0; j <= n; j++) dp[0][j] = j;  // 空串变为s2前j个需j次插入
    
    // 状态转移
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s1.charAt(i-1) == s2.charAt(j-1)) {
                dp[i][j] = dp[i-1][j-1];          // 字符相同,无操作
            } else {
                dp[i][j] = Math.min(
                    Math.min(dp[i-1][j] + 1,      // 删除:s1删除当前字符
                             dp[i][j-1] + 1),     // 插入:s1插入s2当前字符
                             dp[i-1][j-1] + 1);   // 替换:替换s1当前字符为s2当前字符
            }
        }
    }
  • 详细代码

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();
        String t = scanner.nextLine();
        scanner.close();
        
        int result = minEditDistance(s, t);
        System.out.println(result);
    }
    
    public static int minEditDistance(String s, String t) {
        int m = s.length();
        int n = t.length();
        
        // dp[i][j] 表示 s 的前 i 个字符转换为 t 的前 j 个字符所需的最小编辑距离
        int[][] dp = new int[m + 1][n + 1];
        
        // 初始化边界条件
        // s 的前 0 个字符转换为 t 的前 j 个字符,需要 j 次插入操作
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j;
        }
        
        // s 的前 i 个字符转换为 t 的前 0 个字符,需要 i 次删除操作
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i;
        }
        
        // 填充 dp 表
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    // 字符相同,不需要操作
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 字符不同,需要操作
                    // 三种操作的最小值:插入、删除、替换
                    dp[i][j] = Math.min(
                        Math.min(dp[i - 1][j] + 1,      // 删除 s[i-1]
                                dp[i][j - 1] + 1),       // 插入 t[j-1]
                        dp[i - 1][j - 1] + 1            // 替换 s[i-1] 为 t[j-1]
                    );
                }
            }
        }
        
        return dp[m][n];
    }
}
3.正则表达式匹配
4.最长回文字串
  • 转移条件说明
    边界相等: s[i] == s[j](首尾字符必须相等)
    内部回文: dp[i+1][j-1] == true(去掉首尾后仍是回文)

  • 详细表达式:

    java 复制代码
    // 初始化
    for (int i = 0; i < n; i++) {
        dp[i][i] = true;         // 单个字符是回文
        if (i < n-1) {
            dp[i][i+1] = (s[i] == s[i+1]);  // 两个相邻字符相等是回文
        }
    }
    
    // 状态转移(按长度从小到大)
    for (int len = 3; len <= n; len++) {      // 枚举长度
        for (int i = 0; i + len - 1 < n; i++) {  // 枚举起始位置
            int j = i + len - 1;              // 结束位置
            dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1];  // 状态转移
        }
    }

六、计数DP:

1.不同路径数
2.解码方法数
  • dp[i] 表示字符串前i个字符的解码方法数
  • 转移表达式:dp[i] = dp[i-1] (当 s[i-1] 可以单独解码时) + dp[i-2] (当 s[i-2:i] 可以组合解码时)
  • 转移条件说明
    单字符解码条件: s[i-1] != '0'(字符不能为'0')
    双字符解码条件: 10 ≤ parseInt(s[i-2:i]) ≤ 26
  • 核心代码:
java 复制代码
public int numDecodings(String s) {
    int n = s.length();
    if (n == 0 || s.charAt(0) == '0') return 0;
    
    // dp[i] 表示 s 前 i 个字符的解码方法数
    int[] dp = new int[n + 1];
    dp[0] = 1; // 空字符串有一种解码方式
    dp[1] = 1; // 第一个字符不为'0'时有一种解码方式
    
    for (int i = 2; i <= n; i++) {
        // 单个字符解码
        if (s.charAt(i-1) != '0') {
            dp[i] += dp[i-1];
        }
        
        // 两个字符解码
        int twoDigit = Integer.parseInt(s.substring(i-2, i));
        if (twoDigit >= 10 && twoDigit <= 26) {
            dp[i] += dp[i-2];
        }
    }
    
    return dp[n];
}

七、优化问题:

1.最长递增子序列 (看不懂的话可以去看看LIS思维训练哈哈哈
java 复制代码
import java.util.*;
class Solution {
    public void redraiment(int[] woods, int num) {
        int[] dp = new int[num];

        if(num == 0) System.out.print(0);

        if(woods == null) System.out.print(0);

        for(int i = 0; i < num; i++){
            dp[i] = 1;
        }

        for(int i = 0; i < num; i++){
            for(int j = 0; j < i; j++){
                if(woods[i] > woods[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);//到底是原来的路更长还是走新的那条路更长
                }
            }
        }

        int max = 0;
        for(int a : dp){
            max = Math.max(max,a);
        }

        System.out.print(max);

    }
}

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int num = in.nextInt();
        int[] woods = new int[num];

        for(int i = 0; i < num; i++){
            woods[i] = in.nextInt();
        }

        Solution solution = new Solution();
        solution.redraiment(woods, num);

        in.close();
    }
}
2.股票买卖问题
3.戳气球问题
相关推荐
Aliex_git1 小时前
Nuxt 学习笔记(一)
前端·笔记·学习
探物 AI1 小时前
【感知·车道线检测】UFLDv2车道线检测与车道偏离预警(LDWS)实战
人工智能·算法·目标检测·计算机视觉
烤麻辣烫1 小时前
json与fastjson
前端·javascript·学习·json
菜鸟丁小真2 小时前
LeetCode hot100 -54.螺旋矩阵
算法·leetcode·矩阵·知识点总结
weixin_468466852 小时前
排列组合算法之隔板问题与错排公式
c++·算法·数学建模·排列组合·竞赛·错排·隔板
tryqaaa_2 小时前
学习日志(二)【linux全部命令,http请求头{有例题},Php语法学习】
linux·学习·http·php·web
wsoz2 小时前
Leetcode链表-day9
c++·算法·leetcode·链表
sxjk19872 小时前
WPS表格REGEXP公式提取车牌学习
学习·wps·表格·数据处理
Lumos_7772 小时前
Linux -- 系统调用
linux·运维·算法