正整数中数字1的计数问题 - 采用分治法快速计算f(n)(下)
在我的另外一篇文章"正整数中数字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)&
gt;=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; import java.util.Collections; import java.util.List; /** * * @author ljs * 2011-05-23 * */ public class OnesCounter { //memoization: save the all nines f(n), like f(9),f(99),f(999).... private long[] allNinesFn; private boolean memoization; public OnesCounter(boolean memoization){ this.memoization = memoization; if(memoization){ long max = Long.MAX_VALUE; int maxDigitsCount=1; while((max /= 10) >0) maxDigitsCount++; allNinesFn = new long[maxDigitsCount + 1]; } } /** * Problem: * Consider a function which, for a given whole number n, * returns the number of ones required when writing out all * numbers between 0 and n. * For example, f(13)=6. Notice that f(1)=1. * What is the next largest n such that f(n)=n? * * Quick Solution: using divide-and-conquer * return f(i) * n: long integer */ public long quickSolve(long n){ //the number of digits in n int digitsCount=1; long tmp = n; while((tmp /= 10) >0) digitsCount++; //separate n into digits byte[] digits = new byte[digitsCount]; tmp = n; int p = digitsCount - 1; while(tmp>0){ byte digit = (byte)(tmp%10); digits[p--]=digit; tmp/=10; } long result = quickSolve(digits); //memoization feasibility check: all nines if(memoization && allNinesFn[digitsCount]==0){ boolean isAllNines = true; for(int i=0;i<digitsCount;i++){ if(digits[i]!=9) { isAllNines = false; break; } } if(isAllNines){ //memoization allNinesFn[digitsCount] = result; } } return result; } private long quickSolve(byte[] digits){ int digitsCount=digits.length; if(digitsCount == 1) { if(digits[0]>=1){ return 1; }else{ return 0; } } int leftCount = digitsCount / 2; int rightCount = digitsCount - leftCount; int leftOnes = 0; //count 1's in the left digits for(int i=0;i<leftCount;i++){ if(digits[i]==1) leftOnes++; } long leftNum = digits[0]; //using Horner's rule for(int i=1;i<leftCount;i++){ leftNum = leftNum*10 + digits[i]; } int rightP = leftCount; //the start pos for the right half long rightNum = digits[rightP]; long allRightNines = 9; for(int i=rightP+1;i<digitsCount;i++){ rightNum = rightNum*10 + digits[i]; allRightNines = allRightNines*10 + 9; } long allRightNinesResult = 0; //memoization usage if(memoization && allNinesFn[rightCount] > 0){ allRightNinesResult = allNinesFn[rightCount]; }else{ allRightNinesResult = quickSolve(allRightNines); } long onesCount = leftOnes * (rightNum+1) + quickSolve(rightNum) + quickSolve(leftNum-1)* (allRightNines+1) + allRightNinesResult * leftNum; return onesCount; } public static List<Long> foundList = new ArrayList<Long>(); public static void solveBatch(OnesCounter qc,long m, long n){ if(n<0 || n<m) return; long right = qc.quickSolve(n); if(n==m){ if(right==n){ foundList.add(n); //System.out.format("f(%d) = %d%n",n,right); } return; } if(right<n){ solveBatch(qc,m,right); }else if(right==n){ foundList.add(n); //System.out.format("f(%d) = %d%n",n,right); solveBatch(qc,m,right-1); }else if(right>n){ long midNum = (n+m)/2; long mid = qc.quickSolve(midNum); if(mid<midNum){ solveBatch(qc,m,mid); solveBatch(qc,midNum+1,n-1); }else if(mid==midNum){ foundList.add(midNum); //System.out.format("f(%d) = %d%n",midNum,mid); solveBatch(qc,m,midNum-1); solveBatch(qc,midNum+1,n-1); }else{ solveBatch(qc,m,midNum-1); solveBatch(qc,mid,n-1); } } } public static void main(String[] args) { //long n=1111111110; long n=911111111099999009L; OnesCounter qc1 = new OnesCounter(true); long start = System.currentTimeMillis(); System.out.format("Test 1: caculate F(%d)%n",n); long ones = qc1.quickSolve(n); System.out.format("f(%d) = %d%n",n,ones); System.out.println(); System.out.format("**********************%n"); System.out.println(); System.out.format("Test 2: caculate F(n)=n%n"); OnesCounter qc2 = new OnesCounter(true); OnesCounter.solveBatch(qc2, 0, n); System.out.format("Total numbers satisfying f(n)=n (n<%d): %d%n",n,foundList.size()); Collections.sort(foundList); for(long k:foundList){ System.out.format("%d%n",k); } long end = System.currentTimeMillis(); double timeElapsed = (end-start)/1000.0; System.out.format("Time elapsed(sec): %.3f%n", timeElapsed); } }
测试结果:
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