各位數之和


給定一個十進制正整數 ,從1開始到的所有整數,計算每個數各個位的數字總和。

例如:
N=4,則1+2+3+4=10。
N=14,則1+2+3+4+5+6+7+8+9+(1+0)+(1+1)+(1+2)+(1+3)+(1+4)=60。

int sum_digital(int N)

{

    int sum = 0;

    for (int i = 1; i <= N; i ++) {

       for (int j = i; j; j /= 10)

           sum += j%10;

    }

    return sum;

}

代碼清單1

這樣暴力的方法讓人提不起任何興趣,我們來點有挑戰性的,假設N和sum都不會超過int整型數值范圍,我們提出兩個擴展問題:

  1. 設計一個時間復雜度更低,更有效的算法來解決這個問題。
  2. 假如給定sum的值,能否計算出N的大小?

問題一

解法一

代碼清單1的方法很簡單,一個編程的初學者估計都能寫出那樣的代碼來,我們來分析下它的時間復雜度,N次的循環再乘上每個數的位數為Ο(N*logN ),這樣的計算時間是跟N的大小呈線性增長的。

是否一定需要從1到N循環一遍后再計算呢?如果我們要得到更高效的算法,肯定要拋棄這樣遍歷1到N的方法。

考慮直接統計每個數各個位上的數字出現的次數。假設N=123,設Xi為N的第i位的數字,我們分析下十位上的數字Xi的各種情況。

1.如果X1<2時的情況:

當X1=0時,由於0對總和沒有任何貢獻,所以可以不考慮;當X1=1時,{0~1} 1 {0~9},左邊可選集大小為2,右邊可選集大小為10(因為X1=1,所以右邊任意數字,組合后都不會超過123),那十位數上為1時,組合總數為2*10=20,總和為1*20=20。這種情況的公式為

2.如果X1=2時的情況:

{0} 2 {0~9},如果左邊的可選集都小於1,則右邊可選集大小一定為10;{1} 2 {0~3},如果左邊的數字為1,那右邊的可選集只能是0到3的數字,所以十位數上為2時,組合總數為1*10+1*4=14,總和為2*14=28。這種情況的公式為

3.如果X1>2時的情況:

當時X1=3,{0} 3 {0~9},左邊可選集大小只有1,右邊可選集大小為10。

 

對於每個位上的0~9都計算一次它的組合總數,即左邊組合數乘上右邊組合數,最低位和最高位時不用特殊處理,反正左或右的組合數總有一個是為0的。我們把它歸納成公式,則


這樣得到的算法也很簡單。

int sum_digital_3(int N)

{

    int sum = 0;

    int tmp = N;

    for (int i = 0; tmp; tmp /=10, i ++) {

       int X = tmp%10;

       int left = tmp / 10;

       int right = N % pow(10, i);

       for (int j = 1; j <= 9; j ++)

           sum += j * left * pow(10, i);

       for (int j = 1; j < X; j ++)

           sum += j * pow(10, i);

       sum += X * (right + 1);

    }

    return sum;

}

代碼清單3

這個算法只是簡單的組合數學知識,比起解法一的時間復雜度也只不過多了個常系數而已。從對每個位每數字出發去考慮組合總數,這樣只考慮包含自己的組合即可,不需要擔心是否有數字被忽略或重復計算。

 

問題一

解法二

我們拿N=14的例子來分析下,1+2+3+4+5+6+7+8+9+(1)+(1+1)+(1+2)+(1+3)+(1+4),我們可以重新排列下,(1+2+3+4+5+6+7+8+9)+(1+1+1+1+1)+(1+2+3+4),啊哈!是否覺得有點靈感了,我們可以用乘法去重新排列,這次我們拿N=20做例子,2*(1+2+3+4+5+6+7+8+9)+10*1+(2)。這個乘法的公式跟代碼1的區別就出來了,在代碼1中全部都是加法。

 

圖1

簡單總結下發現,計算兩位數時,以個位數總和作為一個單位,假設有個兩位數整數為X*10,則有公式。再來算算三位數X*100的情況,我們先把抽象為更廣義的一個函數F(x),它表示1~(10x-1)各位數的總和,則對公式就是,歸納一下,我們針對X*10n (0≤X≤9)此類的數據就有了通項公式


