|  | 
 
 发表于 2016-10-17 11:05:00
|
显示全部楼层 
| 本帖最后由 jerryxjr1220 于 2016-10-17 14:02 编辑 
 这题其实有好多解法,我也想正好借这题,分享一些使用python解决一些数学题的心得。
 【版主,在比赛结束后,如果觉得这篇经验分享还实用的话,希望能申请精华,让更多的鱼油看到,毕竟也花了我好些时间写的!@小甲鱼 @冬雪雪冬 】
 主要是针对没有任何算法基础的鱼油,算法高手可以直接跳过。
   
 首先,从解题思路开始吧:
 1. 最简单粗暴的方法,当然是暴力解法,就是从1开始依次往上增加,最大值当然是987654321,因为超过这个值就没法构成Pandigital了。
 a)用暴力解法获取候选数
 b)有了候选数以后就依次判断每个数,是否是Pandigital
 i) 候选数判断法:判断方法其实很简单,只要候选数中从1~n没有重复的数字即是Pandigital (举例:可以利用函数set()函数及len()把候选数列化为元组,再判断元组长度是否与原来相同,如果有重复的,长度必然会不同。)
 ii)Pandigital直接生成法:直接生成的Pandigital数列可以省去判断的步骤,节省计算时间,后面介绍
 c)有了Pandigital数以后就要判断是否是质数(质数的定义是:除了可以被1和自己本身整除以外,不能被其他自然数整除的数)
 质数的判断方法也有很多:
 i) 暴力解法,同样使用从2开始的(1可以不用尝试,因为任何自然数必然能被1整除)自然数依次与候选数相除,上限是比本身小1的整数。如果中间没有任何一个数能与之整除,则即是质数,不然就不是(不是质数的自然数就是合数(除1以外))。
 ii) 优化算法1: 候选数范围优化,后面介绍
 iii)优化算法2: 表格标记法优化,后面介绍
 iv)其他优化算法: 后面介绍
 d)判断完质数以后,如果是质数,则保存起来后续要比较大小,如果不是则废弃。
 比较大小其实也有不同的方法,而且所用时间也各不相同。
 i) 函数法:直接用max()或min()函数,求最大最小值。优点:简单,但如果候选数非常多的话,速度也不快(因为它的原理也是依次比较,然后判断)
 ii)标记法:直接在计算过程中标记出最大值或最小值,这样只要计算完最大或最小值就出来了,不用额外计算时间。
 iii)后排序法:用sort()或者sorted()对运算的结果排序后,那么第一个或者最后一个就是最大最小值。
 iv)前排序法:用sort()或者sorted()对运算之前的候选数排序,那么这样只要在运行过程中第一个满足运算条件的数,就是要找的最大或最小值,后面的其他候选数都不用计算了。
 做完,这么多步骤以后,我们终于得到我们需要的答案了。
 2. 当然,你完全可以按照(1.)中描述的暴力解法的思路进行解题,而且只要思路正确,有足够时间,肯定能计算出结果。
 但是,我要说的当然远不是这样。如果全部用上述的暴力解法解这题估计得花上几天(说不定几周)的时间来运行程序,这当然不是我们所希望的。
 那么,我们就需要对上述的暴力解法进行优化了。(其实上述暴力解法的每个步骤都可以优化,我会逐步说明。)
 a)候选数的优化
 要判断候选数是不是Pandigital,最快的方法,当然是直接生成Pandigital啦,这样根本都不需要判断
   所以,我用了Pandigital的生成法。生成法也有不同的方式,我写了2种不同的方案。
 第一种是利用python的itertools库的函数permutations,生成排列组合。优点是程序简单。
 
 复制代码from itertools import permutations as per
def getPan1(n):
        pan = per(range(1,n+1),n)
        panlist = []
        for each in pan:
            ee = 0
            for e in each:
                ee = ee*10 + e
            panlist.append(ee)
        return panlist
第二种是用插入法,自己写的生成程序,运用了递归算法。
 
 复制代码def getPan2(n):
        if n == 2:
                return [12,21]
        if n>2:
                pan = getPan2(n-1)
                length = len(str(pan[0]))
                pannew = []
                for each in pan:
                        pannew.append(int(str(n)+str(each)))
                        pannew.append(int(str(each)+str(n)))
                        for j in range(1,length):
                                pannew.append(int(str(each)[:j]+str(n)+str(each)[j:]))
                return pannew
两种算法各有利弊,对于本题来说,差异并不是太明显,但是对于大数量的排序的话,差异会越来越明显。
 b)质数判断优化
 有了候选数以后就要判断质数了。
 i)候选数的范围优化
 其实,我们不需要把每个候选数都拿去和比候选数小的自然数相除来判断是否是质数。
 质数有几个特性:
 > 除了2以外,能被2整除的都不是质数(废话)。这样就可以砍掉一半的候选数了。
 > 除了2和3以外的质数必然符合6k+/-1(k为自然数),这样又减少了1/6的数量。
 > 而在相除时,只需相除到n**0.5(即根号n)即可,不需要全部除到完,这样再减少3/4的工作量。
 
 复制代码def isPrime1(num):
        if num == 2 or num ==3:
                return True
        elif num%2 == 0:
                return False
        elif (num-1) % 6 != 0 and (num+1) % 6 != 0:
                return False
        else:
                for i in range(3,int(num**0.5+1),2):
                        if num%i == 0:
                                return False
                return True
