zoukankan      html  css  js  c++  java
  • 素数环谈代码优化

    昨天在博问里面看到的一道算法题,原题如下:

    给出一个N(0<N<20),在1~N的所有排列中,满足相邻两个数之和是素数的排列输出

    比如当N = 4时,满足条件的素数环有如下几种

    1 2 3 4
    1 4 3 2
    2 1 4 3
    2 3 4 1
    3 2 1 4
    3 4 1 2
    4 1 2 3
    4 3 2 1

    常规的做法是,找出这N个数的所有排列,然后依次检查每个排列,筛选出符合条件的排列即可。求排列可以用回溯法的排列树模型,筛选就按照题目要求即可,判断素数的算法也有很多,选择一个即可。注意不要忘记最后一个元素和第一个元素的检测。优化前的代码如下:

    代码
     1 // Is n prime ?
     2 bool IsPrime(int n)
     3 {
     4     for (int i = 2; i * i <= n; i++)
     5         if(n % i == 0)
     6             return false ;
     7     return true ;
     8 }
     9 
    10 // Check a permutation
    11 bool Check(int a[], int n)
    12 {
    13     if(!IsPrime(a[0+ a[n - 1]))
    14         return false ;
    15 
    16     for(int i = 0; i < n - 1; i++// avoid duplicate
    17         if(!IsPrime(a[i] + a[i + 1]))
    18             return false ;
    19 
    20     return true ;
    21 }
    22 
    23 void Perm(int a[], int n, int t)
    24 {
    25     if(t == n)
    26     {
    27         if(Check(a, n))
    28             Output(a, n) ;
    29     }
    30     else
    31     {
    32         for(int k = t; k < n; k++)
    33         {
    34             swap(a[k], a[t]) ;
    35             Perm(a, n, t + 1) ;
    36             swap(a[k], a[t]) ;
    37         }
    38     }
    39 }

    题目不难,做完以后,我发现有很多可以优化的地方,可以大幅提高速度。

    1. 首先,找出所有排列并逐个检查,这是很浪费时间的,更高效的方法是,一边排列一边检查,这样可以提早发现不满足条件的候选解,提早剪枝,避免不必要的搜索,例如当N=10时,排列到1234的时候,满足条件,下一次选择5,序列变为12345,由于4 + 5 = 9,非素数,所以后面不用再排列了,也就是从当前位置开始,以5为根的子树可以不用再搜索了,直接跳到6,序列变为12346,由于4 + 6 = 10,非素数,同样舍弃6为根的子树。下一次搜索变成12347,这回满足条件,继续排列下一个元素,如此直到10个元素全部排列完成。代码如下:a是储存排列的数组,n是元素个数,t用来控制递归过程。

     1 void PrimeCircle(int a[], int n, int t)
     2 {
     3     if(t == n)
     4     {
     5         Output(a, n) ; // 找到一个解
     6     }
     7     else
     8     {
     9         for(int i = 1; i <= n; i++)
    10         {
    11             a[t] = i ;
    12             if(IsOk(a)) // 检查当前值,满足条件才继续
    13                 PrimeCircle(a, n, t + 1) ;
    14         }
    15     }
    16 }

    2. 再看题目的输入范围,1 < N < 20,由于输入规模比较小,所以考虑使用查表法来判定素数,查表法是典型的以空间换时间的方法。20以内两个数之和最大是18 + 19 = 37,而37以内的素数分别是2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,我们可以定义一个38个元素的数组,以i为数组下标。当i为素数时,令a[i] = 1,否则a[i] = 0。这样,要判断一个数是否为素数时,直接判断a[i]是否为1即可。对应的数组如下:

     1 int prime[38= 
     2 {
     3     0011010
     4     1000101
     5     0001010
     6     0010000
     7     0101000,
     8     001,
     9 } ;

    判断i是否为素数的代码也很简单

    1 if (prime[i] == 1)  //素数
    2 {
    3     // do something
    4 }

    3. 再考虑输入的特点,如果输入N是奇数的话,由于起点从1开始,那么1-N之间一共有N / 2个偶数,N / 2 + 1个奇数,也就是奇数的个数比偶数多一个,那么把这N个数排成一个环,根据鸽巢原理,必然有两个奇数是相邻的,而两个奇数之和是偶数,偶数不是素数,所以我们得出结论,如果输入N是奇数的话,没有满足条件的排列。这样当N是奇数的时候,直接返回即可。如果1-N之间每个数输入的几率相同,这个判断可以减少一半的计算量。

    1 if(n & 1// 奇数无解,直接返回
    2     return ;
    3 

    4. 扩展一下第三点,可以发现,任何一个满足条件的排列都有一个共同点:相邻的两个数奇偶性必然不同,原因是:两个奇数之和或者两个偶数之和都是偶数,而偶数一定不是素数,所以在选取当前元素的时候,比较一下它和前一个元素的奇偶性。再做决定,可以减少一部分计算量。

    由 于奇数 + 偶数 = 奇数, 而奇数的二进制表示中,最低位是1, 所以有下面的代码, 其中curValue是当前值, a[lastIndex]是前一个值.

    1 if(!((curValue + a[lastIndex]) & 1)) // 相邻的数奇偶性必然不同
    2     return false ;
    3 

    经过上面的优化,代码如下,应该会比原来快很多。还有什么地方可以优化么?欢迎讨论!

    代码
     1 #include <iostream>
     2 using namespace std ;
     3 
     4 // 小于37的所有素数
     5 int prime[38= 
     6 {
     7     0011010
     8     1000101
     9     0001010
    10     0010000
    11     0101000,
    12     001,
    13 } ;
    14 
    15 // 输出一个解
    16 void Output(int a[], int n)
    17 {
    18     for(int i = 0; i < n; i++)
    19         cout << a[i] << " " ;
    20     cout << endl ;
    21 }
    22 
    23 // 判断当前序列是否满足条件
    24 bool IsOk(int a[], int lastIndex, int curValue)
    25 {
    26     if(lastIndex < 0// 第一个元素没有前驱元素,返回真
    27         return true ;
    28 
    29     if(!((curValue + a[lastIndex]) & 1)) // 相邻的数奇偶性必然不同
    30         return false ;
    31 
    32     if(!prime[a[lastIndex] + curValue]) //相邻元素和为素数
    33         return false ;
    34 
    35     for(int i = 0; i <= lastIndex; i++// 去重,curValue没有出现过
    36         if(a[i] == curValue)
    37             return false ;
    38 
    39     return true ;
    40 }
    41 
    42 void PrimeCircle(int a[], int n, int t)
    43 {
    44     if(n & 1// 奇数无解,直接返回
    45         return ;
    46 
    47     if(t == n) 
    48     {
    49         if(prime[a[0+ a[n - 1]]); // 判断首尾元素
    50             Output(a, n) ; 
    51     }
    52     else
    53     {
    54         for(int i = 1; i <= n; i++)
    55         {
    56             a[t] = i ;
    57             if(IsOk(a, t - 1, i)) //如果当前元素满足条件
    58                 PrimeCircle(a, n, t + 1) ; //进行下一次递归
    59         }
    60     }
    61 }
    62 
    63 int main(void)
    64 {
    65     int a[20] ;
    66     const int n = 4 ; // 4个元素的排列
    67     PrimeCircle(a, n, 0) ;
    68     
    69     system("pause") ;
    70     return 0 ;
    71 }
  • 相关阅读:
    Linux RAID部署
    系统运维架构师体系
    Linux系统上文件的特殊权限及文件acl
    Linux磁盘使用及文件系统管理
    中小规模网站架构组成
    优化配置模板主机
    网络原理基础
    MySQL二进制安装
    网络通讯基础
    点击改变背景
  • 原文地址:https://www.cnblogs.com/graphics/p/1698403.html
Copyright © 2011-2022 走看看