今天看到一道非常经典的题,题目很简单,求非负数1到N之间出现1的个数。
这题用暴力的话直接爆内存,肯定不行。然后我仔细分析了一种比较认可的思路,其巧妙程度让我叹为观止。
我们认真分析一下这题,1的出现位置不外乎个位,十位,百位,千位等等,所以我们可以采取的策略就是将所有的位置上的1出现的次数分别计算,然后再相加,最后得到的就是我们要的答案。
比如随便举几个例子:
N=5,那么个位上为1有哪些数?1 共1个(M=1)
N=63,那么个位上为1有哪些数?1 11 21 31 41 51 61 共7个(M=7)
N=167,那么个位上为1有哪些数?1 11 21 31 41 ... 161 共17个(M=17)
N=1321,那么各位上为1有哪些数字? 1 11 21 31 41 ... 1321 共133个(M=133)
这里似乎就有了一点规律,有没有发现个位上为1的数似乎与该数的十位有点关系,对,就是前面为的数字+1.(如1321,前面位为132)那么暂时可以得出M=N/10+1
然后我们来看看十位的情况
N=12,那么十位上位1的数有哪些?10 11 12 共3个(M=3)
N=23,那么十位上位1的数有哪些?10 11 12 ... 19共10个(M=10)
N=126,那么十位上位1的数有哪些?10 11 12 ... 118 119 共20个(M=20)
N=302,那么十位上位1的数有哪些?10 11 12 ... 218 219 共30个(M=30)
N=1315,那么十位上位1的数有哪些?10 11 12 ... 1314 1315 共136个(M=136)
假设当前十位上的数为n,可看出:
n=1 ==> M=N/10+N%10+1; (上面第1,5两种情况,N%10表示个位数)
n!=1 ==> n>1,则每一个百位就会有一个10 11 12... 19,所以M=(N/100+1)*10; (上面第2,3种情况)
n=0,则当前百位不会有10,11,12...19,而前面的百位都会有,所以M=N/100*10 ==> (N/100+0)*10;
可以看出是否+1与n的大小有关,整合一下,那么M=(N/100+(n+8)/10)*10 ==> (N/100*10+n+8)/10*10;
然后我们继续整合,仔细观察可以发现,n=1时,M=N/10+n ==> (N/100+(n+8)/10)*10+N%10+1 == > (N/100*10+n+8)/10*10+N%10+1这样写是成立的!!!
N/100*10+N/10%10 ==> N/10
然后我们就把n用N来替换,n=N/10%10:
(N/100*10+N/10%10+n+8)/10*10 ==> (N/10+8)/10*10;
综上:M=(N/10+8)/10*10+(N/10%10==1)*(N%10+1);
到了这里,我们不妨大胆推测一下,每个位置上的数和它前面位,当前位,后面位有关。
例: N=1324 个位为1的总和为133 (这结果与前面132,当前位2,后面为4有关:132+1==133)
N=115 十位上为1的总和为16 (这结果与前面11,当前位1,后面为5有关:10+1+5==16)
那么我们得出的个位公式就可以转换一下了: M=N/10+1 ==> (N+8)/10*1+(N%10==1)*1==>(N+8)/10*1+(N%10==1)*(N%1+1)
可以明显看出公式似乎和当前所处的位置有关。那么我们大胆的推测一下:
那么设前面为a(即N/i=a,其中i表示当前的位置,默认个位为1,i*=10,即十位为10,一次类推),后面为b(即N%i=b)可以暂时得出通用方法:
M=(a+8)/10*i+(a%10==1)*(b+1)
经过验证,这种方法是正确的!在这里我从一个思考者的角度来分析,而不是从一个辨证者的角度。这里再附上源码一份
int NumberOf1Between1AndN_Solution(int n)
{
int a,b,sum=0;
for(int i=1;i<=n;i*=10){
b=n%i;
a=n/i;
sum+=(a+8)/10*i+(a%10==1)*(b+1);
}
return sum;
}
那我们这里推广一下,如果是1到N中j的出现次数(j表示0~9之间的任意正整数)
通用公式为:M=(a+9-j)/10*i+(a%10==j)*(b+1);