西西軟件園多重安全檢測下載網站、值得信賴的軟件下載站!
軟件
軟件
文章
搜索

首頁編程開發(fā)其它知識 → 趣味編程之計算相親數(上)

趣味編程之計算相親數(上)

相關軟件相關文章發(fā)表評論 來源:本站整理時間:2010/8/22 18:20:15字體大。A-A+

作者:佚名點擊:140次評論:1次標簽: 編程

  • 類型:編程輔助大小:1.8M語言:英文 評分:6.0
  • 標簽:
立即下載

一直想寫這篇關于算法的文章,但是由于看到園子里眾多研究算法的高手讓我一直沒有決心寫下來,但高手歸高手,不是高手也可以寫出來讓高手們拍磚,所以今天就在這里獻丑了。相親數和完全數作為數學問題的確是頂級難題,可是拿來做娛樂就不同了,從剛接觸編程時C語言書上的課后習題到兩年前的Intel多核編程大賽,這個題目一直伴隨著我們,讓我們來娛樂一下吧。

簡單說一下概念,相親數是指兩個正整數中,彼此的全部約數之和(本身除外)與另一方相等。舉例來說:

220的全部約數(除掉本身)相加是:1+2+4+5+10+11+20+22+44+55+110=284

284的全部約數(除掉本身)相加的和是:1+2+4+71+142=220

所以220和284就是一對相親數。

那什么是完全數呢?即它所有的真因子(即除了自身以外的約數)的和恰好等于它本身。例如:

第一個完全數是6,它有約數1、2、3、6,除去它本身6外,其余3個數相加,1+2+3=6

第二個完全數是28,它有約數1、2、4、7、14、28,除去它本身28外,其余5個數相加,1+2+4+7+14=28

概念不多說了,直接上算法。

算法一:直接計算約數之和

這是最直接的方式了,循環(huán)計算所有可能成為約數的數字然后加和,直接到不用做過多的解釋,只要會寫程序,用任何語言都能實現(xiàn)!

1: /// <summary>
2: /// 直接計算約數之和(串行)
3: /// </summary>
4: public class Algorithm1
5: {
6: private int GetSum(int num)
7: {
8: int sum = 1;
9: int limit = (int)Math.Sqrt(num);
10: for (int i = 2; i <= limit; i++)
11: if (num % i == 0) sum += i + num / i;
12: return sum;
13: }
14:
15: public void Run(int from, int to)
16: {
17: int perfertCount = 0;
18: int amicablePairCount = 0;
19: for (int num = from; num <= to; num++)
20: {
21: int sum1 = this.GetSum(num);
22: if (sum1 == num)
23: {
24: Console.WriteLine("{0}是完全數", num);
25: perfertCount++;
26: }
27: if (sum1 > num)
28: {
29: int sum2 = this.GetSum(sum1);
30: if (sum2 == num)
31: {
32: Console.WriteLine("{0}和{1}是一對相親數", sum1, sum2);
33: amicablePairCount++;
34: }
35: }
36: }
37: Console.WriteLine("在{0}到{1}中共有{2}個完全數和{3}對相親數", from, to, perfertCount, amicablePairCount);
38: }
39: } 測試代碼,從2計算到5000000:

1: static void Main(string[] args)
2: {
3: var stopwatch = Stopwatch.StartNew();
4: Algorithm1 algorithm = new Algorithm1();
5: algorithm.Run(2, 5000000);
6: stopwatch.Stop();
7: Console.WriteLine("計算完成共花費{0}秒", stopwatch.Elapsed.TotalSeconds);
8: Console.ReadKey();
9: } 在我的ThinkPad R400上測試運行時間大概在51秒左右,速度能不能再提高呢,讓我們看看.Net4.0為我們帶來的并行計算的新特性表現(xiàn)如何。

