递归和动态规划是算法界的两个扛把子,想进入算法之门,则必须理解、掌握这两种算法的本质。一旦参悟透这2种算法的精髓,再加上对树、图等复杂数据结构的深入理解,可以解决大部分的算法问题。
本文通过几个典型案例,再次聊聊动态规划算法。其实动态规划算法也就 2 把刷子。
动态规划的基本理念:步步为营,每次选择最好,达到最终是最好的。
如下通过几个案例来理解动态规划如何步步为营。
A现假设有一个特殊的键盘包含下面的按键:
Key 1: (A):在屏幕上打印一个A。Key 2:(ctrl-A):选中整个屏幕。Key 3: (ctrl-c):复制选中区域到缓冲区Key 4: (ctrl-v) : 将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。现在,你只可以按键 N 次(使用上述四种按键) ,请问屏幕上最多可以显示几个 A呢?
1输入:N =3 输出:3解释:我们最多可以在屏幕上显示三个,A通过如下顺序按键:A,A,A
2输入:N = 7输出:9解释:我们最多可以在屏幕上显示 9 个,A通过如下顺序按键A,A,A,ctrl-A,ctrl-c,ctrl-V, ctrl V
本题是求最值问题,可使用动态规划实现。如前所说,要使用动态规划算法,首先要知道对于每一个子问题而言有哪些选择项。
Tips: 于本题而言,不同的按键次数可以认为是一个个子问题。
在屏幕上输出A,也就是让屏幕上的A字符的个数发生变化,可以有2种选择:
A键。只需要一次按键就能输出`A`。A。先按下ctrl+A、ctrl+C,在缓冲区添加内容 ,然后可以重复按ctrl+v在屏幕上输出字母A。则在不同的按键次数下,哪一种选择最佳?
本题中动态规划算法要做的是:
A的个数。解题流程:
可以先定义一个一维 dp 数组。用来存储不同次数状态下子母A的个数。

现分析在不同次数下,哪一种选择方案可得到最理想结果。
1时。此状态下只可能通过按下A键输出子母A。
2时。也只能通过直接按下A键输出子母A,这时屏幕上的字母个数为 dp[2]=dp[1]+1。
3时。通过直接按`A`键,也可以通过复制输出A 。
直接按下A键的个数为dp[3]=dp[2]+1。
复制输出`A`。因复制的前置条件是要先按下`ctrl+A、ctrl+C`,意味着需要消耗掉2次按键,`ctrl+V`复制的内容应该是`dp[0]`位置字母`A`的个数。

显然,直接按键输出的字母A更多。在两个方案中选择直接按下子母键为最佳方案。

4时。
直接按下A键输入A,此时屏幕上的A字符为4个。
使用复制方案在屏幕上输出A时。复制方式有 2 种:
在dp数组位置1处ctrl+A、在2处ctrl+C。这样复制的是dp[0]位置的子母个数。

在dp数组位置2处ctrl+A、在位置3处ctrl+C。这样复制的是dp[1]位置的子母个数。

编码实现:
#include <iostream>
using namespace std;
//一维动态数组
int dp[100]= {0};
//按键次数
int n;
int main(int argc, char** argv) {
cin>>n;
for(int i=1; i<=n; i++) {
//直接按下A键
dp[i]=dp[i-1]+1;
//如果复制
for( int j=2; j<i; j++ ) {
dp[i]=max( dp[i],dp[j-2]*(i-j+1) );
}
}
cout<<dp[n];
return 0;
}
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
[10,9,2,5,3,7,101,18]4解释:最长的上升子序列是 [2,3,7,101],它的长度是 4。说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。子序列和子串的区别,子串是连续的,子序列不一定是连续的。
### 3.2 问题分析
如何使用动态规划思想解决此问题。
dp数组。记录当数组中的数据规模变化时,其子序列的长度。初始值为 1,数列是自己的子序列。
10时,显然,其子序列的个数为 1。
9时,将其和前面的 10 进行比较,因比其小,故9不能为递增子序列做出贡献,保留原来子序列的个数。
2时,其对应dp数组中的值为 1。
5时,其比10、9小,但比2大,可以成为以2为当前状态值的递增子序列。
3时,因3只比2大,此时最长子序列应该是以2结束时的最长子序列加1。
7时,因 7比2,5,3都大,则需要在以2、5、3结束时最长子序列中求最大值。动态规划的特点就是,状态的改变时,往往需要在多个选择中选择最佳的。
101,因为它比前面的所有数字都大,则需要在已经填充的dp数组中找出最大值且再加 1。
dp数组中的值应该如下所示。
#include <iostream>
#include <cstring>
using namespace std;
//原始数组
int nums[]= {10,9,2,5,3,7,101,18};
//一维动态数组
int dp[100]= {1};
int main(int argc, char** argv) {
int size=sizeof(nums)/sizeof(int);
//最长子序列
int maxVal=1;
//遍历原始数组
for( int i=0; i<size; i++ ) {
dp[i]=1;
//以 i 为当前位置,向原始数组之间扫描
for( int j=i-1; j>=0; j-- ) {
if( nums[i]>nums[j] ) {
dp[i]=max(dp[i] ,dp[j]+1 ) ;
}
}
if(maxVal<dp[i]) maxVal=dp[i];
}
for(int i=0; i<size; i++)
cout<<dp[i]<<"\t";
cout<<"\n最长子序列长度:"<< maxVal<<endl;
return 0;
}
现有一个二维数组nums,其中的元素都是非负整数,现在你站在左上角,只能向右或者向下移动,需要到达右下角。现在请你计算,经过的路径和最小是多少?
二维数组如下图所示。

本题是典型的动态规划类题型。
基本流程如下:
dp数组。存储出发位置到表格中每一个位置的最短路径之和。动态规划的基本套路就是步步为营,如果能保证从出发点到每一个位置的路径和都是最小的,自然能求解出到目标地的最短路径和。
dp表先填充一些显然易见的值,也称为base case。如出发点的最短路径是本身,如下图所示:
dp表中的第一行的值,只受左边值的影响,不存在多个选择,也容易找出来。其值为dp[0][i]=dp[0][i-1]+nums[0][i]。
dp表中的第一列的值只受上边值的影响,也不存在多个选择,其值为dp[i][0]=dp[i-1][0]+nums[i][0]。
A位置可以有2 个选择,选择其中较小的值。



#include <iostream>
#include <cstring>
using namespace std;
//原始数组
int nums[3][3]= { {2,4,3},
{7,6,2},
{1,8,5},
};
//二维动态数组
int dp[3][3]= { {0,0,0},
{0,0,0},
{0,0,0},
};
int main(int argcwfh, char** argv) {
//起始位置的路径路径是本身
dp[0][0]=nums[0][0];
//第一行值只受左边值影响
for(int i=1; i<3; i++ ) {
dp[0][i]=dp[0][i-1]+nums[0][i];
}
//第一列值只受上边影响
for(int i=1; i<3; i++ ) {
dp[i][0]=dp[i-1][0]+nums[i-1][0];
}
//由上向下
for(int i=1; i<3; i++) {
for(int j=1; j<3; j++ ) {
dp[i][j]=nums[i-1][j-1]+min(dp[i][j-1],dp[i-1][j]);
}
}
for(int i=0; i<3; i++) {
for(int j=0; j<3; j++ ) {
cout<<dp[i][j]<<"\t";
}
cout<<endl;
}
return 0;
}
递归、动态规划是算法世界的两大剑客,两者互通款曲,解决同一个问题时,一个站在问题域的正方向,一个站在问题域的反方向。灵活运用且掌握这两大算法,是通向算法界的必修之路。