文章目录
- 1.绪论
- 1.1 什么是动态规划
- 1.2 递归写法(自顶向下)
- 1.3 递推写法(自底向上)
- 1.3 分治、贪心与动态规划
- 2.最大连续子序列和
- 2.1 问题分析
- 2.2 状态转移方程:
- 2.3 边界
- 2.4 代码
- 2.5 DP思想
- 3 最长不下降子序列(LIS)
- 3.1 问题分析
- 3.2 状态转移方程
- 3.3 边界
- 3.4 代码
- 3.5 待优化DP+二分法(坑一)
- 4.最长公共子序列(LCS)
- 4.1 问题分析
- 4.2 状态转移方程
- 4.3 边界
- 4.4 代码(坑2,输出有问题)
- 5.最长回文子串
- 5.1 问题解决
- 6.关于上述题目的输出总结
- 7 易错点
1.绪论
1.1 什么是动态规划
用来解决 最优化问题的算法思想
将一个复杂的问题可分解为若干个子问题且这些问题会重复出现——综合子问题的最优解(原问题最优解可以由子问题最优解推导来,并将每个求解过的子问题的解记录下来)——得到原问题的最优解
方法:递归(记忆化搜索)、递推
使用条件:拥有重叠子问题,最优子结构的问题
1.2 递归写法(自顶向下)
从目标问题开始,分解成子问题的组合,知道分解到边界位置
普通的斐波拉契数列:复杂度O(2^N)
改进:开一维数组dp保存已经计算过的结果,dp[n]记录F[n],dp[n] = -1表示还没有计算过
复杂度:O(n)
1.3 递推写法(自底向上)
从边界开始,不断向上解决问题,直到解决目标问题
数塔问题
状态转移方程(决策,往左下还是右下)
边界:
using namespace std;
const int maxn = 1000;
int f[maxn][maxn],dp[maxn][maxn];//the node of number tower and the value of startnode
int main() {
int n;
scanf("%d",&n);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= i; j++){
scanf("%d",&f[i][j]);//input the tower
}
}
for(int j =1;j<=n;j++){
dp[n][j] = f[n][j]; //setting of the boundaries
}
for(int i = n-1;i>=1;i--){
for(int j=1;j<=i;j++){
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1])+ f[i][j] ; //computing from the n-1 layer
}
}
printf("%d\n",dp[1][1]);
return 0;
}
1.3 分治、贪心与动态规划
- 分治与DP:都是分解子问题并且合并子问题得到原问题的解;但前者的问题不重叠,譬如:归并排序与快排
- 贪心与DP: 都要求原问题必须拥有最优子结构;
前者类似于”自顶向下“,但并不等待子问题求解完毕后再选择使用哪一个,而通过一种策略直接选择一个子问题求解,没被选择的子问题就不求解了,直接抛弃(单链的流水方式)
譬如数塔问题:从最上层,每次选择两个分支中最大的也给,一直到最底层(所以不一定会得到最优解)
DP考虑所有子问题,并选择继承能得到最优结果的哪个,对暂时没被继承 的子问题,由于重叠子问题的存在,还有机会成为全局最优的一部分
2.最大连续子序列和
2.1 问题分析
暴力:枚举左右端点,O(N^ 2),求和,O(n),一共O(N^3)
DP思想:以 dp[i] 表示以 A[i] 作为末尾的连续序列和,求其中最大值,复杂度 O(N)
2.2 状态转移方程:
2.3 边界
2.4 代码
简单写法
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100;
int main(){
int a[maxn],dp[maxn];
int n;
scanf("%d",&n);
for(int i =1;i<=n;i++){
scanf("%d",&a[i]);
}
dp[1]=a[1];
for(int i =2;i<=n;i++){
dp[i] = max(dp[i-1]+a[i],a[i]);
}
int k=1;
for(int i =2;i<=n;i++){
if(dp[i]>dp[k]) k=i;
}
printf("%d\n",dp[k]);
return 0;
}
改进后使用向量
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int sz = array.size();//获取向量长度
vector<int> dp(sz+1,1); //开辟DP数组,默认全为1
dp[0] = array[0];//边界值
int ret = array[0];
for(int i =1;i<sz;i++){//从零开始存储,注意是小于不是小于等于
dp[i]=max(dp[i-1]+array[i],array[i]);
ret = max(ret ,dp[i]);//改进
}
return ret;
}
};
2.5 DP思想
状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。
每次计算dp[i],都只会涉及dp[i-1],而不直接用到dp[i-1]蕴含的历史信息
动态规划必须设计一个拥有无后效性的状态以及相应的状态转移方程(难点)
3 最长不下降子序列(LIS)
3.1 问题分析
暴力:对每个元素选择取与不取,O(2^N)
重复子问题:每次碰到子问题”以A[i]结尾的LIS”时都去重新遍历所有子序列,而不是直接记录
DP: 以dp[i]表示以A[i]结尾的LIS
3.2 状态转移方程
3.3 边界
3.4 代码
枚举:将 i 从小到大遍历(dp[i]只与小于i的j有关)
class Solution {
public:
vector<int> LIS(vector<int>& arr) {
// write code here
vector<int> dp(arr.size(),1);//简化1——dp数组的设置以及边界条件初始化
int ans=1;
int maxn=0,n=arr.size();
for(int i =1;i<n;i++){
for(int j = 0;j<i;j++){
if( arr[j]<arr[i] && dp[j]+1>dp[i]) dp[i]=dp[j]+1;//状态转移方程
}
ans= max(ans,dp[i]);//找到最大长度
}
//难点——如何输出相应的序列呢,从尾巴向前找,满足条件的一定dp[i]对应相同
vector<int> x(ans);
for(int i = n-1,j=ans;j>0;i--){
if(dp[i]==j){
x[--j]=arr[i];//简化2——辅助数组的遍历赋值
}
}
return x;
}
};
3.5 待优化DP+二分法(坑一)
4.最长公共子序列(LCS)
4.1 问题分析
暴力解法:分别对两个字符串的每个字符进行选与不选决策,得到两个子序列后再比较是否相同,复杂度O(2^(m+n)*max(m,n))
DP——dp[i][j]表示A的i号位与B的j号位之前的LCS长度
4.2 状态转移方程
4.3 边界
复杂度 O(mn)
4.4 代码(坑2,输出有问题)
lass Solution {
public:
string LCS(string s1, string s2) {
int n1 = s1.size(), n2 = s2.size();
string ans;
int dp[n1 + 1][n2 + 1]; //开辟dp
memset(dp, 0, sizeof(dp)); //初始化为0
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
//ans=ans+s1[i-1]; 易错点1——并不是每个相等的都是解
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
if (dp[n1][n2] == 0) return "-1";
else {
//易错点2——并不能从前向后或者从后往前直接遍历(固定j,遍历i)
for (int i = n1 - 1, j = n2 - 1; i >= 0, j >= 0;) {
if (s1[i] == s2[j]) {
ans = ans + s1[i];
i--;
j--;
} else if (dp[i + 1][j] > dp[i][j + 1]) {
j--;
} else {
i--;
}
}
reverse(ans.begin(), ans.end());//反转字符串
return ans;
}
}
};
关于以上易错点的测试用例:
寻找答案思路:从后往前找,当说明找到了一个字母, 和 都执行前进,否则选择值较大的方向前进
5.最长回文子串
5.1 问题解决
暴力:枚举区间端点+判断回文,O(n^3);
DP:
dp[i][j]表示S[i]~S[j]是否是回文子串,是则为1,不是为0
状态转移方程:
边界(从区间如何发展的角度考虑):
难点:如何进行枚举?
如果从i和j从小到大的顺序枚举并为更新,无法计算所有的dp;
故根据递推写法,考虑子串的长度和子串初始位置
复杂度 O(n^2)
class Solution {
public:
int getLongestPalindrome(string A) {
int n = A.length();//获取字符串长度
vector<vector<int> > dp(n,vector<int>(n));//开辟DP数组并且初始化为0
int ans = 1;
//l表示子串长度,i为子串起始位置
for(int l =0;l<n;l++){ //枚举子串长度
for(int i=0;i+l<n;i++){ //枚举左端点,不能取到n,a[n]不存在
int j=i+l ;//子串最后元素
if(l==0){
dp[i][j]=1;//边界条件1
}
else if(l==1){
dp[i][j]=(A[i]==A[j]);//边界条件2
}
else {
dp[i][j]=(A[i]==A[j]&&dp[i+1][j-1]);//状态转移方程
}
if(dp[i][j]&&l+1>ans){//每次都更新最长值,首先是回文串,并且大于等于
//当前最大值
ans=l+1;
}
}
}
return ans;
}
};
如果要求输出子串:
string ans;
if(dp[i][j]&&l+1>ans.size()){//每次都更新最长值,首先是回文串,并且大于等于
ans = s.substr(i,l+1);
} //当前最大值
6.关于上述题目的输出总结
从后往前找路径
如果路径是二维的——最长公共序列
要判断三个 方向(左上,左,上),判断条件为的值或者数组值;
如果路径是一维的——最长不下降子序列
找dp变化的临界点的数组值
7 易错点
遍历I,J时相互写错;
DP数组没有给定大小或者没有赋初始值;
for循环的终止条件多个时用,而不用&&;