动态规划(dp) 简介
作为算法设计者和程序员,我们经常需要编程寻找一些问题的最优解。通常很容易得到优秀而可行的解,但要保证程序总是返回绝对意义下的最优解,往往需要对问题进行深入的分析和思考。
动态规划是求解最优化问题的一个非常强大、通用的工具。它对于字符串这样“从左到右排列好”的元素序列尤其有效。一旦学透,动态规划是很好用的,但是很多人在尝试理解它时遇到了重重困难。
在看了足够的例子之前,你会觉得动态规划跟变魔术一样。建议大家可以到网上搜一下编辑距离(动态规划的经典例子),二项式系数的求法(如何利用部分解来逐步递推出我们想要的最终答案的)。
很多问题都可以归结为寻找满足特定约束条件的最优解。处理这类问题有很多方法。例如,回溯法所解决的问题常常要求我们找出最大、最小或者得分最高的方案。回溯法搜索所有的可能的解,并从中挑出我们所想要的,因此保证得到正确的答案。但是这个方法只适用于很小的问题实例。
很多重要的图论问题都存在正确并且高效的算法,包括最短路、最小生成树、匹配等。在自己设计算法之前最好看看需要完成的任务是否可以转化成这些经典问题。如果可以的话,直接套用经典算法即可。
贪心(greedy)算法在每次决策时,做出局部最优选择。例如,求解从x到y的最短路径的一种容易想到的方法就是从x出发反复沿着最短边走,直到走到y。很直观,可惜是错的!事实上,没有正确性证明的贪心法很有可能是错误的。
那我们该怎么办呢?动态规划为我们提供了一个设计新算法的方式,不仅系统地搜索了所有的可能性(从而保证正确性),而且通过保存中间结果来避免重复计算(从而保证效率)。
动态规划建立在递归的基础上,它需要把整个问题的解用若干小规模子问题的解来表示。我们已经介绍过的回溯法和图的深度优先遍历都属于这样的递归过程。
要想让这种递归算法高校执行,程序中必须存储足够的信息以避免重复计算。为什么图的深度优先遍历算法高效?因为算法每访问一个节点后给它加上标记,防止重复访问。为什么纯粹的回溯法运算量巨大,因为它搜索了所有可能的路径\解,而不是只考虑那些以前没有考虑过的。
动态规划是一个高效实现递归算法的技巧,它的核心在于保存中间结果。首先需要发现朴素的递归算方法一而再再而三地计算一些相同的子问题,接下来吧答案放在表格中而不是重复计算。这样算法就高效了。
这算是理论知识吧,也算dp的入门,以后搞点题目练习,详细介绍。下面是dp各种:
dp的变种很多,下面就是一些例子,搞ACM的可以找这样的题目去练习。
1.不完全状态记录
<1>青蛙过河问题
<2>利用区间dp
2.背包类问题
<1> 0-1背包,经典问题
<2>无限背包,经典问题
<3>判定性背包问题
<4>带附属关系的背包问题
<5> + -1背包问题
<6>双背包求最优值
<7>构造三角形问题
<8>带上下界限制的背包问题(012背包)
3.线性的动态规划问题
<1>积木游戏问题
<2>决斗(判定性问题)
<3>圆的最大多边形问题
<4>统计单词个数问题
<5>棋盘分割
<6>日程安排问题
<7>最小逼近问题(求出两数之比最接近某数/两数之和等于某数等等)
<8>方块消除游戏(某区间可以连续消去求最大效益)
<9>资源分配问题
<10>数字三角形问题
<11>漂亮的打印
<12>邮局问题与构造答案
<13>最高积木问题
<14>两段连续和最大
<15>2次幂和问题
<16>N个数的最大M段子段和
<17>交叉最大数问题
4.判定性问题的dp(如判定整除、判定可达性等)
<1>模K问题的dp
<2>特殊的模K问题,求最大(最小)模K的数
<3>变换数问题
5.单调性优化的动态规划
<1>1-SUM问题
<2>2-SUM问题
<3>序列划分问题(单调队列优化)
6.剖分问题(多边形剖分/石子合并/圆的剖分/乘积最大)
<1>凸多边形的三角剖分问题
<2>乘积最大问题
<3>多边形游戏(多边形边上是操作符,顶点有权值)
<4>石子合并(N^3/N^2/NLogN各种优化)
7.贪心的动态规划
<1>最优装载问题
<2>部分背包问题
<3>乘船问题
<4>贪心策略
<5>双机调度问题Johnson算法
8.状态dp
<1>牛仔射击问题(博弈类)
<2>哈密顿路径的状态dp
<3>两支点天平平衡问题
<4>一个有向图的最接近二部图
9.树型dp
<1>完美服务器问题(每个节点有3种状态)
<2>小胖守皇宫问题
<3>网络收费问题
<4>树中漫游问题
<5>树上的博弈
<6>树的最大独立集问题
<7>树的最大平衡值问题