一些算法
算法什么是算法算法所具有的特征算法复杂性什么是好的算法 贪心法贪心算法思想贪心法求解问题所具有的性质贪心选择性质最优子结构性质算法关键活动安排问题证明活动安排问题的正确性基本步骤动态规划(dynamic programming)基本思想动态规划求解子问题特征设计步骤分支限界法基本思想与回溯法差别常用的分支限界队列式分枝限界法优先队列式分支限界BFS算法一般模式递归与分治策略分治法的设计思想分治法步骤递归的定义能够用递归解决的问题分治法所能解决问题的特征算法设计模式回溯法问题的解空间基本思想回溯法算法框架子集树和排列树
算法
什么是算法
算法是求解问题的一系列计算步骤,用来将输入数据转换成输出结果
正确的算法:如果一个算法对其每一个输入实例,都能输出正确的结果并停止,则称它是正确的。
算法所具有的特征
输入性:必须有0个或多个输入(待处理信息);
输出性:应有一个或多个输出(已处理信息);
确定性:组成算法的每条指令是清晰的、无歧义的。
可行性:每一基本操作都可实现,且在常数时间内完成。
有限性:算法中每条指令的执行次数有限,执行每条指令 的时间也有限(一个算法无论在什么情况下都应 在执行有穷步后结束)。
算法复杂性
算法复杂性是算法运行所需要的计算机资源的量,随着问题规模的增长
(大O符号): 如果 C>0, 以及自然数N0,使得当N>=N0时有:f(N)<=Cg(N), 则称函数f(N)当N充分大时上有界,且g(N)是它的一个上界, 记为f(N)=O(g(N))。即f(N)的阶不高于g(N)的阶。
什么是好的算法
正确性:符合语法、编译通过;能够正确处理各种输入 (简单的、大规模的、一般性的、退化的等任意合法输入)
健壮性:能辨别不合法的输入并做适当的处理,不至于异 常退出(崩溃)
可读性:结构化 + 准确的命名 + 注释 效率性:速度尽可能快;存储空间尽可能少。
贪心法
贪心算法思想
贪心算法的基本思想是找出整体当中每个小的局部的最优解,并且将所有的这些局部最优解合起来形成整体上的一个最优解。因此能够使用贪心算法的问题必须满足下面的两个性质: 1.整体的最优解可以通过局部的最优解来求出; 2.一个整体能够被分为多个局部,并且这些局部都能够求出最优解。使用贪心算法当中的两个典型问题是活动安排问题和背包问题。 贪心法在解决问题的策略上目光短浅,每一步对目前构造的部分解做一个扩展,直到获得问题的完整解为止。所做的每一步必须满足: 1. 可行:即它必须满足问题的约束 2. 局部最优:它是当前步骤中可行选择的最优解 3. 不可取消:选择一旦做出,在算法的后续步骤中不可改变 在对问题求解时,总是作出在当前看来是最好的选择。也就是说,不从整体上加以考虑,它所作出的仅仅是在某种意义上的局部最优解(是否是全局最优,需要证明)
贪心法求解问题所具有的性质
贪心选择性质
贪心选择性质是指所求问题的整体最优解可以通过局部最优的选择,即贪心选择来达到
证明贪心选择性质,必须证明贪心选择最终导致问题的整体最优解
证明方法:数学归纳法
最优子结构性质
一个问题的最优解包含其子问题的最优解
算法关键
关键:贪心选择策略
贪心算法往往效率较高,时间复杂性为多项式阶
难点在于证明相应的贪心算法确实可求出最优解
Greedy(C) //C是问题的输入集合即候选集合
{
S={ }; //初始解集合为空集
while (not solution(S)) //集合S没有构成问题的一个解
{
x=select(C); //在候选集合C中做贪心选择
if feasible(S, x) //判断集合S中加入x后的解是否可行
{
S=S+{x};
C=C-{x};
}
}
return S;
}
活动安排问题
问题描述:假设有一个需要使用某一资源的n个活动所组成的集合E,E={1,…,n}。该资源一次只能被一个活动所占用,每一个活动i有一个开始时间si和结束时间fi(si<fi)。若活动i和活动j有si≥fj或sj≥fi,则称这两个活动兼容。 问题求解:采用贪心策略如下:每一步总是选择这样一个活动来占用资源,它能够使得余下的未调度的时间最大化,使得兼容的活动尽可能多。
算法每次总是选择具有最早完成时间的相容活动加入集合A中。 直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。 也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,从而安排尽可能多的相容活动。
证明活动安排问题的正确性
-
贪心选择性质
即证明活动安排问题总存在一个最优解从贪心选择开始。
设E={1,2,…,n}为所给的活动集合。由于E中的活动按结束时间的非递减排序,故活动1具有最早完成时间。
首先证明活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1.
设A⊆E是所给活动安排问题的一个最优解,且A中的活动也按结束时间非递减排序,A中的第一个活动是k。
若k=1,则A就是以贪心选择开始的最优解。
若k>1,设B=A-{k}∪{1}。因为f1<=fk,且因为A中的活动是相容的。故B中的活动也是相容的。又由于B中的活动个数与A中的活动个数相同,故A是最优的,B也是最优的。即B是以选择活动1开始的最优活动安排。由此可见,总存在以贪心选择开始的最优活动安排方案。
-
最优子结构性质
在作出了贪心选择,即选择了活动1后,原问题简化为对E中所有与活动1相容的活动进行活动安排的子问题。即若A是原问题的最优解,则A’=A-{1}是活动安排问题的E’={i∈E:Si ≥ f1}的最优解。反证法:若E’中存在另一个解B’,比A’有更多的活动,则将1加入B’中产生另一个解B,比A有更多的活动。与A的最优性矛盾。因此,每一步所作出的贪心选择都将问题简化为一个更小的与原问题具有相同形式的子问题。对贪心选择次数用归纳法可知,贪心算法greedySelector产生问题的最优解。
基本步骤
1、从问题的某个初始解出发。
2、采用循环语句,当可以向求解目标前进一步时,就根据局部最优策略,得到一个部分解,缩小问题的范围或规模。
3、将所有部分解综合起来,得到问题的最终解。
动态规划(dynamic programming)
基本思想
一种求解多阶段决策最优化问题的工具。
如果能够保存已解决的子问题的答案,而在需要时再 找出已求得的答案,就可以避免大量重复计算,从而 得到多项式时间算法。
与分治法的关系 –
与分治法类似,其基本思想也是将待求解问题分解 成若干个子问题,但各问题间往往有重叠。 – 分治法对子问题的重叠部分要被重复计算多次。 – 动态规划法将每个子问题只求解一次并保存在表 中,下次查表获得解,免去重复计算。
如图,求从A到E的最短距离
Mindistance(E)=min{w(D1,E)+Mindistance(D1),w(D2,E)+Mindistance(2)}
写出状态转移方程
动态规划求解子问题特征
-
最优性原理。问题的最优解包含的子问题的解也是最优的
-
无后效性。某阶段状态确定,此后状态的演变不受此前状态影响。
-
有重叠子问题。子问题间不是独立的。这是dp问题的特征()
设计步骤
-
分析最优解性质,刻画其结构特征。
-
分段将原问题分解为若干个重叠子问题。
-
建立递归关系,递归的定义最优解(状态转移方程)
-
计算最优值。
-
构造最优解。
动态规划基本思想与分治法类似,也是将待求解的问题 分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能 达到最优的局部解,依次解决各子问题,最后一个子问题就是 初始问题的解。 由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不 同状态保存在数组中(填表)。 与分治法最大的差别是:适合于用动态规划法求解的问题, 经分解后得到的子问题往往有重叠子问题。
动态规划方法又和贪心法有些相似,在动态规划中,可将一个问题的解决方案视为一系列决策的结果。 不同的是,在贪心法中,每采用一次贪心准则便做出一个不可回溯的决策,还要考察每个最优决策序列中是否包含一个最优子序列。
分支限界法
基本思想
分枝限界法采用广度优先搜索,并在生成当前结点的孩子结点时,通过一些限界条件即限界函数,帮助避免生成不包含解结点子树的状态空间的检索方法。
与回溯法差别
求解目标:
回溯法:找出解空间树中满足约束条件的所有解;
分枝限界法:找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。 搜索方式的不同:
回溯法:深度优先的方式搜索解空间树;
分枝限界法:以广度优先或以最小耗费优先的方式搜索解空间树。
常用的分支限界
队列式分枝限界法
将活结点表组织成一个队列,按照先进先出(FIFO)原则 选取下一个结点为扩展结点: ①将根结点加入活结点队列。 ②从活结点队中取出队头结点,作为当前扩展结点。 ③对当前扩展结点,先从左到右地产生它的所有孩子结点, 用约束条件检查,把所有满足约束条件的孩子结点加入 活结点队列。 ④重复步骤②和③,直到找到一个解或活结点队列为空为止。
优先队列式分支限界
略
BFS算法一般模式
bool bfs()
{
队列初始化为空;
起点加入到队列;
while(队列非空)
{
从队列中出队一个节点 x(获取并删除); // step1
for (x的所有可扩展节点y) // step2
{
if(检查可达性通过(包括判重)OK) //step3
{
将新扩展节点排入队列 //step4
记录y的父节点为x;
记录到达y的步数
}
// 找到目标,结束搜索
if (新扩展节点y == 目标节点)return true; //step5
}
}
}
递归与分治策略
分治法的设计思想
对于一个规模为n的问题,若该问题可以容易地解决 (比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
分治法步骤
-
将问题的实例划分为同一个问题的几个较小的实例,最好拥有同样的规模;
-
对这些较小的实例求解(一般使用递归的方法,但在问题规模 足够小的时候,有时也会使用一些其他方法)
-
如果必要的话,合并这些较小问题的解,以得到原始问题的解.
递归的定义
在定义一个过程或函数时出现调用本过程或本函数的成分称之为递归。
若调用自身,称之为直接递归。
若过程或函数p调用过程或函数q,而q又调用p,称之为间接 递归。
任何间接递归都可以等价地转换为直接递归。
如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。
能够用递归解决的问题
( 1)需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同;
(2)递归调用的次数必须是有限的;
(3)必须有结束递归的条件(递归基)来终止递归。
分治法所能解决问题的特征
(1)该问题的规模缩小到一定的程度就可以容易地解决。
(2)该问题可以分解为若干个规模较小的相同问题。
(3)利用该问题分解出的子问题的解可以合并为该问题的解。
(4)该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
求解过程
① 分解:将原问题分解为若干个规模较小,相互独立, 与原问题形式相同的子问题。
② 求解子问题:若子问题规模较小而容易被解决则直 接求解,否则递归地求解各个子问题。
③ 合并:将各个子问题的解合并为原问题的解
算法设计模式
ivide-and-conquer(P)
{
if |P|≤n0 return adhoc(P); // 返回递归基
将P分解为较小的子问题 P1,P2,…,Pk;
for(i=1;i<=k;i++) //循环处理k次
yi=divide-and-conquer(Pi); //递归解决Pi
return merge(y1,y2,…,yk); }//合并子问题
回溯法
问题的解空间
一个复杂问题的解决方案是由若干个小的决策步骤组成的决策序列,解决一个问题的所有可能的决策序列构成 该问题的解空间。一般用树形式来组织,也称为解空间树或状态空间。
应用回溯法求解问题时,首先应该明确问题的解空间。 解空间中满足约束条件的决策序列称为可行解(是解空间的子集)。 一般来说,解任何问题都有一个目标,在约束条件下使目标达到最优的可行解称为该问题的最优解。
基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索 的策略,从根结点(开始结点)出发搜索解空间树。 (1)首先根结点成为活结点,同时也成为当前的扩展结点。
(2)在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为新的活结点,并成为当前扩展结点。
(3)如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。 回溯法以这种方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点为止。
回溯法算法框架
//递归回溯框架
void backtrack (int t)
{
if (t>n) output(x); //递归结束条件:搜索到叶子结点,输出一个可行解
else //枚举t的所有可能的扩展节点
for (int i=f(n,t);i<=g(n,t);i++) // f(n,t):可扩展的下界;g(n,t):上界;
{
x[t]=h(i); //获取新的扩展点并标记该扩展节点
if (constraint(t)&&bound(t)) //节点t未越界且满足扩展/减枝条件可扩展
{
//进一步深搜前的工作:步长++、记录路径
backtrack(t+1); //递归执行进行下一层(深一层)搜索
//注意:进行回溯回来的清理工作!
}
}
}
子集树和排列树
//子集树
void backtrack (int t)
{
if (t>n) output(x);
else
for (int i=0;i<=1;i++) {
x[t]=i;
if (legal(t)) backtrack(t+1); }
}
//排列树
void backtrack (int t)
{
if (t>n) output(x);
else
for (int i=t;i<=n;i++) {
swap(x[t], x[i]);
if (legal(t)) backtrack(t+1);
swap(x[t], x[i]); }
}