动态规划是一种非常精妙的算法思想,它没有固定的写法,极其灵活,常常需要具体问题具体分析
什么是动态规划
动态规划(Dynamic Programming,DP)是一种用来解决一类最优化问题的算法思想。简单来说,动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解,需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样,当下一次又碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。
一般可以使用递归或递推的写法来实现动态规划,其中递归写法在此处又称作记忆化搜索
动态规划的递归写法
以斐波那契(Fibonacci)数列为例,斐波那契数列的定义为F0=1,F1=1,Fn=Fn-1+Fn-2(n>=2)
int F(int n){
if(n==0||n==1) return 1;
else return F(n-1)+F(n-2);
}
事实上,这个递归会有很多重复的计算,由于没有保存中间计算的结果,实际复杂度会高达O(2^n),即每次都会计算F(n-1)和F(n-2)这两个分支,基本不能承受n较大的情况。
为了避免重复计算,可以开一个一维数组dp,用以保存已经计算过的结果,其中dp[n]记录F(n)的结果,用dp[n]==-1表示F(n)当前还没有被计算过
int dp[MAXN];
然后就可以在递归当中判断dp[n]是否是-1;如果不是-1,说明已经计算过F(n),直接返回dp[n]就是结果,否则,按照递归式进行递归,代码如下:
int F(int n){
if(n==0||n==1) return 1;//递归边界
if(dp[n]!=-1) return dp[n];//已经计算过,直接返回结果,不必重复计算
else{
dp[n]=F(n-1)+F(n-2);//计算F(n),并保存至dp[n]
return dp[n];
}
}
这样就把已经计算过的内容保存了下来,于是当下次再碰到需要计算相同的内容时,就能直接使用上次计算的结果,这可以省区大半无效计算,而这也是记忆化搜索这个名字的由来。通过记忆化搜索,把复杂度从O(2^n)降到了O(n),也就是说,用一个O(n)空间的力量让复杂度从指数级别下降到了线性级别。
通过上面的例子可以引申出一个概念:如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题。动态规划通过记录重叠子问题的解,来使下一次碰到相同的子问题时直接使用之前记录的结果,以此避免大量的重复计算。因此,一个问题必须拥有重叠子问题,才能使用动态规划去解决
动态规划的递推写法
以经典的数塔问题为例,将一些数字排列成数塔的形状,其中第一层有一个数字,第二层有两个数字,第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层的两个数字中的一个:问:最后将路径上所有数字相加后得到的和最大时多少
令dp[i][j]表示从第i行第j个数字出发到达最底层的所有路径中得到的最大和
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
把dp[i][j]称为问题的状态,而把上面的式子称为状态转移方程,它把状态dp[i][j]转移为dp[i+1][j]和dp[i+1][j+1]。可以发现,状态dp[i][j]只与第i+1层的状态有关,而与其他层的状态无关,这样层号为i的状态可以由层号为第i+1的两个子状态得到。那么,递归边界是什么呢?可以发现,数塔的最后一层dp的值等于元素本身,即dp[n][j]=f[n][j],(1<=j<=n),把这种可以直接确定结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组
这样就可以从最底层各位的dp值开始,不断往上求出每一层各位置的dp值,最后就会得到dp[1][1],即问题的解
这种思路的代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000;
int f[maxn][maxn],dp[maxn][maxn];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++)
cin>>f[i][j];
}
//边界
for(int j=1;j<=n;j++)
dp[n][j]=f[n][j];
//从第n-1层不断往上计算出dp[i]]j]
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];
}
}
printf("%d\n",dp[1][1]);//所需的答案
return 0;
}
使用递归也可以实现上面的例子,即从dp[1][1]开始递归,直到到达递归边界时返回结果,
两者的区别在于:
使用递推写法的计算方法是自底向上,即从下边界开始,不断向上解决问题,直到解决了目标问题;而使用递归方式的计算方法是自顶向下,即从目标问题开始,将它分解为成子问题的组合,直到分解至边界为止
通过上面的例子再引申出一个概念:如果一个问题的最优解可以由其子问题的最优解有效的构造出来,那么称这个问题具有最优子结构,最优子结构保证了动态规划中原问题的最优解可以由子问题的最优解推导出来。因此,一个问题必须拥有最优子结构才能由动态规划去解决。
一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划法去解决
概念的区别:
1、分治与动态规划:分治和动态规划都是将问题分解为子问题,然后合并子问题的解得原问题的解,但是不同的是,分治法分解出来的子问题是不重叠的,因此分治法解决的问题不拥有重叠子问题,而动态规划解决的问题拥有重叠子问题。例如,归并排序和快速排序都是使用的是分治法。另外:分治法解决的问题不一定是最优化问题,而动态规划解决的问题一定是最优化问题。
2、贪心与动态规划:贪心和动态规划都要求原问题必须拥有最优子结构,二者的区别在于:贪心法采用的计算方式类似上面的自顶向下,但是并不等于子问题求解完毕后再去选择哪一个,而是通过一种策略直接选择一个子问题去求解,没有被选择的子问题就不求解了,直接抛弃,也就是说,它总是只在上一步选择的基础上继续选择,因此整个过程是一种单链的流水方式,显然这种情况的正确性需要用归纳法去证明。