数据结构与算法学习(九)(续二)

  9.4.3 素性测试

  本节将考察确定一个大数是否是素数的问题。将给出一个可以测试素性的多项式时间算法。如果这个算法宣称一个数不是素数,可以肯定这个数不是素数。如果该算法宣称一个数是素数,那么,这个数将很可能(以高的概率)但不是100%是素数。错误的概率不依赖于被测试的特定的数,而是依赖于由算法做出的随机选择。因此,这个算法偶尔会出错,不过我们将会看到,可以让出错的几率任意小。

  算法的关键是著名的费马(Fermat)定理。

  定理9.10(费马小定理) 如果P是素数,且0<A<P,那么A^(P-1)≡1(mod P)。

  假如A是整数,P是质数,且A,P互质(即两者只有一个公约数1),那么A的(P-1)次方除以P的余数恒等于1。这个算法偶尔会出错,但问题是它总出一些相同的错误。换句话说,存在N的一个固定的集合,对于这个集合该方法行不通。可以尝试将该算法按下面的方式随机化:随机取1<A<N-1。如果A^(N-1)1(mod N),声明N可能是素数,否则声明N肯定不是素数。

  虽然这看起来没有问题,但是却存在一些数,对于A的大部分选择它们甚至可以骗过该算法。一种这样的数集称为Carmichael数(Carmichael number),这些数不是素数但对所有与N互素的0<A<N却满足A^(N-1)1(mod N)。最小这样的数是561。

  定理9.11 如果P是素数且0<X<P,那么X^21 (mod P)仅有的解为X=1,P-1。

  这种策略在下面以伪代码的形式给出。

/**
 * Function that implements the basic primality test.
 * if witness does not return 1. n is definitely composite.
 * Do this by computing a^i (mod n) and Looking for
 * non-trivial square roots of 1 along the way.
 */
 HugeInt witness( const HugeInt & a, const HugeInt & i, const HugeInt & n )
 {
   if( i == 0 )
     return 1;
   
   HugeInt 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 square root of 1
   HugeInt y = ( x * x ) % n;
   if( y == 1 && x != 1 && x != n - 1 )
     return 0;
     
   if( i % 2 != 0 )
     y = ( a * y ) % n;
     
   return y;
 }
 
 /**
  * The number of witnesses queried in randomized primality test.
  */
  const int TRIALS = 5;
  
  /**
   * Randomized primality test.
   * Adjust TRIALS to increase confidence level.
   * n is the number to test.
   * If return value is false, n is definitely not prime.
   * If return value is true, n is probably prime.
   */
   bool isPrime( const HugeInt & n )
   {
     Random r;
     
     for( int counter = 0; counter < TRIALS; couter++ )
       if( witness( r.randomInt( 2, ( int ) n - 2 ), n - 1, n ) != 1 )
         return false;
         
     return true;
   }

  素性测试的随机算法非常重要,因为它们明显比最佳非随机算法快。而且,非随机算法偶尔会产生错误的判断,但这种机会非常小,是可忽略的。

 9.5 回溯算法

  我们将要考察的最后一个算法设计技巧是回溯(backtracking)。许多情况情况下,回溯算法相当于穷举搜索的巧妙实现,但性能一般不理想。不过,情况并非总是如此,即使是如此,在某些情形下它相对于蛮力穷举搜索,工作量也有显著的节省。当然,性能是相对的:对于排序而言,Ο(N^2)的算法是相当差的,但对旅行商(或任何NP完全)问题,Ο(N^5)算法则是里程碑式的结果。

  回溯算法的一个具体例子是在一套新房子内摆放家具的问题。存在许多尝试的可能性,但一般只有一些具体要考虑的。开始什么也不摆放,然后是每件家具被摆放在室内的某个位置。如果所有的家具都已摆好而且户主都很满意,那么算法终止。如果摆到某一步,该步之后的所有家具摆放方法都不理想,那么必须撤销这一步并尝试另外的摆放方法。当然,这也可能导致另外的撤销,等待。如果我们发现撤销了所有可能的第一步摆放位置,那么就不存在满意的家具摆放方法。否则,最终将终止在满意的摆放位置上。注意,虽然这个算法基本上是蛮力的,但它并不直接尝试所有的可能,因为令人讨厌的摆放的子集是已知的。在一步内删除一大组可能性的做法叫做剪裁(pruning)。

 

  9.5.1 公路收费点重建问题

  设给定N个点p1,p2,…,pN,它们位于x轴上。xi是pi点的x坐标。进一步假设x1=0,并且这些点从左到右给出。这N个点确定在每一对点间的N(N-1)/2个(不必是唯一的)形如|xi-xj|(i≠j)的距离d1,d2,…,dN。显然,如果给定点集,那么容易以Ο(N^2)时间构造距离的集合。这个集合不是排好序的,但是如果愿意花Ο((N^2)logN)时间整理,那么这些距离也可以被排序。公路收费点重建问题(turnpike reconstruction problem)是从这些距离重建一个点集,它在物理学和分子生物都有应用。令D是距离的集合,并设|D|=M=N(N-1)/2。作为例子,设

