算法总结之动态规划
动态规划基础
之前刷leecode的时候最怕看到的就是动态规划类的问题,自己敲破脑袋也想不出来,看了答案之后总觉得如神来之笔。无法摸清思考的方向。上了卜老师的算法课之后感触还是挺深的,对于一些常见的动态规划问题也不像之前那样完全丈二摸不着头脑了,现对一些常见的算法和理论基础总结一下。
动态规划是一种适用于大部分优化问题的算法思想,与之前学过的分治法一样,都是希望将问题分解为更小的子问题进行优化求解,但是动态规划相较于分治法更加抽象、难以理解。
可以规约的问题一般用分治法就能解决,但是在包含最优子结构的情况时,需要使用动态规划进行求解。所谓最优子结构,就是子问题与原问题有一样的结构并且原问题的最优解包含子问题的最优解,这样才能利用动态规划的思想递归进行求解。
动态规划最核心的思想就是要定义好状态和状态转移函数。所谓状态就是将问题变成函数,比如0-1背包问题中 d ( i ) d(i) d(i)就是背包内物品重量为 i i i时,背包能产生的最大价值;而状态转移函数就是寻求当前状态与之前状态的关系,比如0-1背包问题中状态函数反映的是第 j j j个物品装与不装对状态的影响, d ( i ) = m a x { d ( i ) , d ( i − w ( j ) ) + v ( j ) } d(i)=max\{d(i),d(i-w(j))+v(j)\} d(i)=max{ d(i),d(i−w(j))+v(j)}
下面列举一些经典的动态规划问题,完全理解对学习动态规划很有帮助。
0-1背包问题
0-1背包问题是最典型的动态规划问题,leecode里没有完全一样的题,但是可以用一个简单的问题描述来说清楚。输入物品的重量数组 w w w,价值数组 v v v,那么需要设一个数组 d p dp dp, d p [ i ] [ j ] dp[i][j] dp[i][j]表示以 j j j为容量放入前 i i i个物品产生的最大价值,边界条件为 d [ i ] [ 0 ] = 0 , d [ 0 ] [ j ] = 0 d[i][0]=0,d[0][j]=0 d[i][0]=0,d[0][j]=0。对于每个物品,有装与不装两种情况,若不装第 i i i个物品,那么对于可装重量为 j j j的背包产生的最大价值为 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j];若装入第 i i i个物品,则必须保证第 i i i个物品被装入,那么可装重量要变为 j − w [ i ] j-w[i] j−w[i],用于装前 i − 1 i-1 i−1件物品,这样就与之前的 d p dp dp数组产生了关联,这时最大价值为 d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] dp[i-1][j-w[i]]+v[i] dp[i−1][j−w[i]]+v[i]。所以综合两种情况,有 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) dp[i][j]=max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])。
根据该递归表达式,可以看出 d p [ i ] [ j ] dp[i][j] dp[i][j]只与 d p [ i − 1 ] [ x ] ( x = 1... j ) dp[i-1][x](x=1...j) dp[i−1][x](x=1...j)有关,这些都是前面计算过的值,因此可以将动态规划的数组纬度降低为一维数组,化简后的递归表达式为 d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + w [ i ] ) dp[j]=max(dp[j], dp[j-w[i]]+w[i]) dp[j]=max(dp[j],dp[j−w[i]]+w[i]),这时dp[j]的含义与上面 d p [ i ] [ j ] dp[i][j] dp[i][j]的含义相同,表示可装重量为 j j j的背包放入前 i i i个物品产生的最大价值。
核心的代码如下所示:
for(int i=0; i<n; ++i){
for(int j=W; j>=w[i]; j--){ //背包的最大重量为W
dp[j]=max(dp[j], dp(j-w[i])+v[i]);
}
}
对于多维的约束问题(除了书包负重之外,还有别的约束如体积约束),那么只需对数组增加一个纬度,多一层循环即可。算法的时间复杂度为 O ( n N ) O(nN) O(nN),空间复杂度为 O ( N ) O(N) O(N)( N N N表示书包能装的最大重量, n n n表示物品的数目)。
完全背包问题
0-1背包问题延伸之一是完全背包问题。
完全背包问题与0-1背包的不同点在于物品的个数可以是无限的,同样的对于输入的物品有重量数组 w w w,价值数组 v v v。对于二维的 d p dp dp数组,其递归表达式为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] + d p [ i ] [ j − w [ i ] ] + v [ i ] ) dp[i][j]=max(dp[i-1][j]+dp\red{[i]}[j-w[i]]+v[i]) dp[i][j]=max(dp[i−1][j]+dp[i][j−w[i]]+v[i]),与0-1背包唯一的不同就是存放完当前第 i i i种物品之后,还可以继续存放第 i i i种物品。
整理为一维的形式,存在的差别就是内层循环顺序不同,0-1背包是内层逆序循环,保证已经取到的物品不会再次被取到;而完全背包是顺序循环,顺序循环可以覆盖以前的状况,所以会存在多次选取的问题,完全符合题意。
核心的算法代码如下所示:
for(int i=0; i<n; ++i){
for(int j=w[i]; j<=W; j++){ //背包的最大重量为W,与0-1背包的不同之处在于内层循环是顺序的
dp[j]=max(dp[j], dp(j-w[i])+v[i]);
}
}
与0-1背包问题一样,算法的时间复杂度为 O ( n N ) O(nN) O(nN),空间复杂度为 O ( n ) O(n) O(n)。
多重背包问题
0-1背包问题延伸之二是多重背包问题
多重背包问题中每个物品的个数是有限个的,即给定的除了重量数组 w w w、价值数组 v v