今天在LeetCode做了一道动态规划的题,一直觉得动态规划的解题思路很有意思,借这道题来总结一下。
动态规划
我最初搞不懂动态规划是什么,那时候懂贪心算法,觉得贪心算法就是在求解问题的每一步都取全局最优,而动态规划在求解问题的每一步都取局部最优。后来做了一些动态规划的题才发现自己早就接触过了,还记得一道很经典的求从起点到终点有多少种走法的题就用了动态规划的思想。现在看来,动态规划就是缩小问题的规模,先在小问题中取得最优解,然后再不断增大问题的规模,通过小问题的最优解求得大问题的最优解。
上楼梯问题
我觉得有一道题能让人很容易理解动态规划,就是一个人上楼梯,一步上两级或三级,问n
级楼梯最多有多少种不同的走法。很显然,如果n = 1
,那么有0
种走法,如果n = 2
或n = 3
,那么有1
种走法,也就是说,当n
较小的时候我们很容易求得最优解,然后关键在于如何通过小问题的最优解求得大问题的最优解。当n > 3
时,我们在上到第n
级楼梯之前可能在第n-3
或n-2
级楼梯上,因为我们最后一步可能走两级也可能走三级,那么如果我们知道n-3
和n-2
级楼梯最多有多少种走法,我们就可以知道n
级楼梯最多有多少种走法。设n
级楼梯有f(n)
种走法,那么f(1) = 0, f(2) = f(3) = 1, f(n) = f(n-3) + f(n-2), n > 3
。在这个问题中,求解用了递归的方法,事实上,动态规划经常会用数组来存储小问题的最优解,以此来降低求解大问题的时间复杂度。
Longest Increasing Subsequence
问题
求一个无序数列中最长递增子数列的长度。(子数列指的是从数列中抽取任意元素按原本顺序排列得到的数列)
思路1
我的想法是一个长度为n
的子数列可以由一个长度为n-1
的子数列加上一个元素得到,为了使子数列尽可能长,相同长度的子数列中最后一个元素越小越好。因此我记录相同长度子数列最后一个元素中的最小值,然后遍历数列不断更新得到最优解。
代码1
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//记录长度为下标值的子数列最后一个元素的最小值,初始化为无穷大,即第一次得到的任意子数列最后一个元素均为相同长度子数列中的最小值,子数列长度的最大可能值为数列的长度
vector<int> v(nums.size()+1, INT_MAX);
v[0] = INT_MIN; //长度为0的子数列最后一个元素初始化为无穷小,即长度为0的子数列加上任意元素可以得到长度为1的数列
int ans = 0; //最长子数列的长度
for (int i = 0; i < nums.size(); ++i) {
for (int j = 0; j < ans; ++j) {
if (nums[i] > v[j] && nums[i] < v[j+1]) v[++j] = nums[i]; //如果下标为i的元素大于长度为j的子数列最后一个元素并且小于长度为j+1的子数列最后一个元素的最小值,则更新为相同长度的长度为j+1的子数列的最后一个元素的最小值,即通过在长度为j的子数列后加上下标为i的元素得到长度为j+1的子数列
}
if (nums[i] > v[ans]) v[++ans] = nums[i]; //如果下标为i的元素大于当前最长子数列的最后一个元素,则通过在当前最长子数列后加上下标为i的元素得到新的最长子数列
}
return ans;
}
};
思路2
我发现答案的思路更加清晰,记录以每一个元素为子数列中最后一个元素的所有子数列中的最大长度,然后取其中的最大值。假设以数列中第n
个元素为子数列最后一个元素的所有子数列中的最大长度为Sn,数列中第m
个元素小于第n
个元素并且满足m < n
,那么Sn = Sm + 1。我觉得这种思路更符合动态规划的思想,因为先求出了小问题的最优解,再求出大问题的最优解。
代码2
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> v(nums.size(), 1); //记录以下标对应元素为最后一个元素的所有子数列的最大长度
int ans = 0; //子数列的最大长度
for (int i = 0; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) v[i] = max(v[i], v[j]+1); //更新以当前元素为最后一个元素的所有子数列的最大长度
}
ans = max(ans, v[i]);
}
return ans;
}
};