动态规划与背包问题学习笔记

一、学习背景与核心目标

本次课程围绕编程作业中的重点难题展开,核心聚焦动态规划思想的应用,具体讲解了华罗庚纪念题、组合数问题、01 背包、完全背包及最长公共子序列问题。通过学习,需掌握动态规划的核心逻辑 ------ 拆分问题、定义状态、边界处理与状态转移,同时熟练运用背包问题的经典模板,完成对应编程作业。

二、核心知识点详解

(一)动态规划基础思想

动态规划(DP)的核心是将复杂大问题拆解为若干可重复解决的子问题,通过存储子问题的解避免重复计算,最终推导全局最优解。关键步骤包括:

  1. 定义 DP 数组含义:明确数组下标对应的约束条件(如长度、容量、数量)及数组值对应的目标(如最大价值、组合结果);
  2. 处理边界条件:初始化极端情况下的子问题解(如无乘号、无物品、空字符串时的结果);
  3. 推导状态转移方程:明确当前状态与前序子问题状态的关联,实现从子问题到全局问题的递推。

(二)重点题型解析(附 C 语言代码)

1. 华罗庚纪念题(动态规划求最大乘积)
  • 问题核心:拆分数字字符串,插入指定数量乘号,求最大乘积。

  • C++ 实现代码(带详细注释)

    #include<bits/stdc++.h> // C++万能头文件,包含所有常用标准库
    using namespace std; // 使用std命名空间,简化代码书写
    typedef long long ll; // 定义long long别名ll,方便处理大数(可选,原代码未实际使用)

    char s[20]; // 存储输入的数字字符串,长度预留20位足够应对常规题目
    int dp[20][20]; // dp[i][j]:前i+1个字符(下标0~i)插入j个乘号的最大乘积

    int main()
    {
    int data[20][20]; // data[i][j]:存储字符串下标i到j对应的整数(闭区间)
    int M, N; // N:数字字符串长度,M:需要插入的乘号数量
    // 输入:先输入字符串长度N、乘号数量M,再输入数字字符串s
    cin >> N >> M;
    cin >> s;

    复制代码
      // 初始化dp数组所有元素为0,避免随机值干扰计算
      memset(dp, 0, sizeof(dp));
      
      // 第一步:预处理data数组,拆分字符串为各段对应的数字
      for (int i = 0; i < N; i++)
      {
          int sum = 0;     // 临时存储分段数字的累加值
          for (int j = i; j < N; j++)
          {
              // 字符转数字:s[j]-'0'得到单个数字,累乘10实现多位数拼接
              sum = sum * 10 + s[j] - '0';
              data[i][j] = sum;  // 存储下标i到j的数字(如s="123",i=0,j=1则data[0][1]=12)
          }
      }
    
      // 第二步:初始化DP边界条件------插入0个乘号时,直接取整个前缀的数字
      // dp[i][0]表示前i+1个字符(0~i)插入0个乘号,即完整数字本身
      for (int i = 0; i < N; i++)
      {
          dp[i][0] = data[0][i];
      }
      
      // 第三步:状态转移,填充dp数组求最大乘积
      // i:遍历字符串的结束下标(对应前i+1个字符)
      for (int i = 0; i < N; i++)
      {
          // j:遍历乘号数量(从1到M,0已初始化)
          for (int j = 1; j <= M; j++)
          {
              // k:拆分位置,将前i+1个字符拆分为前k+1个字符(0~k)和后i-k个字符(k+1~i)
              // 前半部分插入j-1个乘号,后半部分不插乘号,相乘后取最大值
              for (int k = 0; k < i; k++)
              {
                  // 状态转移核心:比较当前dp[i][j]和拆分后的乘积,保留最大值
                  // dp[k][j-1]:前k+1个字符插j-1个乘号的最大乘积
                  // data[k+1][i]:k+1到i的数字(后半段无乘号)
                  dp[i][j] = max(dp[k][j - 1] * data[k + 1][i], dp[i][j]);
              }
          }
      }
      
      // 输出结果:前N个字符(0~N-1)插入M个乘号的最大乘积
      cout << dp[N - 1][M];
      return 0;  // 补充返回值,规范main函数

    }

  • 代码说明

    • 该版本是更简洁的 C++ 实现,核心逻辑与原版本一致,但循环顺序和变量命名更精简;
    • data数组预处理所有分段数字,避免重复计算字符转数字的操作;
    • 边界条件:dp[i][0] = data[0][i],无乘号时直接取完整数字;
    • 状态转移三重循环:i(结束位置)→j(乘号数)→k(拆分位置),通过拆分字符串为两部分,递归求子问题的最大乘积;
    • 注意:若数字字符串较长(如超过 10 位),建议将int替换为long long(代码中已 typedef ll 但未使用),避免数值溢出。
  • 测试示例 :输入:

    复制代码
    4 2  // N=4(字符串长度),M=2(乘号数)
    1234 // 数字字符串

    输出:68(对应拆分方式:1234=68)。

