常见的几种类型(动态规划的题目特点) 1. counting 计数 -有多少种办法走到右下角
-有多少种办法选出k个数使得其和为Sum
2.最大最小值 -从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3.求存在性 -取石子游戏,先手是否必胜
-能不能选出k个数的和是sum
例子:LintCode 669:coin change 说明:你有三枚硬币,分别面值2元,5元和7元,每种硬币都有足够多,而买一本书需要27元,如何用最少的硬币组合付清所有书钱而不需要对象找钱。
动态规划四个组成步骤:解决动态规划的四个部分 组成一:确定状态 状态的确定属于定海神针
简单来说,解动态规划的时候我们需要开一个数组,数组的每个元素f[i]或者f[i] [j]代表什么
—类似于解数学题中,x,y,z分别代表什么
确定状态需要两个意识:
-最后一步:在这个硬币题中,我们虽然不知道最优策略是什么,但是最优策略肯定是k枚硬币:$a_1,a_2,……a_k=27$,故一定有最后一枚
硬币:$a_k$,去掉这枚硬币前面所有的硬币面值加在一起是27-$a_k$
两个关键点:我们确定前面的硬币拼出了27-$a_k$,而且因为最优策略,前面用的硬币一定是最少的
-子问题:所以我们的问题就转化为最少用多少硬币可以拼出27-$a_k$,我们就将原问题化为了一个类型相似而且规模更小的子问题 了:f(27-$a_k$),而最后一枚硬币只有三种可能,如果f(i)表示i元所需要的总次数,那么我们可以得到如下:
f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
那么我们根据上面的提示自然得到了递归的函数解法:
1 2 3 4 5 6 7 8 9 10 11 12 int f (int X) { if (X==0 ) return 0 ; int res=MAX_VALUE; if (X>=2 ) return Math.min{f (X-2 )+1 ,res;} if (X>=5 ) return Math.min{f (X-5 )+1 ,res;} if (X>=7 ) return Math.min{f (X-7 )+1 ,res;} return res; }
递归解法的问题在于:重复调用(类似于分治我们会重复算所有的子情况,再说一点东西分治不能DP的原因就是因为子情况并不相同,子情况相同的时候产生的结果就是DP)
因此DP的做法:将计算结果保存下来,并改变计算顺序
组成二:转移方程 f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
组成三:初始条件和边界情况 这里的例题就有两个情况:x-2,x-5,x-7<0如何处理?什么时候才能停下来?
解决:如果不能拼出Y,就定义$f(Y)=+\infty$ -$f(-1)=f(-2)=…=+\infty$ 故f(1)=min{f(-1)+1,f(-4)+1,f(-6)+1}=+∞,表示拼不出来
初始条件:f[0]=0,初始值的物理意义明确,按照其意义算即可
组成四:计算顺序 这里:按照从小到大的顺序去算
原则:要用到一个新的状态时,其他的子状态一定已经得到结果了
计算得到: |…|f(-1)|f(0)|f(1)|f(2)|f(3)|f(4)|f(5)|f(6)|…|…|f(27)| |:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—| |$\infty$|$\infty$|0|$\infty$|1|$\infty$|2|…|…|…|…|…|
每一步尝试3种硬币:共27步,和递归相比没有任何重复的计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int coinChange (int []A,int M) { int []f=new int [M+1 ]; int n=A.length; f[0 ]=0 ; int i,j; for (i=0 ;i<=M;++i){ f[i]=Integer.MAX_VALUE; for (j=0 ;j<n;++j) { if (i>=A[j]&&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 ; return f[M]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def coinChange (self, coins: List [int ], amount: int ) -> int : length = len (coins) DP = [amount+1 ]*(amount+1 ) DP[0 ] = 0 for i in range (1 , amount+1 ): count = 0 while count < length: if i >= coins[count] and DP[i-coins[count]] != amount+1 : DP[i] = min (DP[i], DP[i-coins[count]]+1 ) count += 1 if DP[amount] == amount+1 : return -1 return DP[amount]
例子2:LintCode 114:Unique Paths 题意:有m行n列的网格,机器人从左上角(0,0)出发,每一步可以向下或者向右走一步,问多少种不同的方法走到右下角
典型的记数型动态规划
步骤:
1.确定状态:最后一步:(m-2,n-1)+(m-1,n-2)=(m-1,n-1)(最后一部分);子问题:(m-2,n-1)+(m-1,n-2)
2.转移方程:f[i] [j]=f[i-1] [j]+f[i] [j-1]
3.初始条件和边界情况
初始条件:f[0] [0]=0
边界条件:m=0,n=0;以及大于左右上下边界
答案:f[m-1] [n-1]
1 2 3 4 5 6 7 8 9 10 11 12 13 public int coinChange (int m,int n) { int [][]f=new int [m][n]; int i,j; for (i=0 ;i<m;++i){ for (j=0 ;j<n;++j) { if (i==0 ||j==0 ) f[i][j]=1 ; f[i][j]=f[i][j-1 ]+f[i-1 ][j]; } } return f[m-1 ][n-1 ]; }
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def uniquePaths (self, m: int , n: int ) -> int : DP = [[0 for i in range (n+1 )] for i in range (m+1 )] DP[1 ][1 ] = 1 for i in range (1 , m+1 ): for j in range (1 , n+1 ): DP[i][j] = DP[i][j-1 ]+DP[i-1 ][j]+DP[i][j] return DP[m][n]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution : def uniquePathsWithObstacles (self, obstacleGrid: List [List [int ]] ) -> int : m = len (obstacleGrid) n = len (obstacleGrid[0 ]) DP = [[0 for i in range (n)] for i in range (m)] count = 0 while count < n and obstacleGrid[0 ][count] == 0 : DP[0 ][count] = 1 count += 1 count = 0 while count < m and obstacleGrid[count][0 ] == 0 : DP[count][0 ] = 1 count += 1 for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i][j-1 ] != 1 : DP[i][j] += DP[i][j-1 ] if obstacleGrid[i-1 ][j] != 1 : DP[i][j] += DP[i-1 ][j] if obstacleGrid[i][j] == 1 : DP[i][j] = 0 return DP[m-1 ][n-1 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def minPathSum (self, grid: List [List [int ]] ) -> int : m = len (grid) n = len (grid[0 ]) DP = [[0 for i in range (n+1 )]for i in range (m+1 )] DP[0 ][0 ] = grid[0 ][0 ] for i in range (1 , n): DP[0 ][i] = DP[0 ][i-1 ]+grid[0 ][i] for i in range (1 , m): DP[i][0 ] = DP[i-1 ][0 ]+grid[i][0 ] for i in range (1 , m): for j in range (1 , n): DP[i][j] = min (DP[i][j-1 ], DP[i-1 ][j])+grid[i][j] return DP[m-1 ][n-1 ]
例子3:LintCode 116:Jump Game 有n块石头分别再x轴的0,1,2,……,n-1位置;一块青蛙在石头0,想跳到石头n-1,如果青蛙在第i块石头上,最多可以向右跳距离$a_i$,问请问能否跳到石头n-1上去
输入:a=[2,3,1,1,4]这样的数据
输出:True
输入:a=[3,2,1,0,4]这样的数据
输出:False
判断:存在型动态规划
步骤:
1.确定状态:最后一步:青蛙可以跳到石头i的同时最后一步距离不超过$a_i$
子问题:能不能跳到i
2.转移方程:$f[j]=OR_{0\leq i=j)$
一些解释:枚举前面所有0-j的石头,但是只需要里面的一块能够符合要求就行
3.初始条件和边界情况:
f[0]=True;因为青蛙一开始就在石头0
依次计算f[1],f[2],f[3],……f[n-1]
答案为f[n-1]
TC:$O(N^2)$ SC:O(N)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public boolean canJump (int []A) { int n=A.length; boolean [] f=new boolean [n]; f[0 ]=True; for (int j=1 ;j<n;++j){ f[j]=false ; for (int i=0 ;i<j;++i){ if (f[i]&&i+A[i]>=j) { f[j]=true ; break ; } } } return f[n-1 ]; }
总结:四个步骤
刷题顺序:https://leetcode.cn/problems/coin-change/solutions/2119065/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-21m5