动态规划——从入门到放弃
基本思想
- 最优子结构
子问题最优时母问题通过优化选择为最优解 - 子问题重叠
母问题与子问题本质上是同一个问题
(不同在被子问题传递的参数) - 边界
(相当于递归的终止条件) - 子问题独立
母问题对子问题选择时,当前被选择的子问题两两互不影响 - 做备忘录
记录求解过的子问题的答案(记忆化搜索)
如何用动态规划解决问题
- 构造问题所对应的过程
- 思考过程最后一步有哪些选择情况
- 找到最后一步的子问题,确保符合子问题重叠,把子问题中不同的地方设置为参数
- 使子问题符合“最优子结构”
- 找到边界,考虑边界的各种处理方式
- 确保子问题独立
- 考虑如何做备忘录
- 写转移方程式
参考资料:
动态规划三部曲之一个故事教你透彻理解动态规划(一)
从暴力到动态规划
引例1:已知nums=[1,5,2,4,3], 求最长递增子序列长度
- 暴力枚举/暴力搜索
时间复杂度:O(n*2n)
以"1"为起点,下一个可选"5、2、4、3",以此类推……
尝试写出递归的代码:
#include <iostream>
#define N 5
using namespace std;
int nums[N]={1,5,2,4,3};
int maxLen;
int L(int i);
int main()
{
for(int i=0;i<N;i++){
maxLen=max(maxLen,L(i));
}
cout<<maxLen;
return 0;
}
int L(int i){
if(i==N-1)
return 1;
int maxL=1;
for(int j=i+1;j<N;j++){
if(nums[j]>nums[i]){
maxL=max(maxL,L(j)+1);
}
}
return maxL;
}
- 优化——记忆化搜索/递归树的剪枝
观察一下递归树发现,其中有很多重复计算(如:在计算1、2、4时已经计算过4的值,而在1、4支上又算了一次)
解决办法:用一个表保存一下计算的值,如果曾计算过就直接取值,否则再进行计算,从而避免许多不必要的重复计算,这种办法也叫做“记忆化搜索”或者叫“剪枝”,就是常说的“用空间换取时间”。
进行“剪枝”优化的代码:
#include <iostream>
#include <bits/stdc++.h>
#define N 5
using namespace std;
int nums[N]={1,5,2,4,3};
int Len[N];
int maxLen;
int L(int i);
int main()
{
memset(Len,-1,sizeof(Len));
for(int i=0;i<N;i++){
maxLen=max(maxLen,L(i));
}
cout<<maxLen;
return 0;
}
int L(int i){
if(Len[i]!=-1){
return Len[i];
}
if(i==N-1){
Len[i]=1;
return 1;
}
int maxL=1;
for(int j=i+1;j<N;j++){
if(nums[j]>nums[i]){
maxL=max(maxL,L(j)+1);
}
}
Len[i]=maxL;
return maxL;
}
- 迭代/非递归的实现
由此可见,知道L(4)可以计算L(3),知道L(4)、L(3)可以计算L(2),以此类推……我们便找到了递推的方向(与递归相反)
动态规划代码实现:
#include <iostream>
#include <bits/stdc++.h>
#define N 5
using namespace std;
int nums[N]={1,5,2,4,3};
int Len[N];
int maxLen;
int main()
{
Len[N-1]=1;
for(int i=N-2;i>=0;i--){
Len[i]=1;
for(int j=i+1;j<N;j++){
if(nums[i]<nums[j])
Len[i]=max(Len[i],Len[j]+1);
}
}
for(int i=0;i<N;i++){
maxLen=max(maxLen,Len[i]);
}
cout<<maxLen;
return 0;
}
引例2:
k好数