分享会之动态规划
一、简介
有这样一种具有特殊性的过程,其可以将被分成若干相互联系的阶段,在它的每个阶段都要做出决策,从而使整个过程达到最好的活动效果。但各个阶段决策的选取不是任意的,依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线。
在求解这种活动过程的最优化结果的时候,通常有4种思路,①暴力穷举:将所有可能的决策序列枚举出来,然后比较每个序列的结果,得到最好的;②简单递归:自顶向下,以边界条件作为递归的结束;③备忘录算法:将每一阶段的决策保存下来,避免每得到一个决策序列都对每个阶段进行一次决策;④动态规划:利用自底向上的方式从最小的子问题开始向上逐渐推到得到最优解。
二、参考博客
https://www.jianshu.com/p/69669c7bd69e
http://www.importnew.com/27444.html
三、动态规划
1、分类:
线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;
区域动规:石子合并, 加分二叉树,统计单词个数,炮兵布阵等;
树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;
背包动规:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶等。
2、基本思想:
大事化小,小事化了。把一个复杂的问题分阶段进行简化,逐步化简成简单的问题。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引入时间因素,把它视为多阶段的决策过程,也可以使用动态规划方法方便地求解。
3、适用条件:
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。
①最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
②无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
③子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
4、基本要素:
最优子结构:一个大问题可以分解为怎样的子问题;
边界:最小的子问题,通常为迭代停止条件;
状态转移方程:由当前状态该如何转化为下一状态,某以问题与其子问题之间的关系。
5、求解步骤:
①根据问题,找到【最优子结构】。
把原问题从大化小的第一步,找到比当前问题要小一号的最好的结果,而一般情况下当前问题可以由最优子结构进行表示。
②确定问题的【边界】。
根据上述的最优子结构,一步一步从大化小,最终可以得到最小的,可以一眼看出答案的最优子结构,也就是边界。
③通过上述两步,通过分析最优子结构与最终问题之间的关系,我们可以得到【状态转移方程】。
6、复杂度分析:
用高空间复杂度换取时间复杂度降低。
算法实现是比较好考虑的。但有时也会遇到一些问题,而使算法难以实现。动态规划思想设计的算法从整体上来看基本都是按照得出的递推关系式进行递推,这种递推相对于计算机来说,只要设计得当,效率往往是比较高的,这样在时间上溢出的可能性不大,而相反地,动态规划需要很大的空间以存储中间产生的结果,这样可以使包含同一个子问题的所有问题共用一个子问题解,从而体现动态规划的优越性,但这是以牺牲空间为代价的,为了有效地访问已有结果,数据也不易压缩存储,因而空间矛盾是比较突出的。另一方面,动态规划的高时效性往往要通过大的测试数据体现出来(以与搜索作比较),因而,对于大规模的问题如何在基本不影响运行速度的条件下,解决空间溢出的问题,是动态规划解决问题时一个普遍会遇到的问题。
一个思考方向是尽可能少占用空间。如从结点的数据结构上考虑,仅仅存储必不可少的内容,以及数据存储范围上精打细算(按位存储、压缩存储等)。当然这要因问题而异,进行分析。另外,在实现动态规划时,一个我们经常采用的方法是用一个与结点数一样多的数组来存储每一步的决策,这对于倒推求得一种实现最优解的方法是十分方便的,而且处理速度也有一些提高。但是在内存空间紧张的情况下,我们就应该抓住问题的主要矛盾。省去这个存储决策的数组,而改成在从最优解逐级倒推时,再计算一次,选择某个可能达到这个值的上一阶段的状态,直到推出结果为止。这样做,在程序编写上比上一种做法稍微多花一点时间,运行的时效也可能会有一些(但往往很小)的下降,但却换来了很多的空间。因而这种思想在处理某些问题时,是很有意义的。
但有时,即使采用这样的方法也会发现空间溢出的问题。这时就要分析,这些保留下来的数据是否有必要同时存在于内存之中。因为有很多问题,动态规划递推在处理后面的内容时,前面比较远处的内容实际上是用不着的。对于这类问题,在已经确信不会再被使用的数据上覆盖数据,从而使空间得以重复利用,如果能有效地使用这一手段,对于相当大规模的问题,空间也不至于溢出(为了求出最优方案,保留每一步的决策仍是必要的,这同样需要空间)。
一般地说,这种方法可以通过两种思路来实现:一种是递推结果仅使用Data1和Data2这样两个数组,每次将Data1作为上一阶段,推得Data2数组,然后,将Data2通过复制覆盖到Data1之上,如此反复,即可推得最终结果。这种做法有一个局限性,就是对于递推与前面若干阶段相关的问题,这种做法就比较麻烦;而且,每递推一级,就需要复制很多的内容,与前面多个阶段相关的问题影响更大。另外一种实现方法是,对于一个可能与前N个阶段相关的问题,建立数组Data[0..N],其中各项为前面N个阶段的保存数据。这样不采用这种内存节约方式时对于阶段k的访问只要对应成对数组Data中下标为k mod (N+1)的单元的访问就可以了。这种处理方法对于程序修改的代码很少,速度几乎不受影响,而且需要保留不同的阶段数也都能很容易实现。
当采用以上方法仍无法解决内存问题时,也可以采用对内存的动态申请来使绝大多数情况能有效出解。而且,使用动态内存还有一点好处,就是在重复使用内存而进行交换时,可以只对指针进行交换,而不复制数据,这在实践中也是十分有效的。
7、例题:01背包问题
给定n种物品和一个背包。物品i的重量是w[i],其价值位v[i] ,背包的承重为W。问应该如何选择装入背包的物品,使得转入背包的物品的总价值为最大?
①暴力枚举:遍历所有结果(排列组合),找出价值最大的一个,时间复杂度为指数级别,O(2^N)。
②简单递归:
1、既然是从大到小,不断调用状态转移方程,那么就可以用递归。
2、递归的时间复杂度是由阶梯数和最优子结构的个数决定的。不同的问题,用递归的话可能效果会大不相同。
3、在阶梯问题,最少找零问题中,递归的时间复杂度和空间复杂度都比动归方法的差, 但是在国王与金矿的问题中,递归的时间复杂度和空间复杂度都比动归方法好。这是需要注意的。
问题分析:每个物品都只有两种状态,放入背包,不放入背包,所以每次只要取出两种情况下背包较重的即可,即总价值V ( i, Wi ) = max ( V ( i - 1, Wi ), V ( i - 1, Wi – w[i] ) + v[i] ),其中Wi为剩余可用容量。
总结得出包括边界在内的状态转移方程:
V ( 1, W1 ) = 0 w[1] > W
V ( 1, W1 ) = v[1] w[1] <= W1
V ( i, Wi ) = V ( i – 1, Wi ) w[i] > Wi
V ( i, Wi ) = max ( V ( i - 1, Wi ), V ( i - 1, Wi – w[i] ) + v[i] ) w[i] <= Wi
w[8] = [4, 3, 2, 5, 1, 2, 3, 1]
v[8]= [3, 4, 1, 4, 2, 4, 5, 6]
W = 10
备忘录算法:
1、在阶梯数N比较多的时候,递归算法的缺点就显露出来了:时间复杂度很高。如果画出递归图(像二叉树一样),会发现有很多很多重复的节点。然而传统的递归算法并不能识别节点是不是重复的,只要不到终止条件,它就会一直递归下去。
2、为了避免上述情况,使递归算法能够不重复递归,就把已经得到的节点都存起来,下次再遇到的时候,直接用存起来的结果就行了。这就是备忘录算法。
3、备忘录算法的时间复杂度和空间复杂度都得到了简化。
动态规划:
1、上述的备忘录算法,尽管已经不错了,但是依然还是从最大的问题,遍历得到所有的最小子问题,空间复杂度是O(N)。
2、为了再次缩小空间复杂度,我们可以自底向上的构造递归问题,通过分析最优子结构与最终问题之间的关系,我们可以得到【状态转移方程】。
然后从最小的问题不断往上迭代,即使一直到最大的原问题,也是只依赖于前面的几个最优子结构。这样,空间复杂度就大大简化。也就得到了正经的动归算法。
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
4 |
|
|
| 3 | 3 | 3 | 3 | 3 | 3 | 3 |
3 |
|
| 4 | 4 | 4 | 4 | 7 | 7 | 7 | 7 |
2 |
| 1 | 4 | 4 | 5 | 5 | 7 | 7 | 8 | 8 |
5 |
| 1 | 4 | 4 | 5 | 5 | 7 | 8 | 8 | 9 |
1 | 2 | 2 | 4 | 6 | 6 | 7 | 7 | 9 | 10 | 10 |
2 | 2 | 4 | 6 | 6 | 8 | 10 | 10 | 10 | 11 | 13 |
3 | 2 | 4 | 6 | 7 | 9 | 11 | 11 | 13 | 15 | 15 |
1 | 6 | 8 | 10 | 12 | 13 | 15 | 17 | 17 | 19 | 21 |
此表格第一行代表背包剩余容重量,第一列代表第i个物体的重量,其余代表价值量。由状态转移方程可以看出,当考虑第i个物体时背包的状态只与考虑过第i - 1个物体的状态有关,即表中除第一行外的每一行都是由上一行得到的,所以可以从第一行开始不断迭代得到最后一行,即考虑第i个物体时背包所有可能的状态,第8行第10列即为问题中所求的最佳状态。
Q&A:大家对于备忘录算法的自顶向下和动态规划的自底向上的思想还存在疑惑。