什么是动态规划?
按照动态规划传统的说法就是,将一个大问题分解成一个小问题,然后分别对各个小问题分别求解,最后整合到一起就是动态规划。按照解题的思路来看,一般求总共有多少种方式的题型属于动态规划题型(计数型动态规划求方式数),求一个完整的过程的属于递归题型(打印出所有的解一般不是动态规划做的事情)
动态规划提醒的特点:
- 计数:求有多少种方式。
- 求最大最小(并不是说求最大最小一定就是用动态规划来做,但是有极大的可能使用动态规划来做,有时候用贪心算法也是用来求最大最小值的)。
- 求存在性
先看下面的这道交换硬币的题
正常情况下,我们可能会对该问题采用先用最大的硬币去试,大硬币试完了再用小硬币去试,或者先用小硬币去试,试完了再用大硬币去试,但是动态规划的题目不要这样去想,下面是动态规划的解题思想和步骤。
动态规划解题步骤:
一: 确定状态
(状态在动态规划中的作用相当于定海神针的作用)**
如同我们小学的时候就学过的数学题,当求一道要用到未知数x的题的时候,先把未知量x设出来,动态规划的题也一样,一开始先把这个状态给确定出来。
两个意识:
- 最后一步是要干嘛?
- 这个问题的子问题是什么?
补充:还有一点就是,在动态规划中实际写代码时,常用数组来表示状态。
什么是最后一步:
在这里,最后一步指的就是每一个问题中最后一次确定的那个未知量的值。
在这里,aK表示的是用到的K枚硬币的第K个硬币的面值。
什么是子问题:
把问题形式完全一样,规模更小的问题叫做子问题。
那么究竟怎么确定状态呢?
可以看到,一旦子问题出来了,我们把相同的内容一copy,然后把变化的内容27和27-ak换成做小学数学题的时候设的x,然后用一个f(X)来表示,那么这个f(X)就是这样的一个未知状态量,和小学里学的设x是一模一样的。
回顾一下,那这个状态从开始没有到最后是怎么一步步出来的呢?
①找到问题最优情况下的最后一步ak,然后把这个最后一步去掉之后,剩下的部分也要最优,那么问题就转化为求解剩下一部分这个子问题的最优解,因此状态就这样出来了。
先看看递归求解的情况:
但是这类问题递归就会存在一个问题:
即很多次的求解都是一个重复的过程,这就导致了时间复杂度的提升和空间复杂度的提升,因为每一次递归都要创建一个函数的栈帧结构去占用内存。
那么动态规划就是来解决这个问题的!
动态规划的做法可以理解为:将每一次的计算结果保存下来,或者说是将每一次的状态f(X)保存下来,即保存状态,当判断到一个状态是已经计算过的结果的时候,就跳过计算这个状态的过程,直接使用上一次的计算结果来进行判断,这就大大减少了一个递归过程的运算次数。
接下来就来说说动态规划的第二部分:
二:转移方程
那么什么是转移方程呢:可以理解为,将当前的一个状态用下一个状态来等效替代的一个方程,在上面我们提到了,要确定一个题目的最后一步和子问题,确定好了这两个要素之后就可以来设未知状态f(X)=求解子问题所需的X,那么将这个未知状态f(X)具体到题目中就是要具体的把这中间的未知量和逻辑运算给带入进这个状态来确定一个数学表达式,这个数学表达式就是这个题目的状态方程,像这个交换硬币的题目的转移方程如下:
我们对比一下上面在确定两个要素(最后一步和状态)的时候写出来的状态f(X):
发现:就是大同小异,把它具体化的一个过程。这个就是写转移方程的过程。
那么是否确定了转移方程之后就可以直接干代码了呢?不是的,很多时候我们还要进入动态规划的第三个部分,往下看。
三:确定初始条件和边界情况
初始条件f[0]的意义相当于支点。
那么什么时候需要用到或者设置初始值呢?
一般来说,当状态转移方程算不出来的时候,但是在解决问题的过程中又需要用到的时候,需要使用初始条件。例如f(0)是=f(-2)+f(-5)+f(-7)=正无穷,但是实际上f(0)应该等于0,这个时候就需要手工设置f(0)为0
边界情况就是数组的下标,让下标不要下溢或上溢。
四:计算顺序
一般的顺序是按照从小到大的顺序,同时满足等式右边的式子都是已经计算好的。
小结:求最值型动态规划
动态规划组成部分
- 确定状态
1:最后一步
2:子问题
- 写转移方程
- 考虑边界情况和初始值
- 考虑计算顺序
下面是具体的题目示例:
代码:
public class Solution {
/**
* @param coins: a list of integer
* @param amount: a total amount of money amount
* @return: the fewest number of coins that you need to make up
*/
// 2,5,7 27
public int coinChange(int[] coins, int amount) {
//0,1,...,n [n+1] 这里需要用到0~27,用n+1;
//0,1,...,n-1 [n]
// write your code here
int[] f = new int[amount+1];
f[0] =0;
int n = coins.length;
int x,j;
//依次从小到大求解f(1),f(2),...,f(27), last coins:A[j]
//f(x) = min{f(x-coins[0])+1,...,f(x-coins[n-1])+1}
//1,2,3,...,27
for (x = 1; x <= amount; ++x ){
//初始化为无穷大
f[x] = Integer.MAX_VALUE;
//0,1,..n-1
for(j = 0; j < n; ++j){
//考虑边缘情况
if(x-coins[j] >= 0 && f[x-coins[j]]!=Integer.MAX_VALUE){
f[x] = Math.min(f[x-coins[j]]+1,f[x]);
}
}
}
if(f[amount]==Integer.MAX_VALUE){
f[amount] =-1;
}
return f[amount];
}
}
代码:
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) {
// write your code here
//这里因为数组的下标为m-1,n-1,因此开数组的时候只需要开m,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];
}
}