2. 组合数问题(回溯法)
  • 问题核心:从 1 到 n 中选出 k 个不同数字,输出所有组合。

  • C 语言实现代码

    #include <stdio.h>

    // 存储组合结果的数组
    int result[100];

    // 递归生成组合
    // start:起始数字,n:范围上限,k:组合长度,current:当前存储下标
    void combination(int start, int n, int k, int current) {
    // 递归终止条件:已选满k个数字
    if (current == k) {
    for (int i = 0; i < k; i++) {
    printf("%d ", result[i]);
    }
    printf("\n");
    return;
    }

    复制代码
      // 遍历从start到n的数字,避免重复组合
      for (int i = start; i <= n; i++) {
          result[current] = i;  // 存入当前数字
          // 递归:起始数字+1,存储下标+1
          combination(i + 1, n, k, current + 1);
      }

    }

    int main() {
    int n, k;
    printf("请输入范围上限n:");
    scanf("%d", &n);
    printf("请输入组合长度k:");
    scanf("%d", &k);

    复制代码
      if (k > n || k < 1) {
          printf("输入无效,k需满足1<=k<=n\n");
          return 1;
      }
    
      printf("所有组合结果:\n");
      combination(1, n, k, 0);
      return 0;

    }

  • 代码说明

    • 递归终止条件:current == k(已选够 k 个数字);
    • 遍历起始值start避免重复组合(如选过 2 就不再从 1 开始);
    • 无需显式回溯:递归参数传递的是新值,不修改原数组,函数返回后自动回到上一层状态。
  • 测试示例 :输入:n=4,k=2 → 输出:1 21 31 42 32 43 4

3. 背包问题(动态规划经典模板)
(1)01 背包问题(每个物品仅 1 个)
复制代码
#include<bits/stdc++.h>  // 万能头文件,包含所有常用标准库(C++专属)
using namespace std;     // 使用std命名空间,简化代码书写

int main(){
    int n,m;             // n:背包总容量,m:物品数量
    // 输入背包容量和物品数量
    scanf("%d %d",&n,&m);
    
    // dp数组:dp[k]表示容量为k的背包能装的最大价值,初始化为0(边界条件:无物品时价值为0)
    int dp[30000]={0};
    // v数组:存储每个物品的体积;p数组:存储每个物品的价值系数(本题中实际价值=v[j]*p[j])
    int v[1000],p[1000];
    
    // 循环输入m个物品的体积和价值系数
    for(int i=0;i<m;i++){
        scanf("%d %d",&v[i],&p[i]);
    }
    
    // 遍历每个物品(01背包核心:先遍历物品)
    for(int j=0;j<m;j++){
        // 计算当前物品的实际价值:体积 * 价值系数(适配特定题目场景的价值计算方式)
        int vel=v[j]*p[j];
        // 倒序遍历背包容量(核心!避免同一物品被重复选择)
        // 从n到v[j]:容量小于v[j]时装不下当前物品,无需处理
        for(int k=n;k>=v[j];k--){
            // 状态转移方程:
            // 选当前物品:dp[k-v[j]] + vel(剩余容量的最大价值 + 当前物品价值)
            // 不选当前物品:dp[k](保持原有价值)
            // 取两者中的最大值更新dp[k]
            dp[k]=max(dp[k-v[j]]+vel,dp[k]);
        }
    }
    // 输出容量为n的背包能装的最大价值
    cout<<dp[n];
    return 0;
}
  • 代码说明

    • 该代码是C++ 版本 的 01 背包一维优化写法,使用#include<bits/stdc++.h>万能头文件简化代码;
    • 价值计算方式:本题中物品实际价值为体积*价值系数(适配课程 / 作业中的特定题目场景);
    • 核心逻辑:倒序遍历容量,确保每个物品仅被选择一次;
    • 数组大小:dp[30000]v[1000]/p[1000]是预留足够空间,适配常规的 01 背包题目数据范围。
  • 测试示例 :输入:

    复制代码
    10 3  // 背包容量10,物品数量3
    3 4   // 物品1:体积3,价值系数4 → 实际价值12
    4 5   // 物品2:体积4,价值系数5 → 实际价值20
    5 6   // 物品3:体积5,价值系数6 → 实际价值30

    输出:42(选物品 1 + 物品 3,12+30=42)。