1: /// <summary>
2: /// 直接計算約數之和(并行)
3: /// </summary>
4: public class Algorithm2
5: {
6: private int GetSum(int num)
7: {
8: int sum = 1;
9: int limit = (int)Math.Sqrt(num);
10: for (int i = 2; i <= limit; i++)
11: if (num % i == 0) sum += i + num / i;
12: return sum;
13: }
14:
15: public void Run(int from, int to)
16: {
17: int perfertCount = 0;
18: int amicablePairCount = 0;
19: Parallel.For(from, to, num =>
20: {
21: int sum1 = this.GetSum(num);
22: if (sum1 == num)
23: {
24: Console.WriteLine("{0}是完全數", num);
25: perfertCount++;
26: }
27: if (sum1 > num)
28: {
29: int sum2 = this.GetSum(sum1);
30: if (sum2 == num)
31: {
32: Console.WriteLine("{0}和{1}是一對相親數", sum1, sum2);
33: amicablePairCount++;
34: }
35: }
36: });
37: Console.WriteLine("在{0}到{1}中共有{2}個完全數和{3}對相親數", from, to, perfertCount, amicablePairCount);
38: }
39: } 注意第19行,我們使用System.Threading.Tasks下的Parallel類取代傳統(tǒng)的for循環(huán),由于在該算法中每一次計算都是獨立的,所以很適合并行,廢話不多說,直接運行看結果,運行時間在26秒左右,由于我的機器是雙核,所以同樣是從2計算到5000000,并行的時間差不多是之前的(51秒)一半,看來Parallel真是不錯的新工具。‘斎,這個是從技術上達到了速度的提升,算法本質還沒有變,那能不能從算法本身提高計算效率呢?答案當然是肯定的!

算法二:通過計算所有質因數來計算約數之和

先說一下原理:記得小學的奧賽有一個題型是計算一個自然數的約數的個數(現(xiàn)在還有沒有不清楚),截法很簡單,先分解質因數,然后把所有質因子的冪加一再相乘就是約數的個數,例如數字36=2^2×3^2,那么36就有(2+1)×(2+1)=9個約數(1,2,3,4,6,9,12,18,36)。其實能算出有9個約數,自然可以計算9個約數是什么,是2個2和2個3排列組合的結果,剩下的就不用我廢話了,該算法的思路就是先分解質因數然后在逐個計算約數并加和,上代碼:

1: /// <summary>
2: /// 通過計算所有質因數來計算約數之和(串行)
3: /// </summary>
4: public class Algorithm3
5: {
6: private int GetNextFactor(int num)
7: {
8: int limit = (int)(Math.Sqrt(num));
9: for (int i = 2; i <= limit; i++)
10: if (num % i == 0)
11: return i;
12: return num;
13: }
14: private List<int> Decomposition(int num)
15: {
16: var factors = new List<int>();
17: while (true)
18: {
19: var divisor = GetNextFactor(num);
20: factors.Add(divisor);
21: if (divisor == num) break;
22: num = num / divisor;
23: }
24: return factors;
25: }
26: private int Sum(List<int> divisors)
27: {
28: int sum = 0;
29: for (int i = 0, count = divisors.Count - 1; i < count; i++)
30: sum += divisors[i];
31: return sum;
32: }
33: private int GetSum(List<int> factors)
34: {
35: if (factors.Count == 1) return 1;//質數
36: var divisors = new List<int>() { 1 };
37: var factorPows = new List<List<int>>() { new List<int>() { factors[0] } };
38: for (int i = 1, count = factors.Count; i < count; i++)
39: {
40: var length = factorPows.Count;
41: if (factors[i] == factorPows[length - 1][0])
42: factorPows[length - 1].Add(Convert.ToInt32(Math.Pow(Convert.ToDouble(factors[i]), Convert.ToDouble(factorPows[length - 1].Count + 1))));
43: else
44: factorPows.Add(new List<int>() { factors[i] });
45: }
46: for (int f = 0, fCount = factorPows.Count; f < fCount; f++)
47: for (int d = 0, dCount = divisors.Count; d < dCount; d++)
48: for (int p = 0, pCount = factorPows[f].Count; p < pCount; p++)
49: divisors.Add(divisors[d] * factorPows[f][p]);
50: return Sum(divisors);
51: }
52: public void Run(int from, int to)
53: {
54: int perfertCount = 0;
55: int amicablePairCount = 0;
56: for (var num = from; num < to; num++)
57: {
58: int sum1 = this.GetSum(Decomposition(num));
59: if (sum1 == num)
60: {
61: Console.WriteLine("{0}是完全數", num);
62: perfertCount++;
63: }
64: if (sum1 > num)
65: {
66: int sum2 = this.GetSum(Decomposition(sum1));
67: if (sum2 == num)
68: {
69: Console.WriteLine("{0}和{1}是一對相親數", sum1, sum2);
70: amicablePairCount++;
71: }
72: }
73: }
74: Console.WriteLine("在{0}到{1}中共有{2}個完全數和{3}對相親數", from, to, perfertCount, amicablePairCount);
75: }
76: } 先看速度如何,是否比算法一更快,從2計算到5000000花費近27秒,幾乎與算法一的并行版本接近,果然快很多,那么快在哪里了呢?算法一做了大量的取模和除法操作,相比于加、減、乘等操作這些操作都是很耗時的,而算法二先計算質因數,減少了很多取模和除法操作,取而代之的是很多乘法和指數運算,性能自然得到提升,但還算細心的我并沒有就此下結論,我把計算范圍縮小到2到100000,此時,算法一花費時間是0.18秒而算法而則是0.36秒,算法一反而取勝了,其實道理很簡單,隨著數字的增大,算法一計算取模和除法的操作增加也不斷增加,而算法二隨著數字增大計算次數卻增加不明顯,因為分解質因數其實是找質數的過程,但找到第一個質因數后,數字就變小了,計算量并沒增加多少,相反,找到的質因數越多,計算約數的計算量就越大,所以在數字不大(相對不大)的領域里,試商次數不多所以算法一很快完成了計算,而算法二相對于算法一的卻沒什么太大優(yōu)勢,但隨著數字的變大,算法二的優(yōu)勢會越來越明顯!例如我從5000000計算到5500000,算法一就豪無優(yōu)勢了,落后算法二一半都不止,這讓我想起了一個古老的但卻眾所周知的梵塔問題,算法一就是這樣一個梵塔……

