当前位置:编程学习 > C/C++ >>

程序员编程艺术:第六章、求解500万以内的亲和数

前奏
    本章陆续开始,除了继续保持原有的字符串、数组等面试题之外,会有意识的间断性节选一些有关数字趣味小而巧的面试题目,重在突出思路的“巧”,和“妙”。本章亲和数问题之关键字,“500万”,“线性复杂度”。

第一节、亲和数问题
题目描述:
求500万以内的所有亲和数
如果两个数a和b,a的所有真因数之和等于b,b的所有真因数之和等于a,则称a,b是一对亲和数。
例如220和284,1184和1210,2620和2924。

分析:
    首先得明确到底是什么是亲和数?

亲和数问题最早是由毕达哥拉斯学派发现和研究的。他们在研究数字的规律的时候发现有以下性质特点的两个数:
220的真因子是:1、2、4、5、10、11、20、22、44、55、110;
284的真因子是:1、2、4、71、142。
而这两个数恰恰等于对方的真因子各自加起来的和(sum[i]表示数i 的各个真因子的和),即
220=1+2+4+71+142=sum[284],
284=1+2+4+5+10+11+20+22+44+55+110=sum[220]。
得284的真因子之和sum[284]=220,且220的真因子之和sum[220]=284,即有sum[220]=sum[sum[284]]=284。

如此,是否已看出丝毫端倪?

如上所示,考虑到1是每个整数的因子,把出去整数本身之外的所有因子叫做这个数的“真因子”。如果两个整数,其中每一个真因子的和都恰好等于另一个数,那么这两个数,就构成一对“亲和数”

求解:
    了解了什么是亲和数,接下来咱们一步一步来解决上面提出的问题(以下内容大部引自水的原话,同时水哥有一句原话,“在你真正弄弄懂这个范例之前,你不配说你懂数据结构和算法”)。

看到这个问题后,第一想法是什么?模拟搜索+剪枝?回溯?时间复杂度有多大?其中bn为an的伪亲和数,即bn是an的真因数之和大约是多少?至少是10^13(@iicup:N^1.5 对于5*10^6 , 次数大致 10^10 而不是 10^13.)的数量级的。那么对于每秒千万次运算的计算机来说,大概在1000多天也就是3年内就可以搞定了(iicup的计算: 10^13 / 10^7 =1000000(秒) 大约 278 小时. )。如果是基于这个基数在优化,你无法在一天内得到结果的。
一个不错的算法应该在半小时之内搞定这个问题,当然这样的算法有很多。节约时间的做法是可以生成伴随数组,也就是空间换时间,但是那样,空间代价太大,因为数据规模庞大。
在稍后的算法中,依然使用的伴随数组,只不过,因为题目的特殊性,只是它方便和巧妙地利用了下标作为伴随数组,来节约时间。同时,将回溯的思想换成递推的思想(预处理数组的时间复杂度为logN(调和级数)*N,扫描数组的时间复杂度为线性O(N)。所以,总的时间复杂度为O(N*logN+N)(其中logN为调和级数)  )。

第二节、伴随数组线性遍历
依据上文中的第3点思路,编写如下代码:

view plaincopy to clipboardprint?
//求解亲和数问题 
 
//第一个for和第二个for循环是logn(调和级数)*N次遍历,第三个for循环扫描O(N)。 
//所以总的时间复杂度为 O(n*logn)+O(n)=O(N*logN)(其中logN为调和级数)。 
 
//关于第一个for和第二个for寻找中,调和级数的说明: 
//比如给2的倍数加2,那么应该是  n/2次,3的倍数加3 应该是 n/3次,... 
//那么其实就是n*(1+1/2+1/3+1/4+...1/(n/2))=n*(调和级数)=n*logn。 
 
//copyright@ 上善若水 
//July、updated,2011.05.24。 
#include<stdio.h> 
 
int sum[5000010];   //为防越界 
 
int main()  

    int i, j; 
    for (i = 0; i <= 5000000; i++)  
        sum[i] = 1;  //1是所有数的真因数所以全部置1 
     
    for (i = 2; i + i <= 5000000; i++)  //预处理,预处理是logN(调和级数)*N。 
        //@litaoye:调和级数1/2 + 1/3 + 1/4......的和近似为ln(n), 
        //因此O(n *(1/2 + 1/3 + 1/4......)) = O(n * ln(n)) = O(N*log(N))。 
    {   
        //5000000以下最大的真因数是不超过它的一半的 
        j = i + i;  //因为真因数,所以不能算本身,所以从它的2倍开始 
        while (j <= 5000000)  
        {   
            //将所有i的倍数的位置上加i 
            sum[j] += i;   
            j += i;      
        } 
    } 
     
    for (i = 220; i <= 5000000; i++)   //扫描,O(N)。 
    { 
        // 一次遍历,因为知道最小是220和284因此从220开始 
        if (sum[i] > i && sum[i] <= 5000000 && sum[sum[i]] == i) 
        { 
            //去重,不越界,满足亲和 
            printf("%d %d ",i,sum[i]); 
        } 
    } 
    return 0; 

运行结果:

 

 @上善若水:

    1、可能大家理解的还不是很清晰,我们建立一个5 000 000 的数组,从1到2 500 000 开

补充:软件开发 , C语言 ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,