C++中的动态规划(Dynamic Programming, DP)是一种通过分解问题、存储中间结果,从而减少重复计算、提高效率的算法技巧。DP主要适用于那些可以通过“子问题”的解逐步构建出“大问题”解的问题。下面将详细讲解动态规划的基本概念、实现步骤、常见问题类型和一些典型的例题。
动态规划的基本概念
动态规划通常用于优化解决以下类型的问题:
- 重叠子问题:问题可以分解为多个子问题,且这些子问题会被多次计算。
- 最优子结构:问题的最优解可以通过子问题的最优解来构建。
为了避免重复计算,动态规划会使用一个数组或表格存储已经计算过的子问题的解,从而实现“记忆化”效果,降低时间复杂度。
动态规划的实现步骤
动态规划的解题一般遵循以下步骤:
-
定义状态:设定一个状态表示。例如,对于一个求最短路径的问题,可以定义
dp[i]
表示到节点i
的最短路径长度。 -
状态转移方程:找出当前状态与之前状态之间的关系。这是动态规划的核心。比如,假设
dp[i]
的值可以通过dp[i-1]
和dp[i-2]
计算得出,那么状态转移方程可以表示为:dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
。 -
初始化:根据题意初始化
dp
数组的值。例如,若dp[0]
表示开始状态,它的值可以设为0或其他值。 -
计算并获取结果:依据状态转移方程逐步填充
dp
数组,最终的解通常会在dp[n]
或dp[n-1]
等位置得到。
动态规划的题目类型
动态规划可以应用于各种类型的问题,以下是几类常见的动态规划题目类型:
-
线性动态规划
- 斐波那契数列:求斐波那契数列的第
n
项。 - 爬楼梯问题:有
n
阶楼梯,每次可以爬1阶或2阶,问有多少种方法爬到顶层。 - 最小路径和:在一个二维数组中,从左上角到右下角的路径,使路径上的数值和最小。
- 斐波那契数列:求斐波那契数列的第
-
区间动态规划
- 最长回文子串:在给定字符串中,找到长度最长的回文子串。
- 戳气球问题:在一系列气球中戳破气球,得到不同分数,目标是使总分最大。
-
背包问题
- 0/1背包问题:有一系列物品和一个容量固定的背包,每个物品有重量和价值,问如何选择物品使得总价值最大。
- 完全背包问题:物品数量不限,可以多次选择某个物品,但不能超过背包容量。
-
序列型动态规划
- 最长上升子序列:在一个数组中找到最长的递增子序列的长度。
- 编辑距离:给定两个字符串,计算将一个字符串转换成另一个字符串所需的最少编辑操作次数(插入、删除、替换)。
-
树形动态规划
- 树的直径:在一棵树中,找到两点间的最长路径长度。
- 选点覆盖问题:在树结构中选择尽可能少的点,使得树的每条边至少有一个端点被选中。
-
数位动态规划
- 数位统计:给定一个数范围,统计符合某些条件的数的个数,例如数位上不含4的数。
-
状态压缩动态规划
- 旅行商问题(TSP):有若干城市,求解从某个城市出发访问所有城市并返回的最短路径。
- 集合划分问题:将集合划分为若干个子集,使得每个子集的某些属性满足条件。
经典例题讲解
1. 斐波那契数列
求解第n
项的斐波那契数。斐波那契数列的状态转移方程是:
[ dp[i] = dp[i-1] + dp[i-2] ]
其中dp[0] = 0
,dp[1] = 1
。
代码实现:
int fibonacci(int n) {
if (n <= 1) return n;
int dp[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
2. 爬楼梯问题
假设有n
阶楼梯,每次可以爬1阶或2阶,问有多少种不同的方式爬到顶层。
状态定义:dp[i]
表示到达第i
阶楼梯的方法数。
状态转移方程:dp[i] = dp[i-1] + dp[i-2]
初始化:dp[0] = 1
, dp[1] = 1
代码实现:
int climbStairs(int n) {
int dp[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
3. 0/1 背包问题
给定一个容量为W
的背包和n
个物品,每个物品的重量为weight[i]
,价值为value[i]
。问如何选择物品使得在不超过背包容量的情况下总价值最大。
状态定义:dp[i][w]
表示前i
个物品在容量w
下的最大价值。
状态转移方程:
- 若不选择第
i
个物品:dp[i][w] = dp[i-1][w]
- 若选择第
i
个物品:dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])
代码实现:
int knapsack(int W, vector<int>& weight, vector<int>& value) {
int n = weight.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= W; w++) {
if (weight[i - 1] <= w)
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weight[i - 1]] + value[i - 1]);
else
dp[i][w] = dp[i - 1][w];
}
}
return dp[n][W];
}
总结
C++中的动态规划是一种将问题分解、存储计算结果的优化方法。它的精髓在于通过状态转移方程逐步构建解,使得算法时间复杂度大大降低。在学习和应用动态规划时,理解题目类型和状态转移方程是关键。