D={1,2,2,2,3,3,3,4,5,5,5,6,7,8,10}

由于|D|=15,因此以置x1=0开始。显然,x6=10,因为10是D中最大元素。将10从D中删除,我们放置的点和剩下的距离如下图所示。

205810_UB9q_2537915.jpg

  剩下的距离中最大的是8,这就是说,或者x2=2,或者x5=8。由对称性可以判断这种选择是不重要的,因为或者两个选择都产生解(它们互为镜像),或者都不产生最终的解,所以可置x5=8而不至于影响问题的解。然后从D中删除距离x6-x5=2和x5-x1=8,得到

210359_qifT_2537915.jpg

  下一步是不明显的。由于7是D中最大的数,因此或者x4=7,或者x2=3。如果x4=7,那么距离x6-7=3和x5-7=1也必须出现在D中,它们也的确存在D中。另一方面,如果置x2=3,那么3-x1=3和x5-3=5就必须在D中,这些距离也在D中。因此我们选哪种都是对的。这样,尝试其中的一种看它是否产生问题的解。如果不相,那么退回来再尝试另外的选择,尝试第一个选择,x4=7,得到

211257_tNtw_2537915.jpg

  此时,我们得到x1=0,x4=7,x5=8和x6=10。现在最大的距离是6,因此或者x3=6,或者x2=4。但是,如果x3=6,那么x4-x3=1,这是不可能的,因为1不再属于D。另一方面,如果x2=4,那么x2-x0=4和x5-x2=4,这也是不可能的,因为4只在D中出现一次。因此,这个推导思路得到不解,我们需要回溯。

  由于x4=7不可能产生解,因此尝试x2=3。如果这也不行,那么停止计算并报告无解。现在,我们有

211746_UIfp_2537915.jpg

  我们必须再一次在x4=6和x3=4之间选择。x3=4是不可能的,因为D中只出现一个4,而该选择意味着要有两个。x4=6是可能的,于是得到

211935_fohU_2537915.jpg

  唯一剩下的选择是x3=5,这是可以的,因为它使得D称为空集,因此我们得到问题的一个解。

212106_EvDQ_2537915.jpg

  图9-46是一棵决策树,代表为得到解而采取的行动。这里,没有对分支进行标记,而是把标记放在子分支的目的节点上。带有一个星号的节点表示所选的点与给定的距离不一致:带有两个星号的节点只有不可能的节点作为儿子,因此表示一条不正确的路径。

212541_w4K4_2537915.jpg

  下面是该算法的伪代码:

bool turnpike( vector<int> & x, DistSet d, int n )
{
  x[ 1 ] = 0;
  d.deleteMax( x[ n ] );
  d.deleteMax( x[ n - 1 ] );
  if( x[ n ] - x[ n - 1 ] ∈ d )
  {
    d.remove( x[ n ] - x[ n - 1 ] );
    return place( x, d, n, 2, n - 2 );
  }
  else
    return false;
}

  下面是回溯算法。与大多数回溯算法一样,最方便的实现方法是递归。我们传递同样的参数以及界Left和Right:xLeft,…,xRight是试图放置的点的x坐标。如果D是空集(或Left>Right),那么解已经找到,可以返回。否则,首先使xRight=dmax。如果所有适当的距离都(以正确的值)出现,那么尝试性地放下这一点,删除相应的距离,并尝试从Left到Right-1填入。如果这些距离不出现,或者尝试从Left到Right-1填入失败,那么尝试置xRight=xN-dmax,使用类似的方法。如果这样不行,则问题无解:否则,已经找到了一个解,而这个信息最终通过return语句和x数组传递桶turnpike。
 

 

 

 

 

 

 

 

 

转载于:https://my.oschina.net/u/2537915/blog/650279

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值