文章参考:http://blog.csdn.net/v_JULY_v/article/details/6441279
本文是基于上文链接的内容进行裁剪,在自己的理解层次上写出的文章。
题目描述:
求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是每个整数的因子,把出去整数本身之外的所有因子叫做这个数的“真因子”。如果两个整数,其中每一个真因子的和都恰好等于另一个数,那么这两个数,就构成一对“亲和数”。
首先,我们想到的是:从1->n遍历,求出i的子因子和j,然后判断j的子因子和是否为i。但是工作量太大了,显然不是一种有效的方法。
如何优化?我们观察数组,i是其某一子因子的倍数。比如8,是1、2、4的倍数;10是1、2、5的倍数。这里的2,即在8中,又在10中。如果对一个数都依次求其单独的子因子,必然会重复。那么换个角度考虑,将数i计算从2、3开始的倍数,那么这个求出来的值,子因子必然包含i,附设一个伴随数组,专门保存子因子和,对应的子因子和加上i即可,然后i++。
先上代码:
//求解亲和数问题
//第一个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/n",i,sum[i]);
}
}
return 0;
}
运行结果:
我再来具体解释下上述程序的原理,ok,举个例子,假设是求10以内的亲和数,求解步骤如下:
因为所有数的真因数都包含1,所以,先在各个数的下方全部置1
- 然后取i=2,3,4,5(i<=10/2),j依次对应的位置为j=(4、6、8、10),(6、9),(8),(10)各数所对应的位置。
- 依据j所找到的位置,在j所指的各个数的下面加上各个真因子i(i=2、3、4、5)。
整个过程,即如下图所示(如sum[6]=1+2+3=6,sum[10]=1+2+5=8.):
1 2 3 4 5 6 7 8 9 10
1 1 1 1 1 1 1 1 1 1
2 2 2 2
3 3
4
5 - 然后一次遍历i从220开始到5000000,i每遍历一个数后,
将i对应的数下面的各个真因子加起来得到一个和sum[i],如果这个和sum[i]==某个i’,且sum[i‘]=i,
那么这两个数i和i’,即为一对亲和数。 - i=2;sum[4]+=2,sum[6]+=2,sum[8]+=2,sum[10]+=2,sum[12]+=2...
i=3,sum[6]+=3,sum[9]+=3...
...... - i=220时,sum[220]=284,i=284时,sum[284]=220;即sum[220]=sum[sum[284]]=284,
得出220与284是一对亲和数。所以,最终输出220、284,...
评价:我们可以清晰的发现连续数据的映射可以通过数组结构本身的特点替代,用来节约空间,这是数据结构的艺术。在大规模连续数据的回溯处理上,可以通过转化为递推生成的方法,逆向思维操作,这是算法的艺术。
另一位牛人的算法,通过数学推导出公式,然后求解得到:
设n = p1^a1 * p2^a2 * .... pn^an(p1-pn为因数分解,a1-an为幂),则n的所有约数和 = (p1^(a1+1) - 1) * (p2^(a2+1) - 1) * .... (pn^(an+1) - 1)/(p1-1)(p2-1).......(pn-1)
比如2*3*5*7的约数和 = 3*4*6*8
设f(n)是n的所有约数的和,有了上面的递推,算90这样的数的约数和时,没有必要把1、2、3、5、6、9、10、15、18、30、45、90都加一遍,f(90) = f(45) * 3,因此把质数的线性筛法改一下,就可以了。
当然很有可能还有更好的方法,如果有构造的可能的话,也许效率会非常高,我就当抛砖引玉了。
//求解亲和数问题
//copyright@ litaoye
//July、胡滨,updated,2011.05.26。
using System;
using System.Collections.Generic;
namespace CSharpTest
{
class Program
{
public static void Main()
{
int max = 5000000;
DateTime start = DateTime.Now;
int[] counter = CreateCounter(max);
for (int i = 0; i < counter.Length; i++)
{
int num = counter[i] - i;
//if (num < counter.Length && num > i && counter[num] == counter[i])
// Console.WriteLine("{0} {1}", i, num);
}
Console.WriteLine((DateTime.Now - start).TotalSeconds);
Console.ReadKey();
}
static int[] CreateCounter(int n)
{
List<int> primes = new List<int>();
int[] counter = new int[n + 1];
counter[1] = 1;
for (int i = 2; i <= n; i++)
{
if (counter[i] == 0)
{
counter[i] = i + 1;
primes.Add(i);
}
for (int j = 0; j < primes.Count; j++)
{
if (primes[j] * i > n)
break;
if (i % primes[j] == 0)
{
int k = i;
int l = primes[j] * primes[j];
while (k % primes[j] == 0)
{
l *= primes[j];
k /= primes[j];
}
counter[primes[j] * i] = counter[k] * (l - 1) / (primes[j] - 1);
break;
}
else
counter[primes[j] * i] = counter[i] * (primes[j] + 1);
}
}
return counter;
}
}
}
/*
测试结果:
0.484375
0.484375
0.46875
单位second。
*/
注释:
本章提到了一些概念,“搜索剪枝”“回溯”,下去要对这些基本算法总结一下。