文章目录
动态规划:解决优化问题的算法精髓
动态规划是一种强大的算法范式,在解决各类优化问题时发挥着重要作用。与其他算法不同,动态规划通过将复杂问题分解成更小的子问题,并存储这些子问题的解来避免重复计算,从而达到高效解决问题的目的。在本篇博客中,我们将深入探讨动态规划问题的解题方法和步骤,帮助大家掌握这一重要的算法思想。
1. 什么是动态规划?
动态规划(Dynamic Programming,DP)是一种解决最优化问题的算法方法。它的核心思想是将复杂的问题分解成更小的子问题,并通过有效地组合这些子问题的解来得到原问题的解。
动态规划问题通常具有两个重要特点:
-
重叠子问题: 问题的递归解会反复求解相同的子问题。动态规划通过记忆化或自底向上的方式避免了重复计算。
-
最优子结构: 问题的最优解可以由其子问题的最优解组合而成。这意味着我们可以通过解决子问题来构建原问题的最优解。
理解这两个特点是掌握动态规划的关键。
2. 动态规划问题的解题步骤
解决动态规划问题通常可以分为以下四个步骤:
步骤 1: 定义子问题
首先,我们需要定义子问题。这通常涉及将原问题分解成更小、更易于管理的部分。例如,在计算斐波那契数列的问题中,子问题可以是计算序列中的前一个和前两个数字。
步骤 2: 确定状态表示
状态是动态规划中的核心概念。一个状态代表了达到某个子问题解时的情况。我们需要找到一种方式来描述问题在给定步骤的状态,这通常涉及创建一个或多个数组来存储子问题的解。在斐波那契数列的例子中,状态可以通过一个数组dp[i]
表示,其中dp[i]
存储第i
个斐波那契数。
步骤 3: 确定状态转移方程
状态转移方程(也称为DP方程)描述了如何从一个状态转移到另一个状态。它是解动态规划问题的核心,我们需要准确地定义如何从已解决的子问题得到当前问题的解。在斐波那契数列的例子中,状态转移方程是dp[i] = dp[i-1] + dp[i-2]
。
步骤 4: 解决问题
最后,我们需要根据定义的子问题、状态表示和状态转移方程来解决原问题。这通常涉及初始化状态数组的边界值,然后按顺序(自底向上)或递归地(自顶向下,通常需要记忆化)计算所有状态的值。对于斐波那契数列,我们需要初始化dp[0]
和dp[1]
,然后计算dp[2]
到dp[n]
。
3. 例子: 最长上升子序列
让我们通过最长上升子序列(LIS)问题来具体看看如何应用上述步骤。
问题描述: 给定一个未排序的整数数组,找到最长上升子序列的长度。
步骤 1: 定义子问题
对于数组nums
中的每个元素nums[i]
,考虑以nums[i]
结尾的最长上升子序列。
步骤 2: 确定状态表示
定义状态dp[i]
表示以nums[i]
结尾的最长上升子序列的长度。
步骤 3: 确定状态转移方程
对于状态dp[i]
,考虑所有j < i
且nums[j] < nums[i]
的位置。状态转移方程为:
dp[i] = max(dp[j]) + 1, for all j < i and nums[j] < nums[i]
步骤 4: 解决问题
首先,我们需要初始化所有dp[i]
为1,因为每个元素至少可以自己构成一个长度为1的上升子序列。然后,我们遍历数组nums
,对于每个i
,计算以nums[i]
结尾的最长上升子序列长度,并更新dp[i]
。最后,我们返回dp
数组中的最大值,即为最长上升子序列的长度。
代码示例
#include <iostream>
#include <vector>
#include <algorithm>
int lengthOfLIS(const std::vector<int>& nums) {
if (nums.empty()) return 0;
std::vector<int> dp(nums.size(), 1); // dp 数组初始化为 1
int max_length = 1; // 最长上升子序列的最小长度为 1
for (size_t i = 0; i < nums.size(); ++i) {
for (size_t j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
max_length = std::max(max_length, dp[i]);
}
return max_length;
}
int main() {
std::vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
std::cout << "Length of LIS is " << lengthOfLIS(nums) << std::endl; // 应输出 4
return 0;
}
代码详解
在这段 C++ 代码中,我们定义了一个名为 lengthOfLIS 的函数,它接收一个整数型向量 nums 作为参数,并返回其中最长上升子序列的长度。我们使用一个名为 dp 的整数型向量来存储状态,其中 dp[i] 表示以 nums[i] 结尾的最长上升子序列的长度。我们还定义了一个名为 max_length 的变量来持续跟踪遇到的最长上升子序列的长度。
在函数体中,我们首先检查输入的向量是否为空。如果为空,我们直接返回 0。否则,我们初始化 dp 向量的所有元素为 1,并开始两层循环来填充 dp 向量。内层循环用于找到所有小于 nums[i] 的 nums[j],并更新 dp[i]。每次更新后,我们还会更新 max_length。
最后,函数返回 max_length 作为最终结果。在 main 函数中,我们创建了一个示例向量 nums,并调用 lengthOfLIS 函数来计算并输出最长上升子序列的长度