动态规划专精:0-1 背包问题与最长公共子序列 (LCS)
真题原型:全国软考动态规划必考双子星主攻考点:最优子结构 · 状态转移方程填表 · C 语言代码填空 · 空间优化保底目标:8 ~ 10 分 (核心得分盘)🎯 章节攻坚目标
动态规划 (Dynamic Programming, DP) 是下午试题四出现频次最高(约占 45%)的命题宗门。其解题模式极其标准化:
- 识别状态数组定义(通常为二维数组
dp[i][j]或c[i][j]); - 翻译状态转移方程中的“选与不选”或“匹配与不匹配”分支;
- 准确填写循环边界与基线初值;
- 理解一维滚动数组的倒序更新机制。
一、 0-1 背包问题全景精解
1.1 问题数学建模
给定
1.2 状态定义与状态转移方程
- 状态定义:设
dp[i][j]表示从前件物品中挑选物品放入承重为 的背包中所能获得的最大总价值( )。 - 边界条件:
dp[0][j] = 0(前 0 件物品放入任何背包价值均为 0);dp[i][0] = 0(背包容量为 0 时装不下任何物品,价值为 0)。
- 状态转移方程:
正在渲染架构图表...
二、 最长公共子序列 (LCS) 全景精解
2.1 问题描述
给定两个序列
2.2 状态定义与状态转移方程
- 状态定义:设
c[i][j]表示序列的前 个字符和序列 的前 个字符的最长公共子序列长度。 - 状态转移方程:
- 方向标记数组
b[i][j]:- 若
, b[i][j] = 1(代表向左上方对角线回溯); - 若
, b[i][j] = 2(代表向上方回溯); - 否则
b[i][j] = 3(代表向左方回溯)。
- 若
三、 C 语言代码骨架与标准演练
3.1 0-1 背包 C 语言标准实现
c
#include <stdio.h>
#define MAX_N 100
#define MAX_W 1000
int max(int a, int b) {
return (a > b) ? a : b;
}
int Knapsack(int n, int W, int w[], int v[]) {
int dp[MAX_N + 1][MAX_W + 1];
int i, j;
// 1. 初始化边界条件
for (i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (j = 0; j <= W; j++) {
dp[0][j] = 0;
}
// 2. 双重循环自底向上递推填表
for (i = 1; i <= n; i++) {
for (j = 1; j <= W; j++) {
if (j < w[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
}
return dp[n][W]; // 返回全局最优解
}四、 考场设问与检索式自测 (Active Recall Drill)
问题 1:算法策略与复杂度分析(4分)
- 0-1 背包问题和 LCS 问题均采用了哪种算法设计策略?该策略的核心特征是什么?
- 给出上述 0-1 背包算法的时间复杂度与空间复杂度(用大
符号表示,物品数量为 ,背包容量为 )。
🔍 查看问题 1 标准答案与解析
标准答案:
- 动态规划法 (Dynamic Programming)。
- 核心特征:最优子结构性质(问题的最优解包含其子问题的最优解)与重叠子问题性质(子问题在递推过程中被反复计算,通过查表避免重复计算)。
- 时间复杂度:
(或伪多项式时间); - 空间复杂度:
。
- 时间复杂度:
问题 2:0-1 背包 C 语言代码填空(4分)
阅读下列 0-1 背包函数代码片段,补全空缺 [空 1] 与 [空 2]。
c
for (i = 1; i <= n; i++) {
for (j = 1; j <= W; j++) {
if (j < w[i]) {
dp[i][j] = [空 1];
} else {
dp[i][j] = max(dp[i - 1][j], [空 2]);
}
}
}🔍 查看问题 2 标准答案与代码精析
标准答案:
[空 1]:dp[i - 1][j][空 2]:dp[i - 1][j - w[i]] + v[i]
填空采分关键:
[空 1]:背包容量不足以装入第件物品,直接继承前 件物品在同容量 下的最优解; [空 2]:装入第件物品的方案:价值为第 件的价值 加上背包剩余容量 下前 件物品的最优解 dp[i - 1][j - w[i]]。
问题 3:LCS 最长公共子序列回溯输出填空(4分)
下面是依据方向标记数组 b 递归打印最长公共子序列字符的 C 语言函数。请补全 [空 1] 与 [空 2]。
c
void PrintLCS(int b[][MAX_LEN], char X[], int i, int j) {
// 递归基线出口
if ([空 1]) {
return;
}
if (b[i][j] == 1) { // 字符匹配,向对角线回溯
PrintLCS(b, X, i - 1, j - 1);
printf("%c", X[i - 1]); // 输出公共字符
} else if (b[i][j] == 2) { // 向上回溯
[空 2];
} else { // 向左回溯
PrintLCS(b, X, i, j - 1);
}
}🔍 查看问题 3 标准答案与递归精析
标准答案:
[空 1]:i == 0 || j == 0(或:i <= 0 || j <= 0)[空 2]:PrintLCS(b, X, i - 1, j)
填空采分关键:
[空 1]递归基线:任何一个串长度到达 0(空串)即为递归终点,无需继续比对;[空 2]向上回溯:向上意味着行号减少,因此传入参数为i - 1,列号j保持不变。
问题 4:0-1 背包一维滚动数组空间优化原理(3分)
在工程实现中,0-1 背包通常将空间复杂度从
c
for (i = 1; i <= n; i++) {
for (j = W; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}请回答:为什么内层循环中变量
🔍 查看问题 4 标准答案与原理解析
标准答案:
- 核心原因:为了保证每件物品在每个阶段只被放入一次(满足 0-1 背包约束)。
- 机理详解: 若采用正序遍历(
从小到大递增),在计算较大的容量 时,所引用的 dp[j - w[i]]已经被当前第件物品更新过,导致第 件物品在同一轮中被重复放入(退化为完全背包问题); 采用倒序遍历时,计算 dp[j]所用到的dp[j - w[i]]尚未被本轮更新,仍然保留着前件物品的历史状态,从而确保了状态转移的正确性。