DP1 动态规划概述
1 常见动态规划类型
- 坐标型动态规划
- 序列型动态规划
- 划分型动态规划
- 区间型动态规划
- 背包型动态规划
- 最长序列型动态规划
- 博弈型动态规划
- 综合型动态规划
2 动态规划题目特点
- 计数
- 有多少种方式走到右下角
- 有多少种方法选出k个数使得和为Sum
- 求最大值最小值
- 从左上角走到右下角路径的最大数字和
- 最长上升子序列长度
- 求存在性
- 取石子游戏,先手是否必胜
- 能不能选出k个数使得和是Sum
3 动态规划解题步骤
-
确定状态
简单的说,就是解动态规划时需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似解数学题中,xyz代表什么一样,具体分为下面两个步骤:
- 研究最优策略的最后一步
- 化为子问题
-
转移方程
根据子问题定义直接得到
-
初始条件和边界情况
初始条件一般都是a[0]、a[1]这种,多看看边界条件主要是看数组的边界,数组越不越界
-
计算顺序
利用之前的计算结果,一般从左到右,从上到下
4 动态规划实例
4.1 硬币问题
题目:有三种硬币,面值2.5.7,买一本书需要27元,如何用最少的硬币刚好付清。
-
确定状态
最后一步:虽然我们不知道最优策略是什么,但是最优策略一定是k枚硬币\(a_1,a_2…a_k\)加起来等于27,所以一定有一枚最后的硬币\(a_k\),除掉这枚硬币,前面硬币的面值相加起来是\(27-a_k\)
转化为子问题:所以就将原问题转化为了子问题如下
原问题是最少用多少枚硬币拼出27(k枚)
子问题是最少用多少枚硬币拼出\(27-a_k\)(k-1枚)经过这两歩,得出状态:f[X]=最少用多少枚硬币拼出X
-
转移方程
\[f\lbrack x\rbrack=min\{f\lbrack x-2\rbrack+1,f\lbrack x-5\rbrack+1,f\lbrack x-7\rbrack+1\} \]\(f\lbrack x\rbrack\)指最少用多少枚硬币拼出x,由于最后一步\(a_k\)有三种情况,最后一枚硬币可能是2,5,7中的一种,所以转移方程应该是取三种情况中能够拼出x使用硬币最少的那种情况
此时,可以用递归进行解题
public int f(int x){ if(x==0) return 0; int res = Integer.Max_Value; if(x>=2) res = Math.min(f(x-2)+1,res); if(x>=5){ res = Math.min(f(x-5)+1,res); if(x>=7){ res = Math.min(f(x-7)+1,res); return res; } }递归是从上到下进行计算的,这样的话会产生大量的重复运算,解决方法是采用动态规划,将计算结果保存下来,改变计算顺序
-
初始条件和边界情况
边界条件X-2,X-5,X-7小于0时,应该进行处理,这种情况其实就是拼不出来的情况,定义为正无穷
初始条件一般就是根据转移方程计算不出来的值,从转移方程变量为0或1来选,根据题目进行分析,这个题目的初始条件就是\(f\lbrack 0\rbrack=1\),代入公式的话应该f[0]为正无穷,显然错误,所以自己定义\(f\lbrack 0\rbrack=1\)
-
计算顺序,从小到大进行进行
\[f\lbrack x\rbrack=最少用多少枚硬币拼出x \]\[f\lbrack x\rbrack=\infty 表示无法用硬币拼出x \]
public int coinChange(int[] A,int M){
int[] f = new int[M+1];
f[0]=0;
for(int i=1;i<=M;i++){
f[i] = Integer.MAX_VALUE;
for(int j=0;j<A.length;j++){
//第一个条件防止数组越界,第二个条件防止Integer.MAX_VALUE+1越界
if(i-A[j]>=0&&f[i - A[j]] != Integer.MAX_VALUE){
f[i] = Math.min(f[i],f[i-A[j]]+1);
}
}
}
if(f[M]==Integer.MAX_VALUE)
return -1;
else
return f[M];
}
4.2 机器人路径问题
题目:给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步,问有多少种不同的方式走到右下角。
-
确定状态
最后一步:聚焦机器人最后挪动的一歩,右下角坐标为(m-1,n-1),那么前一步机器人一定在(m-2,n-1)或者(m-1,n-2)子问题:可得,机器人走到(m-1,n-1)的方式等于机器人走到(m-2,n-1)加上机器人走到(m-1,n-2)
原问题是有多少种方式从左上角走到(m-1,n-1)
子问题是有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)经过这两歩,得出状态:\(f\lbrack i\rbrack\lbrack j\rbrack\)为机器人有多少种方式从左上角走到(i,j)
-
转移方程
\[f\lbrack i\rbrack\lbrack j\rbrack=f\lbrack i-1\rbrack\lbrack j\rbrack+f\lbrack i\rbrack\lbrack j-1\rbrack \] -
初始条件和边界情况
边界条件:i=0或者j=0时前一步只能从一个方向过来,所以f[i][j]=1初始条件:f[0][0]=1,机器人只有一种方式到左上角
-
从上到下,从左到右
public class Solution {
/**
* @param m: positive integer (1 <= m <= 100)
* @param n: positive integer (1 <= n <= 100)
* @return: An integer
*/
public int uniquePaths(int m, int n) {
int[][] f = new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0||j==0)
f[i][j]=1;
else
f[i][j]=f[i-1][j]+f[i][j-1];
}
}
return f[m-1][n-1];
}
}
4.3 青蛙跳石头问题
题目:有n块石头分别放在x轴的0,1,…,n-1位置,一只青蛙在石头0,想要跳到石头n-1,若青蛙在第i块石头上,他最多可以向右跳距离\(a_i\),问青蛙能否跳到石头n-1。
-
确定状态
最后一步:最后一步是从i跳过来的,i<n-1
需要满足两个条件:
青蛙可以跳到石头i;最后一步不能超过跳跃的最大距离,即i+\(a_i\)>=n-1化为子问题:所以就将原问题转化为了子问题
原问题是青蛙能不能跳到石头n-1
子问题是青蛙能不能跳到石头i -
转移方程
\[f\lbrack j\rbrack=OR_{0<=i<j}\{f\lbrack i\rbrack\;and\;i+a\lbrack i\rbrack>=j\} \]\(f\lbrack j\rbrack\)表示青蛙能不能跳到j位置,若在所有小于j的位置i中,至少有一个位置能到j,则j是可以到达的
-
初始条件和边界情况
边界条件:枚举的i和j都不会越界,所以没有边界条件初始条件:f[0]=true,因为青蛙一开始就在石头0上
-
计算顺序
从左到右计算
public class Solution {
/**
* @param A: A list of integers
* @return: A boolean
*/
public boolean canJump(int[] A) {
boolean[] f = new boolean[A.length];
f[0]=true;
for(int j=1;j<A.length;j++){
f[j]=false;
for(int i=0;i<j;i++){
if(f[i]&&i+A[i]>=j){
f[j]=true;
break;
}
}
}
return f[A.length-1];
}
}
4.4 剪绳子问题
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
-
确定状态
最后一步:假设绳子剪得最后一下的长度为j,只需要满足j<i即可
子问题:相当于从1开始遍历j,找出 j*(i-j部分的乘积最大值)的最大值
原问题是长度为i的乘积最大值
子问题是长度为i-j的乘积最大值 -
转移方程
\[f\lbrack i\rbrack=max_{1<=j<i}\{f\lbrack i-j\rbrack\ast j\} \] -
初始条件和边界情况
当n=2,3时,绳子不剪断的长度较长,按照题目的要求,绳子必须间断,但是当n>4进行迭代时,应该用绳子不剪断的长度进行迭代,因此应将n=2,3单独处理,而f[2],f[3]中存放的是不剪断的长度,以便用于迭代
-
计算顺序
从f[4]开始向右计算
class Solution {
public int cuttingRope(int n) {
if(n==2)
return 1;
else if(n==3)
return 2;
int[] f = new int[n+1];
for(int i=1;i<=3;i++)
f[i]=i;
for(int i=4;i<=n;i++){
f[i]=0;
int res = f[i];
for(int j=1;j<i;j++){
f[i] = Math.max(f[i],f[i-j]*j);
}
}
return f[n];
}
}
4.5 连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
-
确定状态
最后一步:假设当前要判断的是数组第i个,最后一步就是i,数组下标是i-1
子问题:找出到第i-1个为止的连续子数组最大和,看其是否为正或为负,再判断
原问题是到数组第i个的连续子数组的最大和
子问题是到数组第i-1个的连续子数组的最大和经过这两歩,得出状态:f[i]表示前i个元素的连续子数组的最大和,结尾元素为nums[i-1]
-
转移方程
\[f\lbrack i\rbrack=max\{nums\lbrack i\rbrack,nums\lbrack i\rbrack+f\lbrack i-1\rbrack\} \] -
初始条件和边界情况
f[0]=nums[0]
-
计算顺序
从f[1]开始计算
class Solution {
public int maxSubArray(int[] nums) {
int[] f = new int[nums.length];
f[0]=nums[0];
int res = f[0];
for(int i=1;i<nums.length;i++){
f[i]=Math.max(nums[i],nums[i]+f[i-1]);
res = Math.max(res,f[i]);
}
return res;
}
}

浙公网安备 33010602011771号