本代码在Code:Blocks 13.12环境下编译通过
noj第三季(枚举)的一道题,第一次写的时候没注意,超时了,就说怎么这么好写(doge)
后来开始找数学规律优化算法,写完后发现运行速度比参考代码还快,于是决定分享一下思路
先贴题
本题难就难受在数比较大时如何找到规律减少不必要的计算
怎么办呢?
注意到1-99(-1)中有20个“1”,1-999(
-1)中有300个“1”,1-9999(
-1)中有4000个“1”......以此类推,可以猜想出:由1到
-1中共有x*
个“1”(笔者未证,数归应该可证)
现在我们知道这样的一个规律,也就意味着,对于一个N来说,我们可以对他进行分解,把他从前到后,位数一位位抽出来计算,下面举例来具体说明:
假设N=6792,那么对于1到999时,由规律知,共有300个1,而同样的,从1000到6000的过程中,如果我们不关注首位,那么便发现在每个x000到(x+1)000的区间里,都有300个数,这样的区间由多少呢?6-1=5个,算上1-999,也就是6个区间,总共6*300个数。但一定不要忘记1000-1999这个特殊的区间,因为在这个区间中,1被重复了1000次。
现在我们便可得出前6000个数中的1的个数,即6*300+1000。同样的道理,我们在算6001到6700时,可以假装首位6不存在,问题也就转化成了求1到700中1的个数。 可以见得,关键就在于求1到某个y*中1的个数,不妨设其为z,而我们拥有上面的规律,便可得知,对于任意的x和y(1到9的自然数)1到y*
中1的个数为z=y*x*
+
。
到这里核心算法已经呼之欲出,我们只需对N进行分离,N=abcdefg,则我们只需先计算1到a000000中1的个数,再计算1到b00000中1的个数,再计算1到c0000中1的个数,以此类推,直到只剩下fg时,我们采取一个简单的for循环,计算fg中的1的个数。但就这么结束了吗?不对,尽管对于单一计算z时,y取了任意(1到9),但计算具体的N时,y=1的情况应该被讨论,因为y=1时, 这部分是不存在的 ,而去除1后的部分在计算时,又不得不每一遍都计算上1,也就是我们不能假装1不存在。
接着思考,既然不能用,那我们只需计算出去掉首位后剩下的数的个数再加1即可,因为这部分就相当于代替了
,而因为从0开始,所以是1+(N去除掉1后,剩下的数字)。
最后我们注意一下pow函数返回类型是double以及其他小问题,代码就写好了。
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
int N,n=1,s=0,i,x,t,p;
double y;
cin>>N;
for(x=1,t=N;1;x++){
t=t/10;
if(t==0) break;
}
for(;N>100;){
y=pow(10,x-1);
p=N/(int)y;
if(p==1) s+=(x-1)*(y/10)+1+N-y*p;//此处运用了优化,
if(p>=2) s+=p*(x-1)*(y/10)+y; //即1-99有20个1,1-999有300个1
x--; //以此类推,可得出递推规律,
N-=y*p; //最终只需计算两位数的1的个数
}
for(;n<=N;n++){
for(i=n;i>=1;){//i>=1保证了没有除超过N的位数
if(i%10==1) s++;
i/=10;
}
}
cout<<s<<endl;
return 0;
}
最后想说的:
这是笔者第一次在网上写这种文章,并且笔者只是一名普通的大一工科学生(非计算机),语文水平不咋地,部分表述和文字如有疏漏之处,难解之处,还望各位多多包涵并指出。
谢谢点击并阅读到这里!