1. 程式人生 > >程式設計師程式設計藝術-----第六章-----求解500萬以內的親和數(素數、完數)

程式設計師程式設計藝術-----第六章-----求解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是每個整數的因子,把出去整數本身之外的所有因子叫做這個數的“真因子”。如果兩個整數,其中每一個真因子的和都恰好等於另一個數,那麼這兩個數,就構成一對“親和數”(有關親和數的更多討論,可參考這:http://t.cn/hesH09)。

求解:
    瞭解了什麼是親和數,接下來咱們一步一步來解決上面提出的問題(以下內容大部引自水的原話,同時水哥有一句原話,“在你真正弄弄懂這個範例之前,你不配說你懂資料結構和演算法”)。

  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 小時. )。如果是基於這個基數在優化,你無法在一天內得到結果的。
  2. 一個不錯的演算法應該在半小時之內搞定這個問題,當然這樣的演算法有很多。節約時間的做法是可以生成伴隨陣列,也就是空間換時間,但是那樣,空間代價太大,因為資料規模龐大。
  3. 在稍後的演算法中,依然使用的伴隨陣列,只不過,因為題目的特殊性,只是它方便和巧妙地利用了下標作為伴隨陣列,來節約時間。同時,將回溯的思想換成遞推的思想(預處理陣列的時間複雜度為logN(調和級數)*N,掃描陣列的時間複雜度為線性O(N)。所以,總的時間複雜度為O(N*logN+N)(其中logN為調和級數)  )。


第二節、伴隨陣列線性遍歷
依據上文中的第3點思路,編寫如下程式碼:

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++)      {    
        
//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]);  
        }  
    }  
    
return0;  
}  

第三節、程式的構造與解釋
    我再來具體解釋下上述程式的原理,ok,舉個例子,假設是求10以內的親和數,求解步驟如下:

因為所有數的真因數都包含1,所以,先在各個數的下方全部置1

  1. 然後取i=2,3,4,5(i<=10/2),j依次對應的位置為j=(4、6、8、10),(6、9),(8),(10)各數所對應的位置。
  2. 依據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
  3. 然後一次遍歷i從220開始到5000000,i每遍歷一個數後,
    將i對應的數下面的各個真因子加起來得到一個和sum[i],如果這個和sum[i]==某個i’,且sum[i‘]=i,
    那麼這兩個數i和i’,即為一對親和數。
  4. 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...
    ......
  5. i=220時,sum[220]=284,i=284時,sum[284]=220;即sum[220]=sum[sum[284]]=284,
    得出220與284是一對親和數。所以,最終輸出220、284,...