在我的另外一篇文章"正整數中數字1的計數問題(上)"中,實現了一個簡單的演算法來計算f(n)。該演算法由於
只考慮相鄰兩個數的變化規律,因此在計算單個長整數時(比如n=911111111099999009L),可
能要很長時間才能算完,顯然該演算法適用範圍比較狹小。本文繼續圍繞那道Google面試題,探討一個
計算單個值f(n)的快速演算法以及如何求出所有滿足f(i)=i這樣的整數(對很大的數計算時間能達到毫秒數量級)。
分析:
首先注意到,比如要計算i=3260這樣的f(i),在算到i=3200的時候,其實又重複了以前的計算,即重新
計算1到60的值。本演算法就是基於這個考慮,將一個數拆成左右兩半,採用分治法(divide-and-conquer)
來計算每一半的值,然後將二個值合并到一起。這與歸併排序演算法(merge sort)的思想是一樣的。
首先推導適合於分治演算法的公式:
以i=3260為例,將該數拆成左右兩半,左邊等於32,右邊等於60。可以找到這樣的等式:
f(3260) = f(3200) + [ones(32)*60 + f(60)]。
其中ones(32)表示數32中含有數字1的個數(這裡等於0),ones(32)*60表示32這個數含有數字1的個數需
要重複60次(即從3201~3260),上面方括弧中的值就是從3201到3260這總共60個數中包含數字1的總數。
很明顯,經過這一步,f(3260)轉化為只需計算很小的數f(60)和另外一個還是很大的數f(3200)。接著對
f(3200)進行類似的處理:
f(3200) = ones(3200) + f(3199)
= ones(32) + f(3199)
= ones(32) + [f(3100) + ones(31)*99 + f(99)]; ....(等式1)
f(3100) = ones(31) + f(3099)
= ones(31) + [f(3000) + ones(30)*99 + f(99)]; ....(等式2)
......
f(200) = ones(2) + f(199)
= ones(2) + [f(100) + ones(1)*99 + f(99)]; ....(等式31)
f(100) = ones(1) + f(99)
= ones(1) + [f(99)]. ....(等式32)
上面這些等式相互替換(例如用第二式的f(3100)值替換第一式右邊的f(3100),最終效果就是左右兩邊
累加,這是因為左邊除了f(3200)未被消掉外,其他的項左右抵消)得到:
f(3200) = [ones(1)+ones(2)+...+ones(32)] + [ones(1)+ones(2)+...+ones(31)]*99 + f(99)*32
= f(31)*100 + ones(32) + f(99)*32;
很漂亮的結果!經過一系列降低複雜度處理,最終f(3200)變成了只需要計算兩位元值,即f(31)和f(99)
!類似,原數值f(3260)的等式為:
f(3260) = f(31)*100 + ones(32)*61 + f(60) + f(99)*32;
= ones(32)*61 + f(60) + f(31)*100 + f(99)*32;
接著對f(31),f(60), f(90)三個數進行同樣的拆分計算(複雜度降低一半!)。
推廣到一般情況,對於一個比較大的數,比如n=1234567890, 將它拆成兩半,設左邊的值為leftNum,右邊
的值為rightNum,對應右邊數值的位元為rightCount,共有rightCount個9的數為allRightNines,比如
1234567890右邊共有5位,那麼allRightNines=99999。這樣可以得到:
f(n) = ones(leftNum)*(rightNum+1) + f(rightNum)
+ f(leftNum-1)*(allRightNines+1) + f(allRightNines)*leftNum;
由於像f(9),f(99),f(999)這樣的數的上面的等式中要
重複使用,因此在計算過程中保留這些結果,便於共用,達到以空間換時間的效果。
更新: 計算所有滿足f(i)=i的演算法也是基於分治法思想,類似於折半尋找,參考以下代碼中的solveBatch(...)。基本原理是基於這樣的事實:f(i)是一個遞增序列。如果f(i)=j,而且j>i,那麼從i到j中間的任何數k(k!=j)不可能等於f(k),因為f(k)>=f(i),也就是f(k)>=j>k。所以只需要考慮下一個數f(j)是否等於j。如果f(i)=j,而且j<i,情況類似。
以下是實現代碼:在Pentium M 1.4GHz, 512M記憶體PC上,計算f(911111111099999009) =
1648888888779991781以及計算滿足f(i)=i(i<=911111111099999009)這樣的數總共耗時0.221秒。
import java.util.ArrayList;<br />import java.util.Collections;<br />import java.util.List;<br />/**<br /> *<br /> * @author ljs<br /> * 2011-05-23<br /> *<br /> */<br />public class OnesCounter {<br />//memoization: save the all nines f(n), like f(9),f(99),f(999)....<br />private long[] allNinesFn;<br />private boolean memoization;</p><p>public OnesCounter(boolean memoization){<br />this.memoization = memoization;</p><p>if(memoization){<br />long max = Long.MAX_VALUE;</p><p>int maxDigitsCount=1;<br />while((max /= 10) >0)<br />maxDigitsCount++;<br />allNinesFn = new long[maxDigitsCount + 1];<br />}<br />}<br />/**<br /> * Problem:<br /> * Consider a function which, for a given whole number n,<br /> * returns the number of ones required when writing out all<br /> * numbers between 0 and n.<br /> * For example, f(13)=6. Notice that f(1)=1.<br /> * What is the next largest n such that f(n)=n?<br /> *<br /> * Quick Solution: using divide-and-conquer<br /> * return f(i)<br /> * n: long integer<br /> */<br />public long quickSolve(long n){<br />//the number of digits in n<br />int digitsCount=1;<br />long tmp = n;<br />while((tmp /= 10) >0)<br />digitsCount++;</p><p>//separate n into digits<br />byte[] digits = new byte[digitsCount];<br />tmp = n;<br />int p = digitsCount - 1;<br />while(tmp>0){<br />byte digit = (byte)(tmp%10);<br />digits[p--]=digit;<br />tmp/=10;<br />}</p><p>long result = quickSolve(digits);</p><p>//memoization feasibility check: all nines<br />if(memoization && allNinesFn[digitsCount]==0){<br />boolean isAllNines = true;<br />for(int i=0;i<digitsCount;i++){<br />if(digits[i]!=9) {<br />isAllNines = false;<br />break;<br />}<br />}<br />if(isAllNines){<br />//memoization<br />allNinesFn[digitsCount] = result;<br />}<br />}<br />return result;<br />}</p><p>private long quickSolve(byte[] digits){<br />int digitsCount=digits.length;</p><p>if(digitsCount == 1) {<br />if(digits[0]>=1){<br />return 1;<br />}else{<br />return 0;<br />}<br />}</p><p>int leftCount = digitsCount / 2;<br />int rightCount = digitsCount - leftCount;</p><p>int leftOnes = 0;<br />//count 1's in the left digits<br />for(int i=0;i<leftCount;i++){<br />if(digits[i]==1)<br />leftOnes++;</p><p>}</p><p>long leftNum = digits[0];<br />//using Horner's rule<br />for(int i=1;i<leftCount;i++){<br />leftNum = leftNum*10 + digits[i];<br />}</p><p>int rightP = leftCount; //the start pos for the right half<br />long rightNum = digits[rightP];<br />long allRightNines = 9;<br />for(int i=rightP+1;i<digitsCount;i++){<br />rightNum = rightNum*10 + digits[i];<br />allRightNines = allRightNines*10 + 9;<br />}</p><p>long allRightNinesResult = 0;<br />//memoization usage<br />if(memoization && allNinesFn[rightCount] > 0){<br />allRightNinesResult = allNinesFn[rightCount];<br />}else{<br />allRightNinesResult = quickSolve(allRightNines);<br />}</p><p>long onesCount = leftOnes * (rightNum+1)<br />+ quickSolve(rightNum) + quickSolve(leftNum-1)* (allRightNines+1)<br />+ allRightNinesResult * leftNum;<br />return onesCount;<br />}</p><p>public static List<Long> foundList = new ArrayList<Long>();<br />public static void solveBatch(OnesCounter qc,long m, long n){<br />if(n<0 || n<m) return;</p><p>long right = qc.quickSolve(n);</p><p>if(n==m){<br />if(right==n){<br />foundList.add(n);<br />//System.out.format("f(%d) = %d%n",n,right);<br />}<br />return;<br />}</p><p>if(right<n){<br />solveBatch(qc,m,right);<br />}else if(right==n){<br />foundList.add(n);<br />//System.out.format("f(%d) = %d%n",n,right);<br />solveBatch(qc,m,right-1);<br />}else if(right>n){<br />long midNum = (n+m)/2;<br />long mid = qc.quickSolve(midNum);<br />if(mid<midNum){<br />solveBatch(qc,m,mid);<br />solveBatch(qc,midNum+1,n-1);<br />}else if(mid==midNum){<br />foundList.add(midNum);<br />//System.out.format("f(%d) = %d%n",midNum,mid);<br />solveBatch(qc,m,midNum-1);<br />solveBatch(qc,midNum+1,n-1);<br />}else{<br />solveBatch(qc,m,midNum-1);<br />solveBatch(qc,mid,n-1);<br />}<br />}</p><p>}</p><p>public static void main(String[] args) {<br />//long n=1111111110;<br />long n=911111111099999009L;</p><p>OnesCounter qc1 = new OnesCounter(true);</p><p>long start = System.currentTimeMillis();</p><p>System.out.format("Test 1: caculate F(%d)%n",n);</p><p>long ones = qc1.quickSolve(n);<br />System.out.format("f(%d) = %d%n",n,ones);</p><p>System.out.println();<br />System.out.format("**********************%n");<br />System.out.println();</p><p>System.out.format("Test 2: caculate F(n)=n%n");<br />OnesCounter qc2 = new OnesCounter(true);<br />OnesCounter.solveBatch(qc2, 0, n);<br />System.out.format("Total numbers satisfying f(n)=n (n<%d): %d%n",n,foundList.size());<br />Collections.sort(foundList);<br />for(long k:foundList){<br />System.out.format("%d%n",k);<br />}</p><p>long end = System.currentTimeMillis();</p><p>double timeElapsed = (end-start)/1000.0;<br />System.out.format("Time elapsed(sec): %.3f%n", timeElapsed);</p><p>}<br />}<br />
測試結果:
Test 1: caculate F(911111111099999009)
f(911111111099999009) = 1648888888779991781
**********************
Test 2: caculate F(n)=n
Total numbers satisfying f(n)=n (n<911111111099999009): 84
0
1
199981
199982
199983
199984
199985
199986
199987
199988
199989
199990
200000
200001
1599981
1599982
1599983
1599984
1599985
1599986
1599987
1599988
1599989
1599990
2600000
2600001
13199998
35000000
35000001
35199981
35199982
35199983
35199984
35199985
35199986
35199987
35199988
35199989
35199990
35200000
35200001
117463825
500000000
500000001
500199981
500199982
500199983
500199984
500199985
500199986
500199987
500199988
500199989
500199990
500200000
500200001
501599981
501599982
501599983
501599984
501599985
501599986
501599987
501599988
501599989
501599990
502600000
502600001
513199998
535000000
535000001
535199981
535199982
535199983
535199984
535199985
535199986
535199987
535199988
535199989
535199990
535200000
535200001
1111111110
Time elapsed(sec): 0.220