动态规划类题目总结
动态规划一直是自己的弱项,当时学01背包都学了好久,特别是遇到动态规划题的时候,看得懂答案,但就是不会做。
动态规划的三大步骤:
同台规划的思想:利用历史记录,避免我们重复计算。而这些历史记录需要用一位数组或二维数组保存。
第一步:定义数组元素含义。如:$dp[i]$是什么意思?
第二步:找出数组元素的关系式。类似高中的数学归纳法。
如:
这一步也是最难的一步。
第三步:找出初始值。虽然我们知道了数组元素之间的关系式,例如 $dp[n] = dp[n-1] + dp[n-2]$,但一直推下去的话,会由 $dp[3] = dp[2] + dp[1]$。 $dp[2]$ 和 $dp[1]$ 是不能再分解的了,这就是初始值。
案例:
案例一:简单一维DP
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
- 定义数组元素
定义 $dp[i]$ 的含义为:跳上一个 i 级的台阶总共有 $dp[i]$ 种跳法。 找出元素间的关系式
青蛙可以选择跳一级,也可以选择跳两级,所以青蛙到达第 n 级的台阶有两种方式- 一种是从第 n-1 级跳上来
- 一种是从第 n-2 级跳上来
由于我们是要算所有可能的跳法的,所以有 $dp[n] = dp[n-1] + dp[n-2]$。
- 找出初始条件
显然,$dp[2] = 2$, $dp[1]=1$,$ dp[0]=0$
注意,初始化一定要找全,不能把$dp[2]$漏掉!
代码如下:
#include<iostream>
using namespace std;
int main(){
int dp[105];
int n;
cin>>n;
dp[0]=1;
dp[1]=1;
for(int i = 2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
cout<<dp[n]<<endl;
return 0;
}
案例二:二维数组DP
一个机器人位于一个 $m \times n$ 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
这是某年蓝桥杯的一个省赛题,也是leetcode第62题。
- 定义数组元素
定义 $dp[i]$ 的含义为:当机器人从左上角走到$(i, j)$ 这个位置时,一共有 $dp[i] [j]$ 种路径。 找出元素间的关系式
机器人到达$(i,j)$有两种方式- 从$(i-1,j)$ 这个位置走一步到达
- 从$(i,j-1)$ 这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 $dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]$。
- 找出初始条件
$dp[0] [0….n-1] = 1$; 相当于最上面一行,机器人只能一直往右走
$dp[0…m-1] [0] = 1$; 相当于最左面一列,机器人只能一直往下走
代码如下:
#include<iostream>
using namespace std;
long long dp[10][10];
int main(){
int m = 3;
int n = 7;
dp[1][1]=1;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
dp[i][1]=1;
dp[1][j]=1;
}
}
for(int i = 2;i<=m;i++){
for(int j = 2;j<=n;j++){
if(i==1){
dp[i][j]=dp[i][j-1];
}
else if(j==1){
dp[i][j]=dp[i-1][j];
}
else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
cout<<dp[i][j]<<"\t";
}
cout<<endl;
}
}
案例三:二维数组DP
给定一个包含非负整数的 $m \times n$ 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
举例:
输入:
arr = [
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
这是leetcode第64题。
- 定义数组元素
定义 $dp[i]$ 的含义为:当机器人从左上角走到$(i, j)$ 这个位置时,最下的路径和是 $dp[i] [j]$ 找出元素间的关系式
机器人到达$(i,j)$有两种方式- 从$(i-1,j)$ 这个位置走一步到达
- 从$(i,j-1)$ 这个位置走一步到达
因为是计算哪一个路径和是最小的,所以要从这两种方式中,选择一种,使得$dp[i] [j] $的值是最小的,显然有:
dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格种的值
- 找出初始条件
$dp[0] [0….n-1] = 1$; 相当于最上面一行,机器人只能一直往右走
$dp[0…m-1] [0] = 1$; 相当于最左面一列,机器人只能一直往下走
代码如下:
#include<iostream>
using namespace std;
int main(){
int grid[201][201];
long long dp[201][201];
int m=2;
int n=3;
for(int i=1;i<=m;++i){
for(int j = 1;j<=n;++j){
cin>>grid[i][j];
}
}
dp[1][1]=1;
for(int i = 2;i<=m;++i){
dp[i][1]=dp[i-1][1]+grid[i][1];
}
for(int j = 2;j<=n;++j){
dp[1][j]=dp[1][j-1]+grid[1][j];
}
for(int i=2;i<=m;++i){
for(int j = 2;j<=n;++j){
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
cout<<dp[m][n];
}
案例四:编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
插入一个字符、删除一个字符、替换一个字符。
示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
这个题自己自己琢磨了一晚上,在leetcode第72题,是hard级别。
经验:90%的字符串问题都可以用动态规划解决,并且90%是采用二维数组。
- 定义数组元素
定义$ dp[i]$ 的含义为:当字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,将 word1 转化为 word2 所使用的最少操作次数为$ dp[i] [j]$。 找出元素间的关系式
由于我们是要让操作的次数最小,所以我们要寻找最佳操作。那么有如下关系式:- 如果我们 word1[i] 与 word2 [j] 相等,这个时候不需要进行任何操作,显然有 $dp[i] [j] = dp[i-1] [j-1]$。(别忘了$ dp[i] [j]$ 的含义哈)。
- 如果我们 word1[i] 与 word2 [j] 不相等,这个时候我们就必须进行调整,而调整的操作有 3 种,我们要选择一种。三种操作对应的关系试如下(注意字符串与字符的区别):
(1)如果把字符 word1[i] 替换成与 word2[j] 相等,则有 $dp[i] [j] = dp[i-1] [j-1] + 1$;
(2)如果在字符串 word1末尾插入一个与 word2[j] 相等的字符,则有$ dp[i] [j] = dp[i] [j-1] + 1$;
(3)如果把字符 word1[i] 删除,则有$ dp[i] [j] = dp[i-1] [j] + 1$;
那么我们应该选择一种操作,使得$ dp[i] [j]$ 的值最小,显然有
$dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1$;
- 找出初始条件
显然,当$ dp[i] [j] $中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的$ dp[0] [0….n] $和所有的 $dp[0….m] [0]$。这个还是非常容易计算的,因为当有一个字符串的长度为 0 时,转化为另外一个字符串,那就只能一直进行插入或者删除操作了。
代码如下:
#include<iostream>
#include<cstring>
using namespace std;
char a[505]="";
char b[505]="";
int dp[501][501];
int main(){
cin>>a;
cin>>b;
int m = strlen(a);
int n = strlen(b);
for(int i = 0;i<=m;i++){
dp[i][0]=i;
}
for(int j = 0;j<=n;++j){
dp[0][j]=j;
}
for(int i = 1;i<=m;++i){
for(int j = 1;j<=n;++j){
if(a[i-1]==b[j-1]){
dp[i][j]=min(min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]);
}
else{
dp[i][j]=min(min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+1);
}
}
}
for(int i = 0;i<=m;++i){
for(int j = 0;j<=n;++j){
cout<<dp[i][j]<<"\t";
}
cout<<endl;
}
return 0;
}
加油吧,道阻路长。
NB