當然我也沒忘記把這個算法改成并行版本,我就不貼代碼了,只要改第56行的for就可以了,從2計算到5000000花費在16秒左右,這樣我們已經從最初的51秒降低到16秒,還能更快嗎?我絞盡腦汁暫時還沒什么結果,因為我發(fā)現(xiàn)最后所花的時間都是在尋找質數上了,難怪數學界的頂級難題都跟質數有關或者圍繞質數展開,還有我們程序員熟悉的加密算法RSA,也是基于大整數難以分解的“特性”上,如果不幸被我找到了快速分解算法我就不用在這寫博客啦,扯遠了,還是回歸娛樂吧,我們還有沒有辦法讓它再快點呢?

算法二SP1:之所以叫SP1是因為它并沒有本質上的更改,只是在外圍讓它顯得更快。話說算法界有兩大秘籍,九陰真經和九陽神功——都不是,開個玩笑,其實沒什么秘籍,而是方法論,無非就是時間換空間和空間換時間,根據我們的需要,時間和空間在我們的代碼中轉換來轉換去,既然我追求的是速度,那自然是用空間來換時間嘍。

算法一我還沒想到哪里可以用空間來換取速度,倒是在對算法二的研究過程中我意識到大量的重復計算,最典型的是計算質數,如果緩存這些質數速度會不會快一些呢,其實是廢話當然會快,花了空間速度還沒提升的事情誰會愿意做呢,但僅僅緩存質數遠遠不夠,因為最大量的計算根本不在這里,如果連續(xù)的看待分解的過程,你就會發(fā)現(xiàn)它是一個遞歸的過程,之前的計算結果對后面很有用,比如我們已經分解了36=2^2×3^2。那么當我們分解72的時候是怎樣的過程呢,先找到了第一個因子2,然后得到36,繼續(xù)分解,36的分解過程又做一次,重復量可想而知,有人說,你把2^2×3^2記錄下來,在計算72的時候直接利用36的計算結果,說的沒錯,但我記錄的信息是不是太大了,空間也不是這么揮霍的啊,于是我權衡再三,采用了下面的算法,先看代碼:

