算法设计技巧
参考 : 《数据结构与算法分析 Java语言描述》. Mark Allen Weiss. 机械工业出版社 第十章 : 算法设计技巧
贪心算法(贪婪算法)
-
贪婪算法分阶段工作,在每一阶段,选择局部最优解,在算法终止时,希望获得全局最优解。
-
问题 : 给定 个作业 ,其运行时间分别是 ,处理器只有一个。
- 如何调度这些作业使得作业的平均完成时间最短?
多处理器情况
问题 : 给定 个作业 ,其运行时间分别是 ,处理器多个。
比如有3个处理器同时工作,下面是每个作业执行的时间,完成这些作业需要最少的时间是多少?
-
一种情况:完成需要40个单位时间
-
另一种情况:完成需要38个单位时间
-
最佳的完成时间需要34个单位时间
-
Minimizing the Final Completion Time 最后完成时间最小化
-
即各处理器之间如何更平均地承担任务
-
用更少的时间完成更多的任务。
-
属于NP Hard 问题
活动选择问题
- 活动选择问题:在一系列给出的活动中选出一个最大兼容活动子集(数目最多)。
- 例如 : 以下示例中,子集是一个解,然而最优解是 ,或 ,最优解的大小为4。
注: 为(start)开始时间 , 为(finish)结束时间
活动选择问题(加权)
- 活动选择问题(加权):给定一系列活动,其对应的开始时间为,结束时间为,每个活动还有一个对应的权值(价值),问题是需要找出若干个相容的活动,使得总的权值最大。
递归解
- 定义为活动上的最优解,则有:
独立集问题
-
独立集问题 : 设 是一个简单无向图,是的子集,若中的节点在图中都无边相连,则称是一个独立集。在普通图中要找出最大独立集是非常困难的。在下面两图中黄色点构成极大独立集,上图中的极大独立集为9,下图中的极大独立集为8。
-
-
树上的独立集问题
- 在一些特殊图要快速找出最大独立集是可以做到的。在下图中标出你做的选择,并给出做出这些选择的理由。换句话说,你能否证明你选出的就是一个最大独立集。
- 贪心算法求解
- 定理 : 如果是一棵树,是的一片树叶,那么存在包含 的最大独立集。
- 贪心算法:考虑一条边 ,其中是树叶,独立集将包含节点,而不包含节点,做出这个决定后,则可以删除节点和节点,得到一棵小一些的树,继续在这棵剩下的树上重复前面的策略。
问题定义:设树,每一个节点关联一个正的权,请在树中找出一个独立集,使得中包含的节点的权之和最大。
-
-
问题的关键:对于节点和与它相邻的树叶构成的子树,实际上只存在两种选择,即要么选择,要么选择所有的树叶。
-
考虑以节点为根构成的子树,最终的独立集要么包含,则不能包含它的任何一个孩子节点;要么不包含,则有包含或删去这些孩子节点的自由。
-
树上最大权独立集的递归式
-
-
自底向上求解
装箱问题
- 装箱问题(Bin Packing) : 给定 项物品,大小为,所有的大小都满足。问题是要把这些物品装到最小数目的箱子中去,已知每个箱子的容量是个单位。
- 如图所示把大小为 的一列物品最优装箱的方法。
联机和脱机
-
有两个版本的装箱问题
- 第一种是联机装箱问题( on-line bin packing problem)。在这种问题中,每一件物品必须放入一个箱子之后才能处理下一件物品。
- 第二种是脱机装箱问题(off-line bin packing problem)。在一个脱机装箱算法中,我们做任何事都可以等到所有的输入数据全被读取之后才进行。
-
联机算法的局限性 —— 不存在最优算法
- 为了证明联机算法不总能够给出最优解,我们将给它一组特别难的数据来处理。考虑由权为 的 个小项和其后权为 的 个大项构成的序列,其中。显然,如果我们在每个箱子中放一个小项再放一个大项,那么这些项物品可以放入到 个箱子中去。
- 假设存在一个最优联机算法 可以进行这项装箱工作。考虑算法 对序列的操作,该序列只由权为 的 个小项组成。是可以装入个箱子中的。然而,由于 对序列处理结果必然和对 的前半部分处理结果相同,而 前半部分的输入跟 的输入完全相同,因此, 将把每一项物品放到一个单独的箱子内,即箱子没装满。这说明 将使用 最优解的两倍多的箱子(原本只需要个箱子,现在要个箱子装)。这样我们证明了,对于联机装箱问题不存在最优算法。
-
联机算法的局限性—— 不存在太好的近似算法
-
定理 :存在使得任意联机装箱算法至少使用4/3最优箱子数的输入。
-
证明(反证法):假设情况相反。即存在算法使用少于最优箱子数完成装箱。
-
(1)设序列有个小项后接个大项组成 (为简单起见并设是偶数, 且规定一个箱子最多只能放一个小项和一个大项) 。考虑任一运行在上面输入序列上的联机算法, 让我们考虑该算法在处理第 项后都做了什么。设 已经用了 个箱子。在算法的这一时刻(即处理了第项之后),箱子的最优个数是 , 因为我们可以在每个箱子里放入两件物品。于是我们知道,根据优于 最优箱子数的性能保证的假设, 这时已用的箱子占全部装满的箱子的个数比例 。
-
(2)现在考虑在所有的物品都被装箱后算法 的性能。在第 个箱子之后开辟的所有箱子的每箱恰好包含一项物品,因为所有小项物品都被放在了前 个箱子中,而两个大项物品又装不进一个箱子中去。由于前b 个箱子每箱最多能有两项物品,而其余的箱子每箱都有一项物品,因此我们看到,将 项物品 (大项加小项) 装箱将至少需要 (即)个箱子。但 项物品可以用** 个箱子最优装箱**,因此我们的性能保障保证得到。
-
在(1)中意味着,在(2)中意味着,这是矛盾的。因此,没有联机算法能够保证使用小于的最优装箱数完成装箱。因此要使(1)和(2)中的条件成立, 即可,即
-
-
有三种简单算法保证所用的箱子数不多于二倍的最优装箱数。也有很多更为复杂的算法能够得到更好的结果。
近似在线装箱算法
- 下项适应算法 (next fit) : 大概最简单的算法就属下项适合(next fit) 算法了。当处理任何一项物品时,我们检查看它是否还能装进刚刚装进物品的同一个箱子中去。如果能够装进去,那么就把它放入该箱中;否则,就开辟一个新的箱子。这个算法实现起来出奇地简单,而且还以线性时间运行。
- 大白话 就是在上一次装的箱子里看是否能装下,装得下就装,装不下就拿新的空箱子装。
- 定理 : 令 是将一列物品 装箱所需的最优装箱数,则下项适合算法所用箱数决不超过 个箱子。存在一些顺序使得下项适合算法用箱数达 个
- 首次适应算法 (first fit) : 首次适合算法(first fit) 的策略是依序扫描这些箱子并把新的一项物品放入足能盛下它的第一个箱子中。因此,只有当前面那些放置物品的箱子已经容不下当前物品的时候,我们才开辟一个新箱子。
- 大白话 就是在下项适应算法的基础上,通过回去找之前没有装满的箱子,看看是否还能装下目前的东西。而下项适应算法只是在上一次装东西的箱子中找是否能装得下,如果装不下就不会再往前面的箱子找,而是去新开辟一个空箱来装。
- 定理 : 令 是将一列物品 装箱所需要的最优箱子数,则首次适合算法使用的箱子数决不多于。存在使得首次适合算告使用 个箱子的序列。
- 最佳适应算法 (best fit) : 该算法不是把一项新物品放入所发现的第一个能够容纳它的箱子,而是放到所有箱子中能够容纳它的最满的箱子中。
- 大白话 就是在首次适应算法的基础上,找出剩余空间最小并且能容纳下当前物品的箱子, 比如下图中的箱子 和 的箱子,可以看出当放入东西为0.3前, 剩余0.6, 但剩余0.3, 即刚好剩余最小且能容纳它,这就是最佳适应算法,这样的好处就是就能腾出大的空间放大东西。 而使用首次适应算法, 就会放在那个箱子里, 它是根据顺序进行放的,这样的缺点是不能保证每次箱子剩下的容量是最大的, 但最佳适应算法一定能保证最后每次箱子剩下的都是最大的空间,这样下次放东西的时候减少因为容量不够而去开辟新的空间的次数,从而减少箱子个数。
脱机算法
- **如果我们能够观察全部物品以后再算出答案,那么我们应该会做得更好。**事实确实如此,由于我们通过彻底的搜索最终能够找到最优装箱方法,因此我们对联机情形就已经有了一个理论上的改进。
- 所有联机算法的主要问题在于将大项物品装箱困难,特别是当它们在输入的后期出现的时候。围绕这个问题的自然方法是将各项物品排序,把最大的物品放在最先。此时我们可以应用首次适合算法或最佳适合算法,分别得到首次适合递减算法(first fit decreasing)和最佳适合递减算法(best fit decreasing) 。
- 首次适合递减算法
- 定理 : 令 是将物品集 装箱所需的最优箱子数,则首次适合递减算法所用箱子数决不超过。
- 定理 : 令 是将物品集L 装箱所需的最优箱数,则首次适合递减算法所用箱数决不超过 。此外,存在使得首次适合递减算法用到 个箱子的序列。
分治算法
用于设计算法的另一种常用技巧为分治算法( divide and conquer)。分治算法由两部分组成 :
- 分(divide) : 递归解决较小的问题(当然,基本情况除外)。
- 治( conquer) : 然后从子问题的解构建原问题的解。
分治算法的运行时间
定理1 :
-
对于以下递归方程式
, 其中 , 且 。该方程式的解为:
定理2 :
-
对于以下递归方程式
, 其中 , 且 。该方程式的解为:
- 例如 : 归并排序 a = b = 2, p = 0 and k = 1.
- 注意 : 就是以2为底数的对数 , 计算机术语中默认省略2
定理3 :
- 若 , 则方程
归并排序
选择问题
- 从一组未排序的数中选出第k大的数。
最近点对问题
- 平面上有N个点,找出距离最近的两个点。
最近点对的分治
- 对所有点按坐标排序。画一条想象的垂线将点集分为两半和。那么最近点对要么出现在中,要么出现在中,要么跨越和。我们把这三个距离分别叫做。 可以递归计算,但如何计算。另外,要想得到算法,则的计算代价最多为。
- 考察条带中的点
- 对于均匀分布的点,平均只有个点分布在这个条带中。因此使用蛮力法计算这个点两两之间的距离也只需O(N)时间。
/* points are all in the strip */
for ( i=0; i<NumPointsInStrip; i++ )
for ( j=i+1; j<NumPointsInStrip; j++ )
if ( Dist( Pi , Pj ) < s )
s = Dist( Pi , Pj );
- 考察坐标
-
在 的区域中,最多存在4个满足条件的点。即每两点之间的距离小于等于 。因此在 区域中最多存在8个满足条件的点。因此对于每个,最多考虑另外7个点即可。
-
-
-
/* points are all in the strip */
/* and sorted by y coordinates */
for ( i = 0; i < NumPointsInStrip; i++ )
for ( j = i + 1; j < NumPointsInStrip; j++ )
if ( Dist_y( Pi , Pj ) > s )
break;
else if ( Dist( Pi , Pj ) < s )
s = Dist( Pi , Pj );
整数相乘问题
- 两个 位数 相乘,采用传统的竖式乘法,需要执行 乘法。
- 整数相乘的改进
- 设是两个位整数(为方便起见,设),用表示 的前半部分, 表示的后半部分;用表示的前半部分,表示的后半部分;则:
-
- 以上方程由四次乘法和三次加法构成,因此可得递归式:
- 根据[定理一](# 定理1 : ), 即
- 因此,关键是能否将以上递归式中的 “4” 变小。
- 对此有: 而和是已经计算过的。
- 因此,计算可以仅由三次一半规模的乘法和次加减法来完成。所以递归式变为 。根据[定理一](# 定理1 :)可知 ,算法的时间复杂度为。
- 设是两个位整数(为方便起见,设),用表示 的前半部分, 表示的后半部分;用表示的前半部分,表示的后半部分;则:
- 整数相乘的"三分"
- 分成三部分
- 相同方式下,它可以降低时间复杂度
矩阵乘法
- 两个阶矩阵和相乘,最终得到一个阶矩阵,矩阵中有个元素,每个元素需要花费次乘法和次加法获得。因此传统的矩阵相乘的时间复杂度为 。
- 矩阵乘法改进(Strassen)
-
与整数乘法类似的分治,采用分块相乘方法。需要8次阶矩阵的乘法和4次阶矩阵的加法。阶矩阵有个元素,因此加法所需运算为。
-
因此有:
-
-
-
若,根据 [定理一](# 定理1: )可知,
-
因此关键问题也是如何将上式中的 “8” 缩小。
-
8次乘法的压缩,
-
-
-
-
-
动态规划
-
动态规划—Dynamic programming
-
动态规划中涉及到递归的处理。
-
Fibonacci Numbers:
int Fib( int N )
{
if ( N <= 1 )
return 1;
else
return Fib( N - 1 ) + Fib( N - 2 );
}
-
动态规划适用于子问题不是独立的情况,也就是各子问题包含公共的“子子问题”。动态规划对每个子子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算答案。
-
通常应用于最优化问题,其步骤如下 :
-
- 描述最优解的结构;
- 递归定义最优解的值;
- 按自底向上的方式计算最优解的值;
- 由计算出的结果构造一个最优解。
-
动态规划基础
- 适合采用动态规划方法的最优化问题中的两个要素:
- (1)最优子结构
- (2)重叠子问题
- 应用动态规划的一种方法,称为备忘录(memoization),以充分利用重叠子问题性质。
最优子结构
- DP以自底向上的方式来利用最优子结构。即首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。寻找问题的一个最优解需要在子问题中做出选择,即选择将用哪一个来求解问题。问题解的代价通常是子问题的代价加上选择本身带来的开销。
重叠子问题
-
适用于DP求解的问题的第二个要素是“子问题”的空间要很小,即用来求解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。典型地,不同的子问题数是输入规模的一个多项式。
-
DP算法总是充分利用重叠子问题,即通过每个子问题只解一次,把解保存在一个在需要时就可以查看的表中,而且每次查表的时间为常数。
低效的递归
- Fibonacci Numbers:
int Fib( int N )
{
if ( N <= 1 )
return 1;
else
return Fib( N - 1 ) + Fib( N - 2 );
}
更高效的递归
- 通过避免重复计算来优化递归
int Fibonacci ( int N )
{ int i, Last, NextToLast, Answer;
if ( N <= 1 ) return 1;
Last = NextToLast = 1; /* F(0) = F(1) = 1 */
for ( i = 2; i <= N; i++ ) {
Answer = Last + NextToLast; /* F(i) = F(i-1) + F(i-2) */
NextToLast = Last; Last = Answer; /* update F(i-1) and F(i-2) */
} /* end-for */
return Answer;
}
动态规划与贪心算法
- 加权活动选择问题
- 树上的最大权独立集
装配线调度
-
汽车工厂有两条装配线,每条有 个装配站。装配线 的第 个装配站表示为 ,在该站的装配时间为 。汽车底盘进入装配线 ,花费时间为 。在通过一条线的第 个装配站后, 这个底盘可来到任一条装配线的第 个装配站。如果它留在相同的装配线,则没有移动开销。但是,若它移动到另一条线上,则花费时间为 。在离开一条装配线的第 个装配站后,汽车底盘花费时间 离开工厂。
-
问题:确定应该在装配线 选择哪些站,在装配线 选择哪些站, 才能使汽车通过工厂的总时间最短。
-
-
装配线调度 —— 一个最优解
-
-
(1)最优解的结构
- 通过装配站的最快路线包含了子问题,即通过前一个装配站的一个最优解。这个性质称为“最优子结构”,这是能否应用DP方法的标志之一。
- 通过建立子问题的最优解,就可以建立原问题的一个最优解。
-
(2)递归表达式
-
动态规划的第二个步骤就是利用子问题的最优解来递归定义一个最优解的值。
-
通过装配线1上的节点的最快时间:
-
通过装配线2上的节点的最快时间:
-
-
(3)计算最快时间
-
根据前面的两个递归式很容易得知计算该递归式的复杂度为。
-
但是从递归式中也可以发现,很多计算是重复的,若从左到右进行计算则复杂度竟然是,这正是动态规划区别于分治法的关键之一。因为可以利用后面的计算可以利用子问题的计算结果。
-
Fastest Way —— 自底向上的计算
-
-
-
-
(4)构造经过的最快线路
- 按站号的递减顺序,输出所使用的各个装配站。
-
矩阵乘法的顺序安排
-
设 计算 的不同方法。则
-
设 . 则
-
假设我们要乘 个矩阵 其中是一个 矩阵。设为计算的最优方法的成本。然后我们有递归方程:
-
不同 的个数为。
/* r contains number of columns for each of the N matrices */
/* r[ 0 ] is the number of rows in matrix 1 */
/* Minimum number of multiplications is left in M[ 1 ][ N ] */
void OptMatrix( const long r[ ], int N, TwoDimArray M )
{ int i, k, Left, Right;
long ThisM;
for( Left = 1; Left <= N; Left++ ) M[ Left ][ Left ] = 0;
for( k = 1; k < N; k++ ) /* k = Right - Left */
for( Left = 1; Left <= N - k; Left++ ) { /* For each position */
Right = Left + k; M[ Left ][ Right ] = Infinity;
for( L = Left; L < Right; L++ ) {
ThisM = M[ Left ][ L ] + M[ L + 1 ][ Right ]
+ r[ Left - 1 ] * r[ L ] * r[ Right ];
if ( ThisM < M[ Left ][ Right ] ) /* Update min */
M[ Left ][ Right ] = ThisM;
} /* end for-L */
} /* end for-Left */
}
最优二叉查找树
- 给定 个单词 ,以及它们出现的概率 。 如何将它们安放在一棵二叉查找树中使得总的期望访问时间最小化。
- 举个栗子 : 有如下一些单词和它们的访问概率
-
-
-
-
最优二叉查找树的动态规划
- 最小代价的递归式
最长公共子序列—LCS
-
Longest Common Subsequence
-
定义 : 给定两个序列和, 当另一序列既是的子序列又是的子序列时, 称是序列和的公共子序列。
-
举个栗子 :
- X=< A, B, C, B, D, A, B >
- Y=< B, D, C, A, B, A >
- 则:LCS(X, Y)=<B, C, B, A>
- 或:
- X=< A, B, C, B, D, A, B >
- Y=< B, D, C, A, B, A >
- 则: LCS(X, Y)=<B, D, A, B>
-
定理 : LCS的最优子结构
- 设 和 为两个序列,并设 为和的任意一个LCS。
-
- 如果 , 那么, 而且是和的一个LCS。
- 如果 , 那么, 则可推出是和的一个LCS。
- 如果 , 那么, 则可推出是和的一个LCS。
-
LCS 的重叠子问题
- LCS的重叠子问题:为找出X和Y的一个LCS,可能需要找出和的一个LCS,以及和的一个LCS。但这两个问题都包含找和的一个LCS的子子问题。原问题总共包含个不同的子问题,所以可以用DP自底向上来计算解。
-
LCS 的递归树
-
计算 LCS 长度的递归式
- 用 表示 和 的 LCS 的长度,则有 :
-
-
计算LCS的长度
-
-
构造一个LCS
-
-
一个更通用的序列比对问题
- 20世纪70年代,分子生物学家Needleman和Wunsch提出了一个相似性的定义,用以描述两个序列的相似性。
- 问题定义:给定两个串,,假定是与之间的一个比对,定义罚分规则如下:
- (1)存在一个参数 δ >0,用于定义空隙罚分。对于每个 或 没在 中被匹配的位置 — 一个空隙,付出代价为 δ 。
- (2)对于字母表中的每对字符p和q,存在一个把p和q对准的错配代价。于是,对每个,我们把和对准支付的错配代价。
- (3)的代价是它的空隙和错配代价之和,我们要找一个最小代价的比对。
-
比对的定理
- 定理1:在一个最优比对M中,至少下述情况之一为真 :
-
- ;或者
- 的第个位置没被匹配;或者
- 的第个位置没被匹配
-
- 定理1:在一个最优比对M中,至少下述情况之一为真 :
| m | e | a | n | - |
|---|---|---|---|---|
| n | - | a | m | e |
| -1 | -2 | -1 | -2 |
-
对比的递推式
- Recurrence:对于i≥1和j≥1,最小代价比对满足下面的递推式:
-
-
序列比对算法
-
比对示例
-
比对示例,最优比对结果如下。
-
δ=2
-
元对元,辅对辅=1
-
元音对辅音=3
-
-
m e a n - n - a m e -1 -2 -1 -2
-
图 中所有点对间的最短路径
- Floyd算法
- Floyd-Warshall 算法考虑最短路径上的中间节(intermediate),简单路径 上的中间节点是除 和 ,以外的任意节点。
递归解
-
设 G 的节点为 ,对参数考虑节点集 。对任意一对节点 , 考虑从 到 且中间节点都属于集合 的所有路径,设 是其中的最短路径。记为有如下结论。
- 若 k 不是路径 p的中间节点,则p的所有中间节点属于集合 。即
- 若 是 的中间节点,则如下图,可将分解为两条子路径,即 。是从 到 中间节点属于集合 的最短路径, 是从 k 到 j 中间节点属于 的最短路径。
-
Floyd-Warshall 算法的递归解。 表示从 到 且中间节点都属于集合 的所有路径中的最短路径的权值。
-
Floyd算法示例
/* A[ ] contains the adjacency matrix with A[ i ][ i ] = 0 */
/* D[ ] contains the values of the shortest path */
/* N is the number of vertices */
/* A negative cycle exists iff D[ i ][ i ] < 0 */
void AllPairs( TwoDimArray A, TwoDimArray D, int N )
{ int i, j, k;
for ( i = 0; i < N; i++ ) /* Initialize D */
for( j = 0; j < N; j++ )
D[ i ][ j ] = A[ i ][ j ];
for( k = 0; k < N; k++ ) /* add one vertex k into the path */
for( i = 0; i < N; i++ )
for( j = 0; j < N; j++ )
if( D[ i ][ k ] + D[ k ][ j ] < D[ i ][ j ] )
/* Update shortest path */
D[ i ][ j ] = D[ i ][ k ] + D[ k ][ j ];
}
在某种意义上,如果你看出一个动态规划问题,那么你就看出了所有的问题。
随机算法
- 产生随机数的方法:线性同余法
- Lehmer(1951),
-
- Lehmer 建议(素数),(能给出整周期)。
跳跃表
-
目标:以期望时间支持查找和插入的数据结构。
-
-
带有指向前个表元素的指针的链表,总的指针数加倍,但查找时间复杂度降为。但该结构的插入过于呆板。
-
-
-
-
采用随机方法实现的跳跃表。表中有1/2的一阶节点,1/4的二阶节点,…。按概率分布随机确定节点的阶数。任意 阶节点上的第 阶()指针指向的下一个节点至少具有 阶。
-
跳跃表需要预估表中元素个数,以便确定节点阶的最大值。
-
跳跃表可以获得类似平衡查找树或伸展树的性能,但实现起来更简单。
素数测试问题
- 素数测试:确定一个数是否素数。
- 该问题不被认为是NP完全的,它的复杂性尚为未知。
模运算的几条性质
关于素数的几个定理
- 费马小定理 :如果是素数,且,那么 。
- 因此,对于整数,若,那么可以肯定不是素数。
- 平方探测定理 :如果是素数且,那么仅有的两个解为,。
费马定理证明
平方探测定理的证明
- 平方探测定理 :如果P是素数且,那么仅有的两个解为,。
- 证明: 意味着。即。由于P是素数, ,因此P必然是或者整除,或者整除,所以定理成立。
使用随机方法测试素数的思路
-
(1)利用费马小定理,选用不同的,对待测数P执行,若结果≠1,则可以肯定是不是素数;若结果=1,则换别的测试。若用了很多不同的,结果都=1,则P是素数的可能性非常大。
-
(2)利用平方探测定理,若仅有的两个解不是,则一定不是素数;反之,采用不同的继续测。
-
若函数Witness返回任何不是1的数,那么就已经证明了N不是素数(但证明是非构造性的)。也已证明,对于任何,至多有A的个值会使该算法得出错误的结论。因此,若A是随机选取的,而算法的结论是“N是素数”,那么该算法有75%的概率是正确的。
-
HugeInt Witness( HugeInt A, HugeInt i, HugeInt N ) { HugeInt X, Y; if( i == 0 ) return 1; X = Witness( A, i / 2, N ); if( X == 0 ) /* If N is recursively composite, stop */ return 0; /* N is not prime if we find a non-trivial root of 1 */ Y = ( X * X ) % N; if( Y == 1 && X != 1 && X != N - 1 ) return 0; if( i % 2 != 0 ) Y = ( A * Y ) % N; return Y; } /* IsPrime: Test if N >= 3 is prime using one value of A */ /* Repeat this procedure as many times as needed for */ /* desired error rate */ int IsPrime( HugeInt N ) { return Witness( RandInt( 2, N - 2 ), N - 1, N ) == 1; }
回溯算法
- 许多情况下,回溯相当于穷举搜索的巧妙实现,但性能一般不理想。
收费公路重建问题
-
收费公路重建问题 :给定X轴上的 个点,其坐标满足 。假设。 每一个点对间有一个距离,共计 个点对间的距离。给定这 个距离。请根据这些距离数据重建这个点序列。
-
-
- 首先确定 和 。
- 接下来可以尝试 。
-
再尝试x4=7。
-
剩下的距离中,最大为6。因此,要么,要么。若,则,而1已不在集合中;若, , ,这都是不可能的。所以需要回溯。
- 回溯,撤消,尝试。
- 接下来需要在和之间做出选择。而是错的,因为,。
- 所以选择 。
- 最后剩下唯一的选择是 ,而这是可行的。
皇后问题
- 问题描述:在8×8的国际象棋盘上放8个皇后,使得任意两个皇后都不能相互吃掉。规则:皇后能吃掉同一行、同一列、同一对角线的任意棋子。
- 回溯法求解
public boolean positionIJOk(int[][] queens, int i, int j) {
//检测在queens数组中位置(i,j)是否合法
int m,n;
boolean ok=true;
for (m=i-1;m>=0;m--) //垂直上方
if (queens[m][j]==1) ok=false;
for (m=i-1,n=j-1;m>=0 && n>=0;m--,n--) //左上方
if (queens[m][n]==1) ok=false;
for (m=i-1,n=j+1;m>=0 && n<8;m--,n++) //右上方
if (queens[m][n]==1) ok=false;
return ok;
}
int[][] queens=new int[8][8];
for (i=0;i<8;i++)
for (j=0;j<8;j++)
queens[i][j]=0;
int[] position=new int[8];
for (i=0;i<8;i++) position[i]=-1;
i=0;
while (i<=7 && i>=0) {
for (j=position[i]+1;j<=7;j++) {
//测试位置(i,j)是否合适
if (positionIJOk(queens,i,j)) {
queens[i][j]=1;//放置皇后
position[i]=j;//记下皇后在当前行的位置
i=i+1;
j=20;//强制跳出循环
}
}
if (j==8) {//在本行没找到合适位置
position[i]=-1;//回溯前将position[i]复原
i=i-1;//退回上一行,回溯
if (i>=0) queens[i][position[i]]=0;//i<0为结束条件
}
}
- 递归算法
private int[][] queens=new int[8][8];
public QueensRecursion() {
//构造函数,对queens初始化
int i,j;
for (i=0;i<8;i++)
for (j=0;j<8;j++)
queens[i][j]=0;
}
public boolean queensRecursion(int i) {//求解八皇后问题,输出一个可行解
int j;
if (i==8) {
printQueens();//输出一个解
return true
} else {
for (j=0;j<8;j++) {
if (positionIJOk(i,j)) {
queens[i][j]=1;
if (queensRecursion(i+1)) {
return true;//当前位置合法且下一阶问题为true,则返回true
} else {
queens[i][j]=0;//下一阶问题为false,则复原
}
}
}
return false;
}
}
- 递归函数的调用方法
public static void main(String[] args) {
QueensRecursion eq1;
boolean bl;
//测试queensRecursion
System.out.println("Test queensRecursion...");
eq1=new QueensRecursion();
bl=eq1.queensRecursion(0);
}
博弈
-
三连棋
-
-
三连棋的分析
- 终端位置:通过考察盘面能确定这局棋输赢的位置称为终端位置。
- 极小极大策略:如果一个位置不是终端位置,那么该位置的值通过递归地假设双方最优棋步而确定。该策略叫极小极大策略,因为下棋的一方(人)试图使这个位置的值极小,而另一方(计算机)却要使它的值极大。
- 位置P的后继位置:即从P走一步后能到达的任何位置Ps。若在某个位置P,计算机要走棋,则需要递归地计算所有后继位置的值,然后选定具有最大值的一个后继位置。
- 为了得到Ps的值,要递归地计算出Ps的所有后继位置,然后选择其中最小的值。
-
通过置换表降低搜索层次
ENDING
说明该文章转载自CSDN博主CS@zeny