动态规划概览
1.动态规划概念
动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。这里的递推可以理解为一种递归的反向。
递推不是关键,关键是如何拆分问题,这就涉及到了对于问题状态的定义和状态转移方程的定义。
动态规划的本质,是对问题状态的定义和状态转移方程的定义。
状态的定义:是对问题的重新描述,使得问题在不同的时刻,有不同状态。
状态转移方程:是状态和状态之间转换的关系式,一般为递归表达式的方式。此时应当明确递归的初始状态,即边界条件。
2.动态规划的适用问题
当原问题可以由一系列的子问题计算得到的时候
- 子问题的数量是常数级的
- 原问题可以由子问题简单的计算得到
- 子问题之间存在递归的解决方式,即存在递归表达的初始条件。
3.动态规划和分治算法的区别
与分治法的相似之处:基于递归,通过获得子问题的解来得到问题的解。
与分治法的不同之处:由小问题延伸到大问题,是自底向上的过程。
动态规划可以存储子问题的解,从而避免了冗余的计算,有一种空间换时间的感觉。同时,动态规划有时会计算一下与子问题无关的东西。
分治法 | 动态规划 | |
---|---|---|
核心思想 | 递归 | 递推 |
递归使用 | 自顶向下 | 自底向上 |
存储子问题的解 | 否 | 是 |
解决子问题次数 | 多次 | 一次 |
4.关于递推、贪心、搜索与动态规划的区别
一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到
这个性质叫做最优子结构;
而不管之前这个状态是如何得到的
这个性质叫做无后效性。
动态规划的经典问题
1.加权间隔调度
输入:一组n个间隔,没个时间段有开始时间、结束时间以及时间段的权重。
输出:不互相重叠的一组具有最大权重的间隔集。
Eg:
解决方法:
通过“带记忆”的储存加递归的方式解决。
int Memoized Opt(j)
if j = 0 then return(0)
else
if M[j] = "undefined" then
M[j] ← max v(j) + Opt(p(j)), Opt(j − 1)
return M[j]
将间隔按照结束时间排序,第j个间隔时的权重和应该为:j的权重与上一个不与第j个间隔重合的间隔的Opt(p(j))的和;与Opt(j-1)相等。当中最大的一个。
也可以自底向上的用递推的方式计算
IterativeComputeOpt
M[0] ← 0
for j ← 1 to n do
M [j] ← max v(j) + M [p(j)], M [j − 1]
2.分段最小二乘问题
给出一组n个点,需要确定一条能够最好的拟合(fit)这些点的直线。有error = 点到直线的距离的平方,而这条直线具有最小的error。
使用多条直线能够做到更好的拟合,但是分成越多的段对于我们的数据分析没有意义。因为有指数中不同的划分(相当于求子集),使用暴力搜索是不现实的。
解决方法:
我们引入一个可变的变量C,作为算法的惩罚值,拟合划分的线越多,C值越大。因此我们的目标就是找到最小的:C + 各条线的error值。
定义e(i,j) 是拟合pi到pj这些点时的error;OPT(i)为pi到pj的最优解(OPT(0) = 0)。此时可知:
我们无法确定i的值,但是可以选择能够给出最小值的i的值。
由此我们可以得到伪代码:
计算每个点的分段最小二乘法的error值,然后储存在数组中,之后遍历数组来获得最好的拟合线。
SegmentedLeastSquares(n)
Array M[0...n]
M[0] ← 0
for all pairs (i, j)
compute least squares error[i, j] for segment pi . . . pj
for j ← 1 to n
M [j ] ← min1≤i≤j (error[i, j ] + C + M [i − 1] )
Find Segments(j)
find i that minimizes error[i,j]+C+M[i−1]
if i > 1 then Find Segments(i − 1)
output the best fit line for segment pi...pj
3.汽车装载问题
要求尽可能多的向汽车上装载货物。
输入:n个箱子,每一个箱子i有重量wi;汽车的总承重量为W。
输出:在满足总重量限制下,所能尽可能多的装载重量的箱子的子集。
解决方法:
很明显,在这种情况下,贪心算法并不能给出最优解。比如汽车总承重量为10,有4个箱子,分别重量为8、3、3、3。最优解为9,但贪心只会选择一个重量为8的箱子。
我们可以通过减少箱子数量和减少总重量限制来将问题化解为更小的问题。我么用OPT(i, w)表示我们在重量限制为w条件下,在前i个箱子中所能装载的最大重量。
对于第i个箱子,我们可以选择装载它或者不装载它,则有:
应注意,wi不能大于w,否则只能选择不装载箱子i。此算法有时间复杂度:O(n·W)。
4.背包问题(Knapsack Problem)
输入:n件物品,物品i有重量wi和价值vi;背包总重量W。
输出:一组在满足总重量限制下的具有最大价值的物品集合。
解决方法:
此时贪心也无法得到最优解,例如W = 100,有两个物品,重量与价值分别为(20, 80)、(90, 90),此时贪心会选择最小重量,但是背包已经装不下了。
背包问题就像是引入了价值的卡车装载问题。令OPT(i, w)表示在前i个物品中,在w重量限制下,所能取得的最大价值。我们可以选择要不要这件物品,则有:
应注意,wi不能大于w,否则只能选择不装载物品i。此算法有时间复杂度:O(n·W)。
4.序列对比(Sequence Alignment)
为了确定两个序列之间的相似性,将两个string按照一定的规律排列。序列中可以通过插入间隔来获得更好的相似性,但我们会为gap和mismatch分配不同的惩罚值,gap是有cost的。给出两个string,计算它们之间的相似程度
解决方法:
设两个string为x和y,我们令A(i, j)表示对于x1到xi,y1到yj的最佳匹配值,即最小的gap与mismatch惩罚值。则有:
伪代码:
ALIGN( X[1:m],Y[1:n] )
for j ← 0 to n
A[0,j] ← jδ
for i ← 1 to m
A[i, 0] ← iδ
for j ← 1 to n
A[i,j]←min{αxi,yj +A[i−1,j−1], δ+A[i−1,j],
δ+A[i,j−1] }
5.带负边的最短路径(Bellman-Ford Shorest Path Algorithm)
输入:带权有向图G,c(v, w)表示从顶点v到w的边的cost,可以为负。
输出:一个负环(所有边的cost之和为负)或者一条从s到t的最短路径。
实际生活中,比如经济学中的互相转账就有可能会引入负边,然而Dijkstra算法因此受限,无法解决。这时就应当使用Bellman-Ford Shortest Path Alg,使用前我们需要知道一条定理:如果有向图没有负环,那么对于有n个点的图G,从点s到t的最短路径至多含有n-1条边。
解决方法:
令OPT(i, v)表示在小于等于i条边的情况下,从点v到点t的最小path cost。
我们要求的是OPT(n - 1, s),有递归初始条件OPT(0, t) = 0,OPT(0, v) = +∞(v !=
t)。则有:
伪代码:
Shortest-Path(G, s, t)
n ← number of nodes in G
for all v except t, initialize M[v] ← +∞
M[t] ← 0
for i←1 to n−1
for all e=(v,w)∈E(G)
M [v] ← min(M [v], c(v, w) + M [w])
return M[s]
可以通过在计算OPT(i, v),只取存在边(v, w)的点,来将时间复杂度降低到O(m·n)。