。再來看看函數F(x),明顯得,我們能很輕松的得出它的遞推公式


,聰明的你一定看出了,其實F(x)就是X*10n通項公式當X=10的時候的特例。呼,世界大同了~

一切似乎都顯得很順利,但是我們的問題是當時的算法。相信你應該已經有想法了。

參考的通項公式,直接代入進去有問題嗎?回答是當然有問題,問題在於還有一些數字沒有被計算到。

圖2

圖2是N=121的圖例,我們用X*10n的通項公式來分析。淺色區00~99和0~9由Xn*F(n)計算得到,深色區由計算所得,但剩下的無色區並不只有Xn這么簡單,比如十位的2,它可以是20,21,所以這里應該把Xn改成Xn*(N%10n+1)。代碼清單2就是這個算法。

int sum_digital_2(int N)

{

    int F[11] = {0};

    for (int i = 1; i < 10; i ++)

       F[i] = 10 * F[i-1] + 45 * pow(10, i-1);

   

    int sum = 0;

    int tmp = N;

    for (int i = 0; tmp; tmp /= 10, i ++) {

       int X = tmp%10;

       if (X == 0)

           continue;

       sum += X * F[i];

       sum += (X-1) * X / 2 * pow(10,i);

       sum += X * (N%pow(10,i) + 1);

    }

    return sum;

}

代碼清單2

在代碼中,F(x)被預先計算出來,當然使用pow求10的冪次也可以先預處理,這里只是為了做范例。這個代碼中的循環次數只和的位數有關,它的時間復雜度為O(logN),這樣的復雜度,即使N超出了32位整數的數值范圍,也能迅速計算完成,但那時你得注意sum的溢出問題。

雖然這個高效算法實現了,但我們仍然有疑惑,比如,為何把Xn改成Xn*(N%10n+1)代入后,各個位計算求和就是正確答案了?這個過程似乎太自然,我們回頭來驗證下。

我先整理下我們的思考過程,這里體現了分治思想。將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。問題的規模越小,越容易直接求解,就如遞推函數F(x),以它為基礎再解決這樣X*10n特殊的子問題。

從下而上分析,因為我們發現代碼清單1的計算過程中存在大量的重復,如,周期性不停地循環着,而第n位的數字就是第n-1位的周期數,由0~9的循環我們能得出00~99的循環,進而得到000~999的循環。

從上而下分析,考慮可以把高位的數字統計完,再計算低位的數字,按位分段計算,我們肯定能得到形如類似這樣的公式。

 

問題二

解法一

問題二是問題一的逆命題,對於逆命題,我們可以按着原命題的算法逐個計算再測試計算結果和給定sum的值是否相同,這樣的算法只要一個for循環不停的枚舉N,並調用sum_digital_2函數,當計算的值超過sum時就可以停止了,因為不可能有比正確答案更大的N而它的sum卻小於或等於給定的sum,簡單的說,sum是隨着N增大而增大的,它是一個單調遞增函數。

枚舉法是沒有效率可言的,乘上sum_digital_2復雜度,它的時間復雜度為O(N logN),為了得到更高效的算法,我們使用二分法來替換枚舉法,知道二分查找的同學肯定不會陌生。

之所以二分法在這里能派上用場,就是因為sum值有單調遞增的特點。設置left和right變量,每次取它們的中間值mid來測試,如果小了,按照sum值單調性我們可知肯定大於mid,則left往mid+1移動,否則right往mid移動。應用二分法后的時間復雜度為O(logN logN)。

int find_n(int sum)

{

    int l = 0;

    int r = sum;

    while (l < r) {

       int mid = (l+r) >> 1;

       int tmp = sum_digital_2(mid);

       if (tmp < sum)

           l = mid + 1;

       else

           r = mid;

    }

    return l;

}

代碼清單4

這里還需要注意的是溢出問題,代碼清單4中直接把sum當作right的值,這很容易使得sum_digital_2(mid)計算的結果溢出,有個簡單的解決辦法,可以拿sum和代碼清單2中的F[i]數組去比較,就可以知道N是屬於哪個X*10n范圍之內,再拿X*10n作為right的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM