一、学习背景与核心目标
本次课程围绕编程作业中的重点难题展开,核心聚焦动态规划思想的应用,具体讲解了华罗庚纪念题、组合数问题、01 背包、完全背包及最长公共子序列问题。通过学习,需掌握动态规划的核心逻辑 ------ 拆分问题、定义状态、边界处理与状态转移,同时熟练运用背包问题的经典模板,完成对应编程作业。
二、核心知识点详解
(一)动态规划基础思想
动态规划(DP)的核心是将复杂大问题拆解为若干可重复解决的子问题,通过存储子问题的解避免重复计算,最终推导全局最优解。关键步骤包括:
- 定义 DP 数组含义:明确数组下标对应的约束条件(如长度、容量、数量)及数组值对应的目标(如最大价值、组合结果);
- 处理边界条件:初始化极端情况下的子问题解(如无乘号、无物品、空字符串时的结果);
- 推导状态转移方程:明确当前状态与前序子问题状态的关联,实现从子问题到全局问题的递推。
(二)重点题型解析(附 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 2、1 3、1 4、2 3、2 4、3 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 背包题目数据范围。
- 该代码是C++ 版本 的 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 背包一致,仅遍历顺序不同。
三、关键注意事项
- 动态规划的核心是 "状态定义",需先明确 DP 数组的含义再推导转移方程,避免逻辑混乱;
- 边界条件是 DP 的基础,未正确初始化可能导致结果错误或数组越界;
- 01 背包一维优化必须倒序遍历,完全背包正序遍历,核心是避免物品重复选择;
- 递归问题(如组合数)需明确终止条件和参数传递规则,回溯时无需修改原变量;
- 华罗庚纪念题代码中,若数字字符串较长(超过 10 位),需将
int替换为long long,防止数值溢出; - 你提供的代码均为 C++ 风格(使用
cin/cout、万能头文件),实际编译需用 C++ 编译器(如 g++)。
总结
- 华罗庚纪念题的核心是分段预处理 + 动态规划:先拆分字符串为所有可能的数字段,再通过三重循环遍历结束位置、乘号数、拆分位置,递推最大乘积;
- 你提供的 01 背包代码是一维优化的简洁写法,核心是倒序遍历背包容量,确保每个物品仅选一次;
- 动态规划问题需优先明确 DP 数组含义,再处理边界条件,最后推导状态转移方程,这是解题的通用思路。