亲和数问题学习笔记——调和级数和回溯算法

亲和数问题学习笔记——调和级数和回溯算法

题目描述:

求500万以内的所有亲和数。

如果两个数a和b的所有真因数之和等于b,b的所有真因数之和等于a,则称a,b是一对亲和数。

例如:220和284,1184和1210,2620和2924.

 

需要注意的是关于真因数的定义:出去整数本身之外的所有因子都称作该整数的真因子,这里是包括1的。


首先,对这篇学习笔记中参考到的前辈学习经验的链接,置顶于正文。
http://blog.csdn.net/v_JULY_v/article/details/6441279
http://zh.wikipedia.org/wiki/%E8%B0%83%E5%92%8C%E7%BA%A7%E6%95%B0
http://www.cnblogs.com/hustcat/archive/2008/04/09/1144645.html

July的博文中已经给出的解法,由于我目前对算法的研究不是很深入,所以他解法中提到的一些用法还不是很理解,我再做一下详细解释。

解法一:

要求N以内的所有亲和数组合,我们就把N之类所有数的真因子都求出来,然后遍历一遍有没有满足亲和数条件的数。这样解法的关键就在于如何在一定的复杂度之内把所有数的真因子都求出来。我们先把代码给出,有个整体的认识:

#include<stdio.h>  
  
int sum[5000010];   //为防越界  
 
int main()   
{  
    int i, j; 
	int N=5000000;
    for (i = 0; i <= N; i++)   
        sum[i] = 1;  //1是所有数的真因数所以全部置1  
      
    for (i = 2; i + i <= N; 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 <= N)   
        {    
            //将所有i的倍数的位置上加i  
            sum[j] += i;    
            j += i;       
        }  
    }  
      
    for (i = 220; i <= N; i++)   //扫描,O(N)。  
    {  
        // 一次遍历,因为知道最小是220和284因此从220开始  
        if (sum[i] > i && sum[i] <= N && sum[sum[i]] == i)  
        {  
            //去重,不越界,满足亲和  
            printf("%d %d\n",i,sum[i]);  
        }  
    }  
    return 0;  
}  
这种解法的基本过程是:首先,我们先把所有sum都初始化为1,这个可以理解;然后,我们遍历i的所有倍数,这些数由于是i的倍数,所以它们的真因子一定会包含i,故有了sum[j]+=i;这样的语句,这里需要注意的是,i没有必要遍历到N,因为最大的真因数是不会超过N/2的,这样我们便可以得到所有数的真因子的和sum数组。这里还有一个细节需要解释一下,就是有的数会存在多个真因子,那么每次遍历的时候都会加上的,不会有遗漏。例如:6这个整数的真因子有2和3,那么在遍历2的倍数时,会有sum[6]+2的操作,在遍历3的倍数的时候,又会有sum[6]+3的操作。对于那些只有一个真因子的数,只需要加一次,而对于质数,就不要加了。

得到sum这个数组之后,我们就需要判断了,满足亲和数条件的整数需要给出。判断的条件有三个部分,sum[i]不能出界;去重;亲和数条件。满足条件的输出了。
关于去重的条件,我们也可以输出sum[i]==i;时候的情况,如下:
亲和数问题学习笔记——调和级数和回溯算法_第1张图片
也就是说496和8128这两个的真因子之和等于本身。

下面我们要详细说一下这种算法的复杂度。
显然,这种解法包含两个部分,一个是求出sum数组,一个是判断满足亲和数的条件。
1、求出sum数组:我们需要遍历所有小于N/2的i的倍数,那么遍历2的倍数的时候,我们需要遍历N/2次,遍历3的倍数,我们需要遍历N/3次,这个是显然的。那么依次类推,遍历到N/2的倍数的时候,我们需要遍历N*(2/N)次,把它们相加就是一个调和级数,即O(N/2*(1/2+1/3+...+2/N))。
对于调和级数的求和,有专门的研究资料。首先我们需要指出的是,调和级数是一个发散的无穷级数,如下:

它们的求和趋向于无穷大,那么为何在上july的博文中是ln(N)呢?原因是本题目的调和级数是部分求和,即N是一个确切的数,而不是无穷大。对于确实范围的调和级数求和确实是有界,证明如下:
亲和数问题学习笔记——调和级数和回溯算法_第2张图片
上面的证明说明了,当N为无穷大的时候,调和级数确实是趋向于无穷大;当N是一个确切的数的时候,为ln(k+1),那么本题目中又不包括第一个数,故这种情况下的调和级数之和就会趋向于ln(N)了。
这样以后在分析复杂度的时候,再遇到这种的调和级数就没有疑问了~
2、判断 满足亲和数的条件:正常遍历的复杂度为O(N)。
所以解法一的综合复杂度为O(N*log(N)+N)=O(N*logN).

在解法一的注释中提到了回溯法,下面我们再介绍一下回溯法:
寻找问题的解的一种可靠的方法是首先列出所有候选解,然后依次检查每一个,在检查完所有或部分候选解后,即可找到所需要的解。
理论上,当候选解数量有限并且通过检查所有或部分候选解能够得到所需解时,上述方法是可行的。不过,在实际应用中,很少使用这种方法,因为候选解的数量通常都非常大(比如指数级,甚至是大数阶乘),即便采用最快的计算机也只能解决规模很小的问题。
对候选解进行系统检查的方法有多种,其中回溯和分枝定界法是比较常用的两种方法。按照这两种方法对候选解进行系统检查通常会使问题的求解时间大大减少(无论对于最坏情形还是对于一般情形)。事实上,这些方法可以使我们避免对很大的候选解集合进行检查,同时能够保证算法运行结束时可以找到所需要的解。因此,这些方法通常能够用来求解规模很大的问题。
其实回溯算法的应用有幂集问题、迷宫问题、N皇后问题。个人觉得迷宫问题的解法本质最能够说明回溯算法的思想本质。

计算机解迷宫时,通常用的是"试探和回溯"的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止,如果所有可能的通路都试探过,还是不能走到终点,那就说明该迷宫不存在从起点到终点的通道。

  1.从入口进入迷宫之后,不管在迷宫的哪一个位置上,都是先往东走,如果走得通就继续往东走,如果在某个位置上往东走不通的话,就依次试探往南、往西和往北方向,从一个走得通的方向继续往前直到出口为止;

  2.如果在某个位置上四个方向都走不通的话,就退回到前一个位置,换一个方向再试,如果这个位置已经没有方向可试了就再退一步,如果所有已经走过的位置的四个方向都试探过了,一直退到起始点都没有走通,那就说明这个迷宫根本不通;


所以说,回溯算法的本质就是一个先序遍历一棵“状态树”的过程,只是这棵树不是遍历前就先建立的,而是隐含在遍历过程中的一棵“状态树”。这方面的知识是来源于数据结构!

解法2:质数线性筛法,这个方法比较数学,现在先不理解,日后再看。
可以参考如下链接的帖子:
http://bbs.csdn.net/topics/360246918


你可能感兴趣的:(亲和数问题学习笔记——调和级数和回溯算法)