文章目录
前言
本文是对于算法设计的学习笔记,如有错误,请不吝赐教。
一、动态规划
如同前几部分的叙述,贪心算法可以说是算法设计最自然的算法,然而,在实际情况中,贪心算法不一定能够提出一个有效地方案。而作为替代的分治法优势不能将指数的解法缩小到多项式时间,它更多地是把已经是多项式时间的运行时间减少到更快的运行时间。
动态规划:通过把事情分解为一系列的子问题,然后对越来越大的子问题建立正确的解,从而探查所由可行解的空间。
二、带权的区间调度
2.1 问题
带权的区间调度问题:每个区间有某个值,我们的目标是接受一个最大值得集合。
而普通的区间调度问题可以看做是权值为1的特殊情况。
2.2 算法设计
我们定义p(j)是与区间j不相容的最大的区间i。如下图所示:
算法:
Compute-Opt(j)
if j=0 then
return 0
else
return max(vj+compute-opt(p(j),Compute-Opt(j-1))
endif
但是以上算法的运行时间将是指数增长。
2.3 递归的备忘录形式
事实上,我们并不是没有一个多项式时间的算法。上述的递归算法Compute-Opt实际上只求解了n+1个不同的子问题:Compute-Opt(0),1…………等,上述所需要的指数时间只是因为他每次调用会产生大量的冗余。
我们可以通过备忘录的方法来优化,具体:将之前算好的值存储下来。
算法:
M-Compute-Opt(j)
if j=0 then
return 0
else if M[j]!=null then
return M[j]
else
M[j]=max(vj+M-compute-opt(p(j)),M-compute-opt(j-1))
return M[j]
endif
每次调用时在M中填入新项,故而最多n+1次,所以为O(n)
2.4 记录最优解
上述算法只是计算了一个最优值,如果需要记录最优的区间,则可以维护数组S来几下最优的一组空间,此处可以利用数组M来方向追踪。
算法:
Find-Solution(j)
if j=0 then
什么也不输出
else
if(vj+M[pj]>=M[j-1] then
输出 j 与 FindSolution(pj)的结果
else
输出FindSolution(j-1)的结果
endif
endif
此时算法那只调用On次,所以:
三、动态规划原理:备忘录或者子问题迭代
3.1 算法设计
上述有效算法的关键其实是数组M,对于每个j,我们正在使用关于区间1…j上的子问题的最优解。那么我们可以不适用备忘录式的递归,而通过迭代算法计算M中的项。
相关算法如下:
Iterative-Compute-Opt
M[0]=0
For j=1,2,,,,n
M[j]=max(vj+M[p[j],M[j-1])
Endfor
3.2 算法分析
3.3 动态规划的基本要点
1.只存在多项式个子问题
2.可以很容易的从子问题的解计算初始问题的解
3.子问题从最小到最大存在一种自然的顺序,与某一个容易计算的递推式相联系。
四、分段的最小二乘:多重选择
上述的带权区间调度中,是一种基础的二分选择,即区间n属于或者不属于最优解。而下属问题中,我们要考虑到多重选择。
4.1 问题
最佳拟合线。假设有平面上的n个点,且横坐标的顺序排列,给定y=ax+b直线,那么直线L对于点集P的误差是他对于p中点的距离的平方和。这一问题的目标是找到具有最小误差的直线。此时的a和b由一下关系:
如果用多条直线来拟合,那么会有比单条直线更好的误差。
现在问题就是需要用最少的直线来很好地拟合这些点。
问题:给定一组点P,按横坐标x排序,pi表示(xi,yi)。将p划分为几段,我们按照上述公式来求相对于每段S的最小误差直线。
其中划分的罚分为一下各项之和:
1)段数*给定的C
2)对于每段的误差值。
分段最小二乘就是找到一个最小的罚分的划分。
4.2 算法设计
我们领Opt(i)表示对于点p1…pi的最优解,并令eij表示pi。。。pj的任何直线的最小误差
那么挑出最后一段pi。。。pn,我们可以考虑排除这些点,递归的求解剩下的点上的问题
同样对于p1,pj组成的子问题,我们要找到最好方式产生的最后一段pi。。。pj以同样的方式来构造。
由上,我们可以得到递推公式:
算法:
Segmentd-Least-Squares(n)
数组M[0.。。m]
M[0]=0
for 所有的对i<=j
计算对于pi..pj的最小二乘误差eij
endfor
for j=1,2,,,n
使用递推公式计算M[j]
endfor
return M[n]
类似于带权区间调度问题,我们也可以采用数组追踪的方式来计算最优划分
Find-Segments(j)
if j=0 then
else
找到一个使得ei,j+c+M[i-1]最小的i输出这个段立即Find-segments(i-1)的结果
对于计算最小二乘的误差需要O(n^3),而后续的迭代求解最优则需要o(n ^2),因为每次找最小值需要O(n)
五、子集合与背包
通过对于支持这个动态规划的递推式增加一个新的变量来做到这一点
5.1 问题
给定n个项,每个项由给定的非负的权wi,再给定一个W,使得选定的集合小与W但达到最大。
5.2 设计算法
我们考虑Opt(i,w),其中w是界。
算法:
Sub-Sum(n,w)
M[0..n,0..w]
对于每个w=0,1,,,2初始化M[0,w]=0
for i in 1,n
for w in 0,w
用递推关系计算M
endfor
endfor
返回M[n,w]
5.3 分析算法
同样可以利用数组m来反追踪。
5.4 背包问题(0-1背包问题)
类似于上述的问题,可得到相应的递推公式:
同样,他也可以在O(nw)内求解。
六、RNA二级结构:在区间上的动态规划
在背包问题中,我们能够通过添加新的变量来讲动态规划算法形式化。
6.1 问题
取一条单螺旋RNA分子B=b1b2b3…我们要确定具有最大碱基配对个数的二级结构S
6.2 算法设计
此时会出现子问题的不在我们递归的子问题表里,所以需要设立两个变量,以区间形式考虑。
初始化:只要i>j-4 Opt(i,j)=0
for k=5...n-1
for i=1...n-k
j=i+k
计算Opt(i,j)
endfor
endfor
返回OPT(1,n)
运行时间为O(n^2)
七、序列比对
序列比对:在串的比较中产生的基本问题。
7.1 问题
串的相似性:
使得上述所说的代价最小化的处理叫做序列比对。其中错匹和空隙的代价由外部给出。
此处则是要求我们对于给定的X与Y计算这个最小代价以及它产生的最优比对。
7.2 算法设计
由上我们可以将m和n取出,讨论m-1和n-1的子问题:
算法`
Alignment(X,Y)
数组A[0-m,0-n]
对每个i初始化A[i,0]=i&
对每个j初始化A[j,0]=j&
for j=1..n
for i=1...m
用递推公式计算A[i,j]
endfor
endfor
return A[m.n]
7.3 算法分析
图示法:
故而上述问题也可以转换为改图上的最短路径问题。
补充:
八、通过分治策略在线性空间的序列比对
8.1 问题
在上述算法中,如果用长字符串的比较,则O(mn)的空间需求可能会比较高。
方法思路如下:通过分治法思想,将计算问题分成几次递归调用,这样计算所需要的空间从一次调用到下一次调用就可以被重用。
8.2 算法设计
如果只需要得到最优对比的值,那么可以将数组A进行折叠,只存储递推公式中所需的数据信息。
算法如下:
Space-Ffficient-Alignment(x,y)
数组B[0-m,0-1]
初始化每个i令B[i,0]=i&
for j in i,n
B[0,1]=j&
for i=1,m
B[i,1]=min[axy+B[i,0],&+B[i-1,,1],&+B[i,0]
endfor
将B的第一列移到第0列
endfor
如果要得到最优的比对则需要采用动态规划逆向公式的方法。
类似的,递推公式不变:
其中f为逆向的路径。(0,0)-(i,j)
G(i,j)-(m,n)
如上所述
我们采用他的中心列划分Gxy,对于每个i我们计算f(i,n/2),g(i,n/2),然后递归搜索最短路径。由于空间上的重用,使得我们的总空间量为(m+n)
为了让算法保持O(mn)的时间,我们维持一个全局可访问的表p,p保存最短路径上的节点。
算法如下:
Divide-and-Conquer-Alignment(X,Y)
令m是X中的符号个数
令n是Y中的符号个数
if(m<=2)或者 n<=2 then
使用Alignment计算最优比对
调用space-Efficient-Alignment(X,Y[1:n/2])
调用Backward-space0Efficient-Alignment(X,Y[n/2+1:n])(逆向动态规划)
令q是使得f(q,n/2)+g(q,n/2)达到最小的序标
把q,n/2加入到全局标P中
Divide-and-Conquer-Alignment(X[1:q],Y[1:n/2])
Divide-and-Conquer-Alignment(X[q:n],Y[n/2:n])
return P
8.3 算法分析
此算法返回O(n+m)的空间,一次只工作在一个递归子问题上,空间以复用,最多为(m+n)
对于运算时间可知如下(组合初始化为cmn)
此处可以采用替代法来解决,即我们考虑它的结果与m=n时的情况相同。
此时可选k=2c,结果即完成证明。
九、图中的最短路径
9.1 问题
之前我们考虑过图中的最短路径,采用了Dijkstra算法,但是假如图中有费用为负的边时,Dijkstra算法便不再适用。
当存在负的边但没有负圈时,我们可以尝试用动态规划来求解最短路径。(Bellman-Ford算法)
9.2 算法设计
算法:
到t节点的最短路径
Shortest-Path(G,S,T)
n=G中的节点数
数组M[0-n-1,V]
定义M[0,t]=0 且对于所有其他的v M[0,v]=无穷
for i in 1,n-1
for v in V
递推公式计算M[i,v] 每次考虑是否有新的中间节点使得距离更短
endfor
return M[n-1,s]
9.3 算法分析
表M由n^2项,每项计算有On时间,最多有n个我们必须考虑的节点。
9.4 推广:对算法的某些基本改进
此时为稀疏图:边m远小于n^2
对于存储方面,我们可以只对每个节点更新一个值M[V],即我们至今找到的从v到t的最短路径距离。
为了复原路径,我么可以采用指针的方式,即用指针保存最短距离的上一个节点:
通过指针的传递性来找到最短路径。
十、最短路径和距离向量协议
10.1 最短路径
我们可以通过基于推的方式来改进最短距离的算法。即如果节点w改变了自己的值,那么邻近的节点v就需要进行改变,否则不变。(只传递改变得值)
算法:
Push-Based-Shortest-Path(G,s,t)
n=G中的节点数
数组M[V]
初始化M[t]=0,并且对所有其他的v M[v]=无穷
for i in 1,n-1
for w in V
if M[w]在上一次迭代中被更新 then
for (v,w)
M[v]=min(M[v],cvw+M[w])
if M[v]改变 then
first[v]=w
endfor
endfor
if迭代中无改变 then 结束算法
endfor
return M[s]
按照实际情况,每个节点无法做到及时的回报,所以我们可以做如下修改:
Asynchronous-Shortest-Path(G,s,t)
n=G中的节点数
数组M[V]
初始化M[t]=0,并且对所有其他的v M[v]=无穷
t为活跃节点
while 存在一个活跃节点
选择一个活跃节点w
for (v,w)
M[v]=min(M[v],cvw+M[w])
if(first[v]=w)
v变为活跃节点
endfor
endfor
w变为不活跃节点
endwhile
return M[s]
10.2 距离向量
上述算法假定网络中每条边的费用是保持不变的,然而实际中这种情况则很难发生。
在现实中,链路可能会变得拥塞甚至可能坏掉。以上算法会导致更新的不确定性。为了避免以上的问题,我们需要在每个节点中存储整条路径的相关信息。此时便不再是距离向量,而是转变为了路径向量。
十一、图中的负圈(?)
11.1 问题
如何确定图中是否包含负圈?
如何在包含负圈的图中找到负圈?
增广图:在图G中加入一个新的节点t,然后其他节点v分别通过一条费用为0的边连接到节点t,此时构成的图为增广图。
11.2 算法设计
此条定理能够用来判断图G中是否有负圈。
以上算法可以在Bellman-Ford算法中进行改进。
11.3 推广(?)
我们可以采用之前的指针图来完成负圈的检测。
具体思路:
在加入边(v,w)之前,可知指针图P为一颗有向树,沿w-t路径寻找,如果从沿着路径找到了v,那么就是一个负圈。
同样我们可以采用标记睡眠的方式来对上述进行优化。让每个节点v维护一个有着被选择的边指向v的所有节点的表。v的值被改变,而此时其余节点的值为老的值,故可以直接标记为睡眠,从指针图中删去边(x,first[x])。这样可以减少遍历寻找圈的时间。