作者:上善若水、July、yansha。
出處:http://blog.csdn.net/v_JULY_v 。
前奏
本章陸續(xù)開始,除了繼續(xù)保持原有的字符串、數(shù)組等面試題之外,會有意識的間斷性節(jié)選一些有關(guān)數(shù)字趣味小而巧的面試題目,重在突出思路的“巧”,和“妙”。本章親和數(shù)問題之關(guān)鍵字,“500萬”,“線性復(fù)雜度”。
第一節(jié)、親和數(shù)問題
題目描述:
求500萬以內(nèi)的所有親和數(shù)
如果兩個數(shù)a和b,a的所有真因數(shù)之和等于b,b的所有真因數(shù)之和等于a,則稱a,b是一對親和數(shù)。
例如220和284,1184和1210,2620和2924。
分析:
首先得明確到底是什么是親和數(shù)?
親和數(shù)問題最早是由畢達哥拉斯學(xué)派發(fā)現(xiàn)和研究的。他們在研究數(shù)字的規(guī)律的時候發(fā)現(xiàn)有以下性質(zhì)特點的兩個數(shù):
220的真因子是:1、2、4、5、10、11、20、22、44、55、110;
284的真因子是:1、2、4、71、142。
而這兩個數(shù)恰恰等于對方的真因子各自加起來的和(sum[i]表示數(shù)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是每個整數(shù)的因子,把出去整數(shù)本身之外的所有因子叫做這個數(shù)的“真因子”。如果兩個整數(shù),其中每一個真因子的和都恰好等于另一個數(shù),那么這兩個數(shù),就構(gòu)成一對“親和數(shù)”(有關(guān)親和數(shù)的更多討論,可參考這:http://t.cn/hesH09)。
求解:
了解了什么是親和數(shù),接下來咱們一步一步來解決上面提出的問題(以下內(nèi)容大部引自水的原話,同時水哥有一句原話,“在你真正弄弄懂這個范例之前,你不配說你懂?dāng)?shù)據(jù)結(jié)構(gòu)和算法”)。
- 看到這個問題后,第一想法是什么?模擬搜索+剪枝?回溯?時間復(fù)雜度有多大?其中bn為an的偽親和數(shù),即bn是an的真因數(shù)之和大約是多少?至少是10^13(@iicup:N^1.5 對于5*10^6 , 次數(shù)大致 10^10 而不是 10^13.)的數(shù)量級的。那么對于每秒千萬次運算的計算機來說,大概在1000多天也就是3年內(nèi)就可以搞定了(iicup的計算: 10^13 / 10^7 =1000000(秒) 大約 278 小時. )。如果是基于這個基數(shù)在優(yōu)化,你無法在一天內(nèi)得到結(jié)果的。
- 一個不錯的算法應(yīng)該在半小時之內(nèi)搞定這個問題,當(dāng)然這樣的算法有很多。節(jié)約時間的做法是可以生成伴隨數(shù)組,也就是空間換時間,但是那樣,空間代價太大,因為數(shù)據(jù)規(guī)模龐大。
- 在稍后的算法中,依然使用的伴隨數(shù)組,只不過,因為題目的特殊性,只是它方便和巧妙地利用了下標(biāo)作為伴隨數(shù)組,來節(jié)約時間。同時,將回溯的思想換成遞推的思想(預(yù)處理數(shù)組的時間復(fù)雜度為logN(調(diào)和級數(shù))*N,掃描數(shù)組的時間復(fù)雜度為線性O(shè)(N)。所以,總的時間復(fù)雜度為O(N*logN+N)(其中l(wèi)ogN為調(diào)和級數(shù)) )。
第二節(jié)、伴隨數(shù)組線性遍歷
依據(jù)上文中的第3點思路,編寫如下代碼:
int sum[5000010]; //為防越界
int main()
{
int i, j;
for (i = 0; i <= 5000000; i++)
sum[i] = 1; //1是所有數(shù)的真因數(shù)所以全部置1
for (i = 2; i + i <= 5000000; i++)
{
//5000000以下最大的真因數(shù)是不超過它的一半的
j = i + i; //因為真因數(shù),所以不能算本身,所以從它的2倍開始
while (j <= 5000000)
{
//將所有i的倍數(shù)的位置上加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;
}
第三節(jié)、程序的構(gòu)造與解釋
我再來具體解釋下上述程序的原理,ok,舉個例子,假設(shè)是求10以內(nèi)的親和數(shù),求解步驟如下:
因為所有數(shù)的真因數(shù)都包含1,所以,先在各個數(shù)的下方全部置1
- 然后取i=2,3,4,5(i<=10/2),j依次對應(yīng)的位置為j=(4、6、8、10),(6、9),(8),(10)各數(shù)所對應(yīng)的位置。
- 依據(jù)j所找到的位置,在j所指的各個數(shù)的下面加上各個真因子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每遍歷一個數(shù)后,
將i對應(yīng)的數(shù)下面的各個真因子加起來得到一個和sum[i],如果這個和sum[i]==某個i’,且sum[i‘]=i,
那么這兩個數(shù)i和i’,即為一對親和數(shù)。 - 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是一對親和數(shù)。所以,最終輸出220、284,...