(2)完全背包问题(物品数量无限)
复制代码
#include <stdio.h>
#include <string.h>

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int n, V;
    printf("请输入物品种类数:");
    scanf("%d", &n);
    printf("请输入背包容量:");
    scanf("%d", &V);

    int w[100], v[100];
    printf("请依次输入每种物品的体积和价值(空格分隔):\n");
    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &w[i], &v[i]);
    }

    // 一维DP解法(正序遍历容量)
    int dp[V+1];
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) {
        // 正序遍历,允许重复选同一物品
        for (int j = w[i]; j <= V; j++) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }

    printf("完全背包最大价值:%d\n", dp[V]);
    return 0;
}
  • 代码说明
    • 完全背包与 01 背包的核心区别:正序遍历容量,允许同一物品被多次选择;
    • 状态转移方程与 01 背包一致,仅遍历顺序不同。

三、关键注意事项

  1. 动态规划的核心是 "状态定义",需先明确 DP 数组的含义再推导转移方程,避免逻辑混乱;
  2. 边界条件是 DP 的基础,未正确初始化可能导致结果错误或数组越界;
  3. 01 背包一维优化必须倒序遍历,完全背包正序遍历,核心是避免物品重复选择;
  4. 递归问题(如组合数)需明确终止条件和参数传递规则,回溯时无需修改原变量;
  5. 华罗庚纪念题代码中,若数字字符串较长(超过 10 位),需将int替换为long long,防止数值溢出;
  6. 你提供的代码均为 C++ 风格(使用cin/cout、万能头文件),实际编译需用 C++ 编译器(如 g++)。

总结

  1. 华罗庚纪念题的核心是分段预处理 + 动态规划:先拆分字符串为所有可能的数字段,再通过三重循环遍历结束位置、乘号数、拆分位置,递推最大乘积;
  2. 你提供的 01 背包代码是一维优化的简洁写法,核心是倒序遍历背包容量,确保每个物品仅选一次;
  3. 动态规划问题需优先明确 DP 数组含义,再处理边界条件,最后推导状态转移方程,这是解题的通用思路。
相关推荐
语戚1 天前
深入浅出 AOP:织入时机、JDK 动态代理与 CGLIB 原理及 Spring 选择策略
java·开发语言·spring·jdk·代理模式·aop·动态代理
NGC_66112 天前
静态代理和动态代理
代理模式
不光头强3 天前
代理模式实现,静态,动态
代理模式
逆境不可逃3 天前
【从零入门23种设计模式12】结构型之代理模式(Spring AOP + 自定义注解 + 切面的实战)
设计模式·代理模式
一碗烈酒3 天前
【使用Python临时搭建代理转发服务,内网穿透】
python·测试工具·代理模式
tsyjjOvO3 天前
代理模式详解:静态代理、JDK 动态代理、CGLIB 动态代理
java·开发语言·代理模式
柒.梧.18 天前
Java代理模式精讲:静态代理+JDK动态代理
java·开发语言·代理模式
Forget_855019 天前
RHEL——HAProxy模式
代理模式
mjhcsp21 天前
C++ 树形 DP解析
开发语言·c++·动态规划·代理模式