ii)表格标记法优化
 这种方法是先建立一个“空”的表格,然后依次把可能的项填入,或把不可能的项移除。标记法往往可以和递归算法套用,运用递归算法的便捷,加上标记法的高效记录。(纯用递归算法的话,往往效率很低,特别是嵌套层数很深的情况。)
 在这里例子中,我是先建立了一个全为True的表格,然后依次把能除净的数(还包括0和1)对应的空格标记为False,那么剩下的就都是质数了。表格的index和表格的值,可以用enumerate()函数联系起来。另外,表格的范围也只需到n**0.5的大小即可。
 
 复制代码def getPrime(n):
    primes = [True]*n
    primes[0], primes[1] = False, False
    for (i, prime) in enumerate(primes):
        if prime:
            for j in range(i*i,n,i):
                primes[j] = False
    primelist = []
    for (k, trueprime) in enumerate(primes):
        if trueprime:
            primelist.append(k)
    return primelist
primes = getPrime(10001)
def isPrime2(num):
    if num in primes :
        return True
    elif (num-1) % 6 != 0 and (num+1) % 6 != 0:
        return False
    else:
        for eachprime in primes:
            if num % eachprime == 0:
                return False
        return True
iii)其他算法优化
 其实,上述的优化算法都可以相互结合,取长补短,根据不同的情况灵活调用。
 通过这些性质的优化后,所剩的候选数已经足够可以被我们日常的计算机所应付了。
 c) 比较大小数的优化
 在取得了是质数并且是Pandigital的数以后,最后一步就是取得这些值中的最大值
 上面提的的函数法、标记法和后排序法,其实都是属于后运算的算法。这些算法都是要运算并且比较所有的候选数。
 那么,有没有前运算的算法呢? 答案是有的,前排序法就是前运算的算法。
 这种算法的好处是,一旦得到第一个符合条件的值以后,后续的数值就不需要再运算了,可以节省大量时间。同时也省去了保存候选数的麻烦,因为根本都不需要保存。
 方法就是:用sort(),reverse()或者sorted(),reversed()对运算之前的候选数排序,那么这样只要在运行过程中第一个满足运算条件的数,就是要找的最大或最小值,后面的其他候选数都不用计算了。
 
 复制代码for k in range(9,1,-1):
        testpan = reversed(getPan1(k))
        for eachpan in testpan:
                if isPrime1(eachpan):
                    print (eachpan)
                    end = time()
                    print ('Use: %.3f sec.' % (end-start))
                    exit()
3. 结果
 
 这个就是运行的结果,我使用的是getPan1()及isPrime1()的方法。大家可以自行调试不同方法的差异。基本上这几种方法都是很快速的,都可以控制在0.5秒以内完成。这样,我们就把一个程序从原本可能要运行几天的时间,缩短到毫秒级的单位,这都是算法的功劳。
 源代码如下:
 
 复制代码from itertools import permutations as per
from time import time
start = time()
def isPrime1(num):
        if num == 2 or num ==3:
                return True
        elif num%2 == 0:
                return False
        elif (num-1) % 6 != 0 and (num+1) % 6 != 0:
                return False
        else:
                for i in range(3,int(num**0.5+1),2):
                        if num%i == 0:
                                return False
                return True
    
def getPan1(n):
        pan = per(range(1,n+1),n)
        panlist = []
        for each in pan:
            ee = 0
            for e in each:
                ee = ee*10 + e
            panlist.append(ee)
        return panlist
for k in range(9,1,-1):
        testpan = reversed(getPan1(k))
        for eachpan in testpan:
                if isPrime1(eachpan):
                    print (eachpan)
                    end = time()
                    print ('Use: %.3f sec.' % (end-start))
                    exit()
我主要想通过这个例子告诉大家,其实,编程语言固然重要(Python是个非常好的工具,感谢小甲鱼的视频课程,我目前学习了1个月的时间,收益匪浅!),但是一个合理的算法往往比编程语言本身更重要。不要看几个毫秒好像和几秒差别不大,但是如果放到大数量级的地方,往往就是算得出和算不出的差别。
 最后,我想说,算法其实没有好坏之分,只有合适和不合适之别。暴力算法确实是最“笨”的办法,但是当你一头雾水、毫无头绪的时候,暴力算法往往是最直接有效的算法,而且不需要特别高深的数学知识,只需要把描述变成计算机语言即可(对数据量不是特别大的情况都适用)。
 感谢大家耐心看完本篇,我的程序也不是最优的程序,还有多种方法可以继续优化下去。但是对于本题来说,应该足够了。
 
 
 | 
 评分
查看全部评分
 |