1: /// <summary>
2: /// 通過計算所有質因數來計算約數之和(空間算法)
3: /// </summary>
4: public class Algorithm5
5: {
6: public List<int> primeList = new List<int>();
7: public int[] firstFactorList;
8: public int[] remainingList;
9: public int[] resultList;
10: public int GetNextFactor(int num)
11: {
12: var max = (int)Math.Sqrt(num);
13: for (int i = 0; i < primeList.Count; i++)
14: {
15: var p = primeList[i];
16: if (p > max) break;
17: if (num % p == 0)
18: return p;
19: }
20: primeList.Add(num);
21: return num;
22: }
23: public List<int> Decomposition(int num)
24: {
25: var divisors = new List<int>();
26: var factor = firstFactorList[num] = GetNextFactor(num);
27: if (factor == num)
28: remainingList[num] = 1;
29: else
30: remainingList[num] = num / firstFactorList[num];
31: while (true)
32: {
33: divisors.Add(firstFactorList[num]);
34: if (remainingList[num] == 1) break;
35: num = remainingList[num];
36: }
37: return divisors;
38: }
39: private int Sum(List<int> divisors)
40: {
41: int sum = 0;
42: for (int i = 0, count = divisors.Count - 1; i < count; i++)
43: sum += divisors[i];
44: return sum;
45: }
46: public int GetSum(List<int> factors)
47: {
48: if (factors.Count == 1) return 1;//質數
49: var divisors = new List<int>() { 1 };
50: var factorPows = new List<List<int>>() { new List<int>() { factors[0] } };
51: for (int i = 1, count = factors.Count; i < count; i++)
52: {
53: var length = factorPows.Count;
54: if (factors[i] == factorPows[length - 1][0])
55: factorPows[length - 1].Add(Convert.ToInt32(Math.Pow(Convert.ToDouble(factors[i]), Convert.ToDouble(factorPows[length - 1].Count + 1))));
56: else
57: factorPows.Add(new List<int>() { factors[i] });
58: }
59: for (int f = 0, fCount = factorPows.Count; f < fCount; f++)
60: for (int d = 0, dCount = divisors.Count; d < dCount; d++)
61: for (int p = 0, pCount = factorPows[f].Count; p < pCount; p++)
62: divisors.Add(divisors[d] * factorPows[f][p]);
63: return Sum(divisors);
64: }
65: public void Run(int limit)
66: {
67: firstFactorList = new int[limit];
68: remainingList = new int[limit];
69: resultList = new int[limit];
70: int perfertCount = 0;
71: int amicablePairCount = 0;
72: for (var num = 2; num < limit; num++)
73: {
74: var result = resultList[num] = this.GetSum(Decomposition(num));
75: if (result == num)
76: {
77: Console.WriteLine("{0}是完全數", num);
78: perfertCount++;
79: }
80: else if (result < num && resultList[result] == num)
81: {
82: Console.WriteLine("{0}和{1}是一對相親數", result, num);
83: amicablePairCount++;
84: }
85: }
86: Console.WriteLine("在{0}到{1}中至少有{2}個完全數和{3}對相親數", 2, limit, perfertCount, amicablePairCount);
87: }
88: } 我緩存了質數,每個數字的第一個因子和剩余因子的乘積以及每個數字的約數和,代碼之所以沒有注釋是因為我寫不清楚,給大家舉個例子,大家再對照代碼看就好:比如分解72,先找到它的第一個因子2,和剩余因子的乘積36(其實就是72/2),然后緩存2和36做為72對應的緩存變量,然后在緩存列表中找到36的第一個因子(因為之前已經計算過),也是2,然后看看36剩余因子的乘積是18有找到了18的第一個因子9……就這樣利用了原來的結果,把取模和除法變成了查(緩存)表,這樣無疑有個弊端,我們不能從中間開始算了,必須從2開始計算,先不管了,看看速度再說,從2計算到5000000花費了13秒左右,注意這里計算次數跟之前不一樣,之前的算法是算到5000000,但可以超過5000000,而現(xiàn)在是只算到5000000,不管怎樣,速度比并行版本的算法二還要快一些(前提是從2開始計算),緩存的效果不錯哦,不過內存也被吃去大片,空間換時間的代價……

算法二SP2:本來以上就是我要記錄的全部內容,但由于遲遲未成文,所以最近我又發(fā)現(xiàn)一個“超級”快速的空間換時間的算法,比SP1快很多,請允許我先賣一個關子,先公布結果,從2計算到5000000只需要2.8秒,具體實現(xiàn)且聽下回分解……(無恥的淫笑)

后記:鄭重聲明,本貼純屬娛樂帖,很多代碼并沒有在細節(jié)性能上過多糾纏(比如Math.Sqrt的性能還有提升空間,List申請空間處的性能提升等等……),另外賣關子不是吊胃口,而是要說明這個SP2算法所花費的篇幅可能比較長,實在寫不動了,就賣了一個關子,小弟會盡快完成下一篇,敬請期待!不過通過把這篇文章寫出來(很多代碼多年前就寫好了)還是感觸頗深的,我們面對一個問題,可以從多個角度去分析優(yōu)化解決,可以從根本上想辦法(算法本身),也可以找工具(并行),還可以在外圍和結構等方面想辦法(緩存……),只要我們不停的思考,從多個角度想辦法,總會有些手段給我們利用。最后,不知大家是不是也研究過此類問題,或許您有更好的算法,無論是出于興趣還是其它,歡迎一起討論and拍磚。

    相關評論

    閱讀本文后您有什么感想? 已有人給出評價!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過難過
    • 5 囧
    • 3 圍觀圍觀
    • 2 無聊無聊

    熱門評論

    最新評論

    發(fā)表評論 查看所有評論(1)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字數: 0/500 (您的評論需要經過審核才能顯示)