动态规划
动态规划题目类型特点
- 计数
- 有多少种方式走到右下角
- 有多少种方法选出k个数使得和是sum
- 求最大最小值
- 从左上角走到右下角路径的最大数字和
- 最长上升子序列长度
- 求存在性
- 去石子游戏,先手是否必胜
- 能不能选出k个数使得和是sum
类型一:求最大最小值
例题:LintCode 669:Coin Change
动态规划组成部分一:确定状态 (重点)
- 状态在动态规划中的作用属于定海神针
- 简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么
- 类似于解数学题中,X,Y,Z代表什么
- 一个状态一个未知数,几个未知数就开几维数组
- 数组的变量就是状态在变化,每个数组的值就是记录我们想要的的结果。
- 确定状态需要两个意识
- 最后一步
- 子问题
最后一步
-
虽然我们不知道最优策略是什么,但是最优策略肯定是k枚硬币 a 1 a_1 a1, a 2 a_2 a2,…, a 2 a_2 a2面值加起来是27
-
所以一定有一枚最后的硬币: a k a_k ak
-
出掉这枚硬币,前面硬币的面值加起来是27 - a k a_k ak
关键点1
我们不关心前面的k-1枚硬币是怎么拼出27 - a k a_k ak的(可能有一种拼法,可能有100种),而且我们现在甚至还不知道 a k a_k ak和k,但是我们确定前面的硬币拼出了27 - a k a_k ak
关键点2
因为是最优策略,所以拼出27 - a k a_k ak 的硬币数一定要最少,否则这就不是最优策略了
假如五个硬币是最优的策略,那么如果扣掉最后一个 a k a_k ak,剩下的最少也要有四枚硬币拼成,否则其最后的五枚硬币就不是最优的解了
子问题
-
所以我们就要求:最少用多少枚硬币可以拼出27 - a k a_k ak
-
原问题是最少用多少枚硬币拼出27
-
我们将原问题转化成了一个子问题,而且规模更小:27 - a k a_k ak
-
为了简化定义,我们设状态f(x) = 最少用多少枚硬币拼出X
-
等等,我们还不知道最后那枚硬币 a k a_k ak是多少
-
最后那枚硬币 a k a_k ak只可能是2,5,7
-
如果 a k a_k ak是2,f(27)应该是f(27-2)+1(加上最后这一枚硬币2)
-
如果 a k a_k ak是5,f(27)应该是f(27-5)+1(加上最后这一枚硬币5)
-
如果 a k a_k ak是7,f(27)应该是f(27-7)+1(加上最后这一枚硬币7)
-
除此之外,没有其他可能了
-
需要求最少的硬币数,所以:
动态规划组成部分二:转移方程(重点)
动态规划组成部分三:初始条件和边界情况
-
两个问题:x - 2, x - 5或者 x - 7小于0怎么办?什么时候停下来?
-
如果不能拼出Y,就定义f[Y] = 正无穷
- 例如f [-1]= f [-2] = … =正无穷
-
所以f[1] = min{f[-1] + 1, f[-4] + 1, f[-6] + 1} = 正无穷,表示拼不出来1
-
初始条件:f[0] = 0;
初始条件就是用转移方程算不出来的,需要手工定义例如:f[0]
动态规划组成部分四:计算顺序
- 拼出x所需要的最少硬币数: f[x] = min{f[x-2]+1,f[x-5]+1,f[x-7]+1}
- 初始条件:f[0] = 0
- 然后计算f[1],f[2],…,f[27]
- 当我们计算到f[x]时,需要用到f[x-2],f[x-5],f[x-7],不过他们都已经得到结果了,这就是我们需要的计算顺序
代码:
#include<iostream>
using namespace std;
/*
* dp[n]表示凑齐n元最少需要凑多少枚硬币
* 最后一步有三种情况, dp[22] + 1 dp[20] + 1 dp[25] + 1
* 对于最后一步都是加一枚硬币区别只是硬币的面值不一样,关键在于dp[22] dp[20] dp[25] 那个更小
* 然后我们就从求dp[27] -> 求dp[22] dp[20] dp[25] 这就是子问题
* 我们只要将 27 22 20 25这些数字用未知数,代替就可以了,循环一下就完成了
*/
int main(){
int way[3] = {2, 5, 7};
int dp[28] = 无穷大;
dp[0] = 0;
for(int i = 1; i < 28; i++){
for(int j = 0, j < 3; j++){
if(i - way[j] >= 0 && dp[i - way[j]] != 无穷大)
dp[i] = min(dp[i - way[j]] + 1,dp[i]);
}
}
if(dp[27] == 无穷大) cout<<-1<<endl;
cout<<dp[27]<<endl;
}
类型二:计数
例题:LintCode 114: Unique Paths
动态规划组成部分一:确定状态
- 确定最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一步:向右 或者 向下
- 右下角坐标设为(m-1,n-1)
- 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)
子问题
-
那么,如果机器人有x种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有x+y种方式走到(m-1,n-1)
思考:为什么是x+y
加法原理: 加法原理是分类计数原理,常用于排列组合中,具体是指:做一件事情,完成它有n类方式,第一类方式有 M 1 M_1 M1种方法,第二类方式有 M 2 M_2 M2种方法,……,第n类方式有 M n Mn Mn种方法,那么完成这件事情共有 M 1 M_1 M1+ M 2 M_2 M2+……+ M n M_n Mn种方法。
注意点:
1.无重复
2.无遗漏
-
问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和 (m-1,n-2)
-
原题要求有多少种方式从左上角走到(m-1,n-1)
-
子问题
-
状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)
动态规划组成部分二:转移方程
动态规划组成部分三:初始条件和边界情况
- 初始条件:f[0][0] = 1,因为机器人只有一种方式到左上角
- 边界条件:i = 0 或 j = 0 ,则前一步智能有一个方向过来 —>f[i][j] = 1
动态规划组成部分:计算顺序
代码:
#include<iostream>
#include<stdio.h>
using namespace std;
/*
* map[][] 大小表示地图的大小,而map[i][j]对应的是到达其位置有多少种方法
*
* 计数类经常用到加法原理,这题也不例外:因为他行走的方式只能向右和向下
* 所以:到达一个位置的方法只有两种可能,在他的上方或者在他的左边
* 加法原理:到达map[i][j]有两种形式 从map[i-1][j]向下走 和 map[i][j-1]想右走
* 那么到达 map[i][j]的方法便是两种形式的和。所以对于每个位置都可以这样递推
*/
int main()
{
int map[4][8];
for(int i = 1; i <= 3; i++){
for(int j = 1; j <= 7; j++){
if(i == 0 || j == 0){
map[i][j] = 1;
}else{
map[i][j] = map[i][j-1] + map[i-1][j];
}
}
}
cout<<map[3][7]<<endl;
return 0;
}
类型三:求存在性
例题:
LintCode 116 Jump Game
动态规划组成部分一:确定状态
子问题
动态规划组成部分二:转移方程
动态规划组成部分三:初始条件和边界情况
动态规划组成部分四:计算顺序
代码:
#include<iostream>
using namespace std;
/*
* 参数A[]表示,X轴上的石子 A[n]的值表示 n这个石子可以跳的最远距离
* n 表示这个石子的位置
* f[] 对应每个石子能不能跳到 f[n]表示第n个石子能不能跳到
*/
int Jump(int A[])
{
int n = sizeof(A)/sizeof(A[0]);
int f[n];
f[0] = 1;
/*判断能不能调到j,首先认为他不能*/
for( int j = 0; j < n; j++){
f[j] = 0;
/*对j前面的每个石子都进行枚举,因为j是由前面的i跳过来的i < j*/
for( int i = 0; i < j; i++){
/*
*对j前面的每个石子i都进行判断,先判断能不能调到i,首先要能到i,然后判断本身的位置加上自己最远能 *跳的距离能到j吗,如果能到,则就说明j可以调到,改为true
*/
if(f[i] && i + A[i] >= j){
f[j] = 1;
break;
}
}
}
return f[n-1];
}
int main()
{
}