一. 动态规划算法概述
1.什么是动态规划算法
首先,动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。
掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举。而且,你需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在「重叠子问题」,使用「备忘录」或者「DP table」来优化穷举过程。
重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
动态规划思维框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
2.什么样的问题可以考虑使用动态规划解决呢?
如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。
比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问、序列问题、贪心类型、游戏迷宫等等,都是动态规划的经典应用场景。
动态规划的解题思路
动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。(子问题之间完全独立,上下层问题之间完全依赖) 并且动态规划一般都是自底向上的,做动态规划的思路:
- 穷举分析
- 确定边界
- 找出规律,确定最优子结构
- 写出状态转移方程
更术语的解释:blog.csdn.net/qq_37763204…
二. 动态规划细节讲解
1)重叠子问题
解决方案:使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典)
2)最优子结构
Leetcode322: 零钱兑换 要符合「最优子结构」,子问题间必须互相独立(类似数学归化思想)
假设你有面值为
1, 2, 5的硬币,你想求amount = 11时的最少硬币数(原问题),如果你知道凑出amount = 10, 9, 6的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为1, 2, 5的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。
自顶向下:
dp 函数:dp(n) 表示,输入一个目标金额 n,返回凑出目标金额 n 所需的最少硬币数量。
自底向上
dp 数组的迭代解法
自底向上使用 dp table 来消除重叠子问题,dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。
3) 状态转移方程
设计动态规划的通用技巧:数学归纳思想
数学归纳思想:我们先假设这个结论在 k < n 时成立,然后根据这个假设,想办法推导证明出 k = n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。
同理:假设 dp[0...i-1] 都已经被算出来了,可以通过这些结果算出 dp[i]
定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
扑克牌二分法优化
时间复杂度为 O(NlogN)
拓展到二维
Leetcode:354 俄罗斯套娃信封问题
这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。
解题思路:
先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序;之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案。
三.真题实战
1).Leetcode 53:最大子序和
dp数组含义:以 nums[i] 为结尾的「最大子数组和」为 dp[i]
2).Leetcode 1143:最长公共子序列
base case: dp[0][0]=0
3).Leetcode 583. 两个字符串的删除操作
结合上一题“最长公共子序列”
4).Leetcode 712. 两个字符串的最小ASCII删除和
结合上一题“最长公共子序列”
四.拓展:背包问题
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
第一步要明确两点,「状态」和「选择」
- 状态是「背包的容量」和「可选择的物品」
- 选择是「装进背包」或者「不装进背包」
第二步要明确 dp 数组的定义
- dp[i][j]:对于前i个物品,当前容量j,产生的最大价值就是dp[i][j]
- base case是 dp[...][0] = dp[0][...] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
第三步,根据「选择」,思考状态转移的逻辑
五.拓展:贪心算法
什么是贪心选择性质呢,简单说就是:每一步都做出一个局部最优的选择,最终的结果就是全局最优。
很经典的贪心算法问题 Interval Scheduling(区间调度问题),
Leetcode 435:无重叠区间
解题思路:
1、从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中结束最早的(end 最小)。
2、把所有与 x 区间相交的区间从区间集合 intvs 中删除。
3、重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size()==1) return 0;
sort(intervals.begin(),intervals.end(),[](const vector<int>& a,const vector<int>& b){
return a[1]<b[1];
});
int minCut = 1, cur = 0;
for(int i=1;i<intervals.size();i++){
if(intervals[i][0] >= intervals[cur][1]) {
cur = i;
minCut++;
}
}
return intervals.size()-minCut;
}
};
还有一类可以用贪心算法解决的经典问题:会议室安排问题
Leetcode 253:会议室 II
第一个场景,假设现在只有一个会议室,还有若干会议,你如何将尽可能多的会议安排到这个会议室里?
这个问题需要将这些会议(区间)按结束时间(右端点)排序,然后进行处理,详见前文 贪心算法做时间管理。
第二个场景,给你若干较短的视频片段,和一个较长的视频片段,请你从较短的片段中尽可能少地挑出一些片段,拼接出较长的这个片段。
这个问题需要将这些视频片段(区间)按开始时间(左端点)排序,然后进行处理,详见后文 剪视频剪出一个贪心算法。
第三个场景,给你若干区间,其中可能有些区间比较短,被其他区间完全覆盖住了,请你删除这些被覆盖的区间。
这个问题需要将这些区间按左端点排序,然后就能找到并删除那些被完全覆盖的区间了,详见后文 删除覆盖区间。
第四个场景,给你若干区间,请你将所有有重叠部分的区间进行合并。
这个问题需要将这些区间按左端点排序,方便找出存在重叠的区间,详见后文 合并重叠区间。
第五个场景,有两个部门同时预约了同一个会议室的若干时间段,请你计算会议室的冲突时段。
这个问题就是给你两组区间列表,请你找出这两组区间的交集,这需要你将这些区间按左端点排序,详见后文 区间交集问题。
第六个场景,假设现在只有一个会议室,还有若干会议,如何安排会议才能使这个会议室的闲置时间最少?
这个问题需要动动脑筋,说白了这就是个 0-1 背包问题的变形:
会议室可以看做一个背包,每个会议可以看做一个物品,物品的价值就是会议的时长,请问你如何选择物品(会议)才能最大化背包中的价值(会议室的使用时长)?
当然,这里背包的约束不是一个最大重量,而是各个物品(会议)不能互相冲突。把各个会议按照结束时间进行排序,然后参考前文 0-1 背包问题详解 的思路即可解决,等我以后有机会可以写一写这个问题。
第七个场景,就是本文想讲的场景,给你若干会议,让你合理申请会议室。
这道题的本质是:给你输入若干时间区间,让你计算同一时刻「最多」有几个区间重叠
Leetcode1024:视频拼接
区间问题肯定按照区间的起点或者终点进行排序。
贪心策略:
1、要用若干短视频凑出完成视频 [0, T],至少得有一个短视频的起点是 0。
2、如果有几个短视频的起点都相同,那么一定应该选择那个最长(终点最大)的视频。如果起点相同,那肯定是越长越好.
基于以上两个特点,将 clips 按照起点升序排序,起点相同的按照终点降序排序,最后得到的区间顺序就像这样:
class Solution {
public:
int videoStitching(vector<vector<int>>& clips, int time) {
int res = 0, curEnd = 0,nextEnd = 0, i=0;
sort(clips.begin(),clips.end(),[](const auto &a, const auto &b){
if (a[0]==b[0]) return a[1]>b[1];
else return a[0]<b[0];
});
int n = clips.size();
while(i<n && clips[i][0]<=curEnd){
while(i<n && clips[i][0]<=curEnd){
nextEnd = max(nextEnd,clips[i][1]);
i++;
}
res++;
curEnd = nextEnd;
if(curEnd>=time) return res;
}
return -1;
}
};
贪心算法经典问题:跳跃游戏
leetcode45:跳跃游戏
解法1: 动态规划
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, n);//自底向上递推公式:从0跳到第i个位置需要的最小步数
dp[0] = 0;
for(int i=0;i<n;i++){
int steps = nums[i];
for(int j=1;j<=steps && i+j<n ;j++){
dp[i+j] = min(dp[i+j], dp[i]+1);
}
}
return dp[n-1];
}
};
解法2: 贪心算法
int jump(vector<int>& nums) {
//贪心策略,每次选取能跳最远的格子
int n = nums.size(), farthest = 0, end = 0, jumps = 0;
for(int i=0;i<n-1;i++){
farthest = max(farthest, nums[i]+i);
if(i == end){
end = farthest;
jumps++;
}
}
return jumps;
}