动态规划的基本思想
将给定问题分解成不同部分(子问题),通过合并子问题的解来得到原问题的解。通过构建状态列表将子问题的解记忆化储存。再解决临近子问题时通过查表避免重复计算,从而降低时间复杂度。
动态规划的三个步骤
确定目标问题
确定子问题及状态opt[n]
子问题必须具有最优子结构,使得动态规划能够抵达全局最优解而不是如贪心算法一样只关注局部最优解。
子问题必须具有无后效性,当前多个最优解的取得与路劲无关,只与当前结果有关。
确定转移方程
opt[n]=best_of{opt[n-1],opt[n-2]…}
动态规划的两种实现形式
(1)递归:
优点:直观,容易编写
缺点:可能会因递归层数太深而导致爆栈,函数调用带来额外的时间开销,无法使用滚动数组来降低空间复杂度。
(2)递推:
通过滚动数组来降低空间复杂度。
动态规划与分治
相同点: 目标问题具有最优子结构,将原目标问题分而治之,分解成小目标问题(容易解决),再将小问题合并,形成原问题的解。
不同点:分治法的小目标问题相互独立,用递归。
动态规划的子问题相互联系,采用递推。
动态规划的解题步骤
逆向分析,正向做答。
1.确定目标问题的最优解;【最优子结构】
2.目标问题分解成子问题,子问题依次分解,必须保证无后效性;
3.大问题的最优解取决于小问题的最优解;【状态转移方程】
4.确定底层的边界值。【边界值】
例子引出【斐波那契数列】
question:十级台阶,从下往上走,一次只能走一个或两个台阶,走法的总和。
answer:【逆向分析】
1.最优子结构:走到第十个台阶的最后一步必须走到第八或第九
目标问题:f(10) = f(8) + f(9)/ f(n)表示走到第n级台阶的走法。
小目标问题:f(9) = f(8) + f(7)/ f(n) 同时保证了无后效性。
2.状态转移方程: f(n) = f(n-1) + f(n-2)
3.边界值:f(1) = 1, f(2)=2。
递归方式会产生递归树:导致重复计算,最终时间复杂度为2^N(N为递归树的深度)
递推方式:通过滚动数组来储存最终结果。
【正向作答】的coding思想
int get_count(int n) //n为台阶的数量
{
if(n==1) //异常输出
{
return 1;
}
else if(n==2)
{
return 2; //异常机制输出
}
else
{
int* result = new int[3];
result[0] = 0; //虚拟头节点用于缓存中间值
result[1] = 1; //确定边界值1
result[2] = 2; //确定边界值2
//滚动数组,通过迭代由小目标函数求出最大目标函数值
for(int i=3; i<=n; ++i)
{
result[0] = result[1];
result[1] = result[2];
result[2] = result[0]+result[1];
}
return result[2];
}
}
例题1 数组连续子序列最大和
【逆向分析】:
1.最优子结构目标问题:Max[j]表示以j结尾的最大子串和
2.转移方程:
Max[j] = nums[j] + Max[j-1] if Max[j-1]>=0 else nums[j]
3.边界值:Max[0] = nums[0];
【正向作答】
int get_max(int* nums, int length)
{
int Max = nums[0]; //边界值
int tem_max = Max; //临时值,记录当前Max的最大值
for(int i=1; i<length; ++i)
{
//递推式
if(Max<0)
{
tem_max = nums[i];
}
else
{
tem_max += nums[i];
}
//逆向分析递推式结束
if(tem_max>Max)
{
Max = tem_max; //更新当前最大值
}
//仅产生一个空间开销,利用滚动数组更新最大子序和
}
return Max;
}
数字三角形
step1 : 确定目标问题(无后效性)以MaxSum(i,j) 表示第i行j列元到底部的最大值,
step2: 确定边界值 MaxSum(i,j) = arr(i,j)
setp3: 确定转态转移方程 MaxSum(i,j) = max(MaxSum(i+1,j),MaxSum(i+1,j+1)) + arr(i,j)
#define MAX 101
int main()
{
int D[MAX][MAX] = {{7},{8,9},{9,1,0},{2,7,7,4},{4,5,2,6,5}};
int n=5;
int* MaxSum = D[n-1]; //边界值
for(int i=n-1; i>=0;--i)
{
for(int j=0;j<i;++j)
{
MaxSum[j]=max(MaxSum[j],MaxSum[j+1]) + D[i][j];
}
}
cout<<MaxSum[0]<<endl;
return 0;
}
例题3
首先来说一下什么是LIS问题:
有一个长为n的数列a0, a1, …, a(n-1)。请求出这个序列中最长的上升子序列的长度。上升子序列指的是对于任意的i<j都满足ai<aj的子序列,该问题被称为最长上升子序列(LIS,Longest Increasing Subsequence)的著名问题。
举个栗子:给你一个序列为(1,5 ,2,6,9,10,3,15),那么它的最长上升子序列为:(1,2,6,9,10,15)
step1: 确定目标问题,如果将目标问题看作下标n的映射,与前序到达转态有关,不具备无后效性。将目标问题设置为以a[j]结尾的最长子序列,最后选择其中最长的子序列,即可
step2:Maxlen(0) = 1
step3:Maxlen(k) = max{Maxlen(i):0<i<k且a[i]<a[k]} + 1
最后返回Maxlen(k)数组中的最大值即可
#define MAX_N 100
int main()
{
int b[5] = {1,12,4,5,7};
int Maxlen[5] = {1,1,1,1,1};
Maxlen[0] = 1; //边界值
for(int i=1;i<5;++i)
{
int tep_max = 0; //临时值,接住0---j-1的tep最大值
for(int j=0; j<i; ++j)
{
if(b[j]<b[i])
{
if(tep_max<Maxlen[j])
{
tep_max = Maxlen[j];
}
}
}
Maxlen[i] = tep_max + 1;
}
int tep_maxlen =0; //
for(int i=0; i<5; ++i)
{
if(Maxlen[i]>tep_maxlen)
{
tep_maxlen = Maxlen[i];
}
}
cout<<tep_maxlen<<endl;
return 0;
}
最长公共子序列
abcfbc agfcab ----> 4
step1: 确定目标问题:Maxlen(i,j) 表示以a[i],b[j]结尾的子串的最长公共子序列
step2: 边界值,Maxlen(0,j) = 0,j=[0,m]; Maxlen(i,0) =0,i=[0,n]
step3:转态转移方程
Maxlen(i+1,j+1) = Maxlen(i-1,j-1) +1 if a[i] = b[j] else max(Maxlen(i-1,j),Maxlen(i,j-1))
#include<iostream>
using namespace std;
#include<algorithm>
#define MAX 101
#include<string>
int main()
{
string s1 = "abcfbc";
string s2 = "abfcabz";
int MaxLen[5][6]; //目标函数值
//边界值条件
for(int i=0; i<5; ++i)
{
MaxLen[i][0] = 0;
}
for(int j=0; j<6; ++j)
{
MaxLen[0][j] = 0;
}
//递推方程
for(int i=0; i<5; ++i)
{
for(int j=0; j<6; ++j)
{
if(s1[i]==s2[j])
{
MaxLen[i][j] = MaxLen[i-1][j-1] + 1;
}
else
{
MaxLen[i][j] = max(MaxLen[i-1][j], MaxLen[i][j-1]);
}
}
}
cout<<MaxLen[4][5]<<endl;
return 0;
}