参考资料:
算法引入
我们先考虑一个经典问题:
小明要爬上一个n级的台阶, 他每次可以走1级或2级, 求不同的走法数.
经典递归算法
代码:
int solve(int n) {
if (n == 1)return 1;
else if (n == 2)return 2;
else return solve(n - 1) + solve(n - 2);
}
int main() {
int n;
cin >> n;
cout << solve(n);
return 0;
}
时间复杂度: O(2^n)
我们可以发现, 上述算法包含着大量的重复计算:
假设n=5
, 根据上述算法, 我们需要先计算solve(4)
和solve(3)
而要计算solve(4)
, 必须先计算solve(3)
和solve(2)
我们注意到solve(3)
被计算了两次.
当问题规模更大时, 重复计算会更多, 导致问题不能在理想的时间内得到解决.
动态规划算法
如果我们将已经解决的问题的结果保存下来, 在解决新问题时直接调取旧问题的结果, 就能节省不少时间, 这就是最简单的动态规划
#include<iostream>
#include<algorithm>
using namespace std;
int dp[100];
void solve(int n) {
if (n <= 2) return;
else {
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
}
}
int main() {
dp[1] = 1, dp[2] = 2;
int n;
cin >> n;
solve(n);
cout << dp[n];
return 0;
}
时间复杂度: O(n)
动态规划算法
能用动态规划解决的问题的特点
- 最优化原理: 问题的最优解所包含的子问题的解也是最优的.
- 无后效性: 某个阶段的决策一旦确定, 就不受后续决策的影响.
- 有重叠的子问题(该性质不是必须的, 但是若没有这条性质, 动态规划算法将不具有优势).
算法实现的步骤
- 创建一个数组(数组的维数与问题相关), 用以保存子问题的结果.
- 设置数组的边界值.
- 求出状态转移方程, 即找到每个状态与上一个状态的关系.
- 返回需要的值.
经典例题
1.最长不连续递增子列
对应题目: LeetCode第300题
算法思想:
- 定义状态
dp[i]
为: 以nums[i]
为尾元素的上升子序列的最长长度. - 状态转移: 对于每一个
j
属于[0, i)
, 若nums[i] > nums[j]
, 则dp[i] = max(dp[i], dp[j]+1)
- 遍历
dp
数组, 数组中的最大元素即为原问题的答案.
代码实现:
int lengthOfLIS(int* nums, int numsSize){
int dp[2505];
for(int i=0;i<numsSize;i++){ //求以nums[i]为结尾的最长递增子列
dp[i]=1; //初值为1
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=fmax(dp[i],dp[j]+1); //状态转移方程
}
}
}
int max=0;
for(int i=0;i<numsSize;i++){
if(dp[i]>max) max=dp[i];
}
return max;
}
// 时间复杂度O(n^2)
算法改进:
定义tail
数组, tail[i]
表示: 长度为i+1
的所有递增子列的最小尾元素.
我们容易知道, tail
数组是严格单调递增的.
状态转移定义为:
- 若当前元素
e
大于tail[i]
, 则tail[i+1]=e
(得到了一个长度为i+2
的递增子列, 最小尾元素为e
) - 若当前元素
e
小于等于tail[i]
, 二分查找到tail
数组中第一个大于e
的数字, 并将其更新为e
当我们遍历整个数组后, tail
数组中的元素个数即为最长递增子列的长度.
代码实现:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int tail[2505];
tail[0]=nums[0];
int ans=1;
int len=nums.size();
for(int i=1;i<len;i++){
if(nums[i]>tail[ans-1]){
tail[ans++]=nums[i];
}
else if(nums[i]==tail[ans-1]) continue;
else{
int L=0,R=ans-1,flag=0;
while(L<=R){
int mid=(L+R)>>1;
if(nums[i]>tail[mid]) L=mid+1;
else{
flag=mid;
R=mid-1;
}
}
if(nums[i]==tail[flag]) continue;
tail[flag]=nums[i];
}
}
return ans;
}
};
//时间复杂度O(nlogn)
2. 数组最大连续子序列和
对应题目: LeetCode第53题
代码实现:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int len=nums.size();
int dp[100005];
dp[0]=nums[0];
int max=dp[0];
for(int i=1;i<len;i++){
dp[i]=fmax(dp[i-1]+nums[i],nums[i]);
if(dp[i]>max) max=dp[i];
}
return max;
}
};