一、动态规划的基本概念和思想
1.1 动态规划的定义和特点
动态规划是一种解决多阶段决策问题的算法思想,它通过将问题划分为若干个子问题,并保存子问题的解来求解原问题的方法。动态规划的特点包括以下几个方面:
- 最优子结构性质:动态规划问题具有最优子结构,即原问题的最优解可以通过子问题的最优解推导得出。这意味着问题可以被分解为相互关联的子问题,并且每个子问题的最优解能够组合得到原问题的最优解。
- 重叠子问题性质:动态规划问题存在重叠子问题,即不同的子问题会多次重复出现。通过记忆化或者自底向上的方式,可以避免重复计算相同的子问题,从而提高算法的效率。
- 状态转移方程:动态规划问题需要建立递推关系或者状态转移方程,以描述子问题之间的关系。通过分析问题的特性,找到子问题之间的转移规律,可以根据已知的子问题解得到未知的子问题解。
- 自底向上的求解方式:动态规划通常采用自底向上的求解方式,从最小的子问题开始,逐步推导出更大规模的子问题,直到求解出原问题。这种求解方式保证了子问题的解已经计算完毕,可以直接使用,避免了重复计算。
1.2 最优子结构性质和重叠子问题性质
最优子结构性质是动态规划问题的一个重要特点。它指的是原问题的最优解可以通过子问题的最优解来构造。换句话说,如果一个问题具有最优子结构,那么问题的最优解可以由子问题的最优解递推得到。 最优子结构性质的存在使得我们可以将原问题分解为若干个相互关联的子问题,并且可以利用子问题的最优解来构造原问题的最优解。这种分解过程通常通过建立递推关系或者状态转移方程来描述子问题之间的关系。 重叠子问题性质是指动态规划问题中存在相同的子问题被多次重复计算的现象。由于动态规划问题通常是通过自底向上的方式求解,当计算一个子问题的解时,可能会重复计算相同的子问题。这就导致了重复的计算,浪费了时间和资源。 为了避免重复计算,我们可以采用记忆化的方式,即将已经计算过的子问题的解保存起来,下次遇到相同的子问题时直接返回已保存的解。另一种方式是采用自底向上的迭代求解,先计算小规模的子问题,然后逐步推导出更大规模的子问题的解。重叠子问题性质的存在使得动态规划问题可以通过存储已计算的子问题解来提高效率,避免不必要的重复计算。这种优化技巧在动态规划算法中被广泛应用。
1.3 动态规划的基本步骤和思考方式
动态规划是一种解决最优化问题的算法思想,其基本步骤和思考方式可以概括为以下几步:
- 确定问题的状态:将原问题划分为若干个子问题,并定义状态表示子问题的解。
- 确定状态转移方程:建立子问题之间的递推关系,即通过已解决的子问题的解来求解当前问题的解。这一步是动态规划的核心。
- 确定边界条件:确定最简单的子问题的解,即边界条件。
- 自底向上求解:根据状态转移方程和边界条件,从最简单的子问题开始,逐步求解更复杂的子问题,直到求解出原问题的解。
通过以上步骤和思考方式,可以帮助我们理清问题的结构和求解思路,从而高效地解决动态规划问题。
二、动态规划算法的实现和应用
2.1 背包问题:0/1背包问题、完全背包问题
背包问题是一个经典的优化问题,其中包括0/1背包问题和完全背包问题两种变体。
0/1背包问题: 题目描述:有一个背包容量为C,有n个物品,每个物品有自己的重量和价值,在限定的背包容量下,选择一些物品装入背包,使得装入的物品总价值最大。 算法思路:使用动态规划求解0/1背包问题。定义一个二维数组dp,dp[i][j]表示前i个物品在背包容量为j时的最大总价值。通过填表的方式,逐步求解dp[i][j]的值,最终得到dp[n][C]即为问题的解。
代码语言:javascript复制 #include <stdio.h>
int max(int a, int b) {
return (a > b) ? a : b;
}
int knapSack(int C, int wt[], int val[], int n) {
int dp[n 1][C 1];
for (int i = 0; i <= n; i ) {
for (int j = 0; j <= C; j ) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
}
else if (wt[i - 1] <= j) {
dp[i][j] = max(val[i - 1] dp[i - 1][j - wt[i - 1]], dp[i - 1][j]);
}
else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][C];
}
int main() {
int val[] = {60, 100, 120};
int wt[] = {10, 20, 30};
int C = 50;
int n = sizeof(val) / sizeof(val[0]);
int maxVal = knapSack(C, wt, val, n);
printf("Maximum value: %dn", maxVal);
return 0;
}
完全背包问题: 题目描述:有一个背包容量为C,有n种物品,每种物品有自己的重量和价值,每种物品可以无限次选择放入背包,求在限定的背包容量下,选择物品的组合,使得物品总价值最大。 算法思路:同样使用动态规划求解完全背包问题。定义一个一维数组dp,dp[j]表示背包容量为j时的最大总价值。通过遍历物品并更新dp数组的方式,逐步求解dp[j]的值,最终得到dp[C]即为问题的解。
代码语言:javascript复制#include <stdio.h>
int max(int a, int b) {
return (a > b) ? a : b;
}
int knapSack(int C, int wt[], int val[], int n) {
int dp[C 1];
dp[0] = 0;
for (int j = 1; j <= C; j ) {
dp[j] = 0;
for (int i = 0; i < n; i ) {
if (wt[i] <= j) {
dp[j] = max(dp[j], val[i] dp[j - wt[i]]);
}
}
}
return dp[C];
}
int main() {
int val[] = {60, 100, 120};
int wt[] = {10, 20, 30};
int C = 50;
int n = sizeof(val) / sizeof(val[0]);
int maxVal = knapSack(C, wt, val, n);
printf("Maximum value: %dn", maxVal);
return 0;
}
以上代码分别实现了0/1背包问题和完全背包问题的求解,返回的最大价值即为问题的解。
2.2 最长公共子序列问题
最长公共子序列(Longest Common Subsequence,LCS)问题是一个经典的动态规划问题,用于求解两个序列(可以是字符串、数组等)的最长公共子序列的长度。 题目描述:给定两个序列S1和S2,求它们的最长公共子序列的长度。 算法思路:使用动态规划求解最长公共子序列问题。定义一个二维数组dp,dp[i][j]表示序列S1的前i个元素与序列S2的前j个元素的最长公共子序列的长度。通过填表的方式,逐步求解dp[i][j]的值,最终得到dp[m][n]即为问题的解,其中m为序列S1的长度,n为序列S2的长度。
代码语言:javascript复制#include <stdio.h>
int max(int a, int b) {
return (a > b) ? a : b;
}
int LCS(char* S1, char* S2, int m, int n) {
int dp[m 1][n 1];
for (int i = 0; i <= m; i ) {
for (int j = 0; j <= n; j ) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
} else if (S1[i - 1] == S2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
int main() {
char S1[] = "ABCD";
char S2[] = "ACD";
int m = sizeof(S1) - 1; // 需要减去末尾的'