转自自己的关于落谷计数器【p1239】的题解
本蒟蒻写这道题用了两天半里大概五六个小时。(我太弱了)
然后这篇题解将写写我经历的沟沟坎坎,详细的分析一下,
但是由于它很长,因此一定还有多余的地方,比如说我的
预处理,可能比较多余。但是我觉得,信息学需要耐心!
不管是写这道题还是写这个题解,我都花了很长时间。
我认为写一道题,最好是自己完全写出来,所以我才自己琢磨了很久,虽然仍然很多不美满,但我可以骄傲的说这是我自己的成果(蒟蒻蜜汁自满)。 所以如果想凭自己做出来,不应该害怕时间的问题(当然比赛是需要效率的)。如果想从题解吸取经验,也应该看得仔细才有用。
因为我不会什么数位dp,看到算法标签里只有递推,于是我来(自信的)挑战了,但我这次终于是自己研究了一道黄题,还是小有成就感的(蒟蒻蜜汁成就感)。
然后我看题了,在我的印象里,我研究过譬如100~1000内有多少3这样的问题,所以我感觉这题应该不差多少,于是我一开始按照每个数量级的区间有多少个数字1~9来写,于是自然的错了。
然后我想到这应该从1开始记录到某一个数量级(个,十,百,千对应1,2,3,4级),数字0~9有多少个,我一开始只是隐约觉得0和1~9是不一样的,所以我就想先算出来这个再想后面怎么做吧。
首先既然是递推的话,肯定要有边界,因此数量级为1的时候,很显然1~9肯定是只出现了一次。于是我开了一个二维数组,f[i][j]其中i表示数字i,j表示数量级。
然后我用一下代码来给边界赋值。
f[0][0]=0;
f[0][1]=1;
for(int i=1;i<=9;i++)
{
f[i][1]=1;
f[i][0]=0;
}//十以内每个数的数量
所以对于后面的每一个数量级,比如1~99,如果把1~9的十位看作0的话,那么可以看到从0~9作十位,每一个数一定会在作为个位出现十次,可以认为是十位的数字控制了个位上数字出现的次数。而如果十位上是某个数的话,比如1作十位,那么10~19,1不仅会作为个位出现1次,也会作为十位出现一次,因此要加上10。这样各个数字会在1~99中出现10+10=20次,那么如果是1~999,在十位上的每个数字会出现多少次? 那就是f[i][2]*10+100;也就是说
f[i][j]=f[i][j-1]*10+10^(j-1)。 为了将这个10^(j-1)方便的运算,我用o[10]来储存, 将o[1]=1;o[2]=10;
这样,f[i][j]=f[i][j-1]*10+o[j];
如此,对于每一个数量级内1~9出现了多少次,就可以用如下代码运算。
o[1]=1;
for(int i=2;i<=10;i++)
{
o[i]=o[i-1]*10;
}//o[i]用来表示在数量级i中,出现了某一个n,要多叠加o[i]个,比如在10^2的数量级中,即1~99中,对于每一个数,都有它作十位的时候,那么除了每十个数的个位它会出现一次,它会作为十位多出现10次,为了叠加方便,o[2]=10,就可以直接叠加上去了。
for(int i=1;i<10;i++)
{
for(int j=1;j<=9;j++)
{
f[j][i]=f[j][i-1]*10+o[i];
}
}//计算1~9的每个数在某一个数量级中的个数
接下来,考虑一下0; 0特殊在一个地方,那就是每一次最高位是不会出现0的。 因为我做到这里的时候比较懒,用了一个打表找规律。
#include<bits/stdc++.h>
using namespace std;
int a,b[10]={};
int main()//打表器
{
int n;
cin>>n;
int h[10]={};
for(int i=1;i<=n;i++)
{
int m=i;
while(m>0)
{
int v;
v=m%10;
for(int j=0;j<=9;j++)
{
if(v==j) h[j]++;
}
m=m/10;
}
}
for(int i=1;i<=10;i++)
{
cout<<h[0]<<endl;
}
return 0;
}
发现了0的递推式
f[0][i]=f[0][i-1]+(i-1) 9 o[i-1];
到了真正使用的时候我才又更深入的思考。 所以说如果这道题只是问某一数量级的0出现次数,其实打表找规律就好。
那么到这时候,我相当于进行了一个预处理。
下一步就是具体的分析了。
对于一个数12345,找到1~12345中各数字出现多少次。 我首先想的是吧10000以前的直接用刚刚的f[i][4]添加给ans[i],然后处理后面的2345。但是这里的调用很麻烦,于是我想到了倒着来处理,从5开始,看看5对答案的贡献,发现它仅仅贡献了0~5这几个数字,每个多一次。那么4呢,贡献给了所有数字f[i][1]*4个结果,(1~40中每个数字先在个位上有一个),对于1~3,它们还作10位,各多出现10次。 所以我想到了,对于1~9的一个处理方式。
while(u>0)
{
u=u/10;
c++;
}
int l=0,z;//z用来表示当前位数上的数字是几
int r[12]={};//r用来补齐某一位上的数出现的次数要加上r。
while(k>0)//从最后一位开始数,出现了多少次某一个数字
{
l++;//表示这是倒数第几位,也相当于多少的数量级,比如l=1时,表示这是个位
z=k%10;//用z提取数字的最后一位
for(int i=1;i<=9;i++)//先判1~9的数
{
ans[i]=ans[i]+f[i][l-1]*z;
if(i<z)
{
ans[i]=ans[i]+o[l]; //如果说在这一位上的数大于i,说明有o[l]个i要作为l位来记录,比如235,210~219中,1会作为十位出现十次,那么就多记录上o[l]个1;
}
if(i==z)
{
ans[i]=ans[i]+1+r[l];//比如235,在记录3时,不仅要记录200~229,230的3也算一次,后面的5算5次,总共是1+r次
}
}
k=k/10;
r[l+1]=r[l]+z*o[l];//这里可以看到 比如 235,对于3 来说 200~230中可以直接用上面算出来,但是后面的231~235,需要加上5,这里5就用r来记录.
}
我的想法都体现在了注释里面。为了不让自己迷糊,我就边写边注释,我觉得这不失是一种在做比较复杂的题(对我这种蒟蒻来说)的时候的一种好方法。
最后这个0,令我“深恶痛绝”(还是我太弱了) 我一开始以为0也可以这样推,但是0太特殊,它在第一位肯定不会有,而在最后一位上又会受前面所有的数控制。
等到真正思考0的答案时,我才想到了“控制出现”的思路。
比如说110,个位上的0出现的次数,受什么控制?百位上的1,控制了0一定会在10~90的个位一共出现9次,十位上的1,则只控制0在100的个位上会出现一次。那么个位上的0总共出现了9+1 =10 次。
十位上的0呢?
在100~109上出现了十次。 于是我就认为,个位上的0受到前面所有数的控制,而十位上的0受到了自己的控制,因为十位上是1,所以个位十位是0的情况一定出现了,这里和1~9的思想一样 其实还是如果这一位上的数字大于0,0作为这一位的情况已经出现了,那么这时候可以认为这一位上的0受前面更高位的控制。
经过几个数的分析,我做出了一下总结。 对于个位上的0,在数量级十位以上,那么它受控制,十位会控制它有几个,百位控制有几十个,例如320,3控制了它有10~99,100~199,200~299的个位上分别有10个零,310-1,因为单个零不计,所以减一就好。2则控制了他从300~320上有3个0。个位本身不控制自己。对于十位来说,百位控制了它的有几十个数,也就是说3,控制了它有100~109,200~209,十位上均有十个0,因为在0~99内十位上没有0,所以就是310-10,这时候,它本身就会控制自己了,因为从300~309有多少个十位上的零,取决于十位上的数,当它>=1时,肯定有300~309的十个,如果为零,则受后边数的控制 ,比如说308,那么十位上出现的次数就加上(8+1),即九次。对于最高位三,他肯定不会出现0;
所以说,对于一个四位数abcd来说,a上没有0,d上的零有abc-1个,c上的0有ab0个(c>=1)或者ab0-10+d个(c=0),对于b来说,b上的0有a00个(b>=1)或者cd个.
所以我就做了开了两个数组,一个t[i]表示i位上的0有多少个,一个r[i]表示i位以后的数字是多少。
int t[10]={},s;
s=n;
for(int i=c;i>=1;i--)
{
if(i==c)
{
t[i]=0;
}
if(i<c&&i!=1)
{
if(s/o[i]>=1)
{
t[i]=(n/o[i+1])*o[i];
}
if(s/o[i]==0)
{
t[i]=(n/o[i+1])*o[i]-o[i]+r[i]+1;
}
}
if(i==1)
{
t[i]=n/o[i+1];
}
s=s%o[i];
}//读取每一位上的数字,并且判断其贡献
for(int i=1;i<=c;i++)
{
ans[0]=ans[0]+t[i];
}
到了这里,整个题基本结束了。中间不管是思路还是细节的处理,(因为我很弱)都花了不少时间,但我觉得这是值得的,是一次锻炼(因为我很弱)。
下面是我AC代码。
#include<bits/stdc++.h>
using namespace std;
int f[10][10];//表示0~9 十个数在每个数量级里的数量
int ans[10]={};
int main()
{
int o[11]={};
o[1]=1;
for(int i=2;i<=10;i++)
{
o[i]=o[i-1]*10;
}
f[0][0]=0;
f[0][1]=1;
for(int i=1;i<=9;i++)
{
f[i][1]=1;
f[i][0]=0;
}//十以内每个数的数量
f[0][2]=9;
for(int i=1;i<10;i++)
{
for(int j=1;j<=9;j++)
{
f[j][i]=f[j][i-1]*10+o[i];
}
}//计算1~9的每个数在某一个数量级中的个数
f[0][2]=9;
for(int i=3;i<10;i++)
{
f[0][i]=f[0][i-1]+(i-1)*9*o[i-1];
} //计算0在每个数量级里的数量
int n;
int k;
cin>>n;
k=n;
int u,c=0;
u=n;
while(u>0)
{
u=u/10;
c++;
}
int l=0,z;
int r[12]={};//r用来补齐某一位上的数出现的次数要加上r。
while(k>0)//从最后一位开始数,出现了多少次某一个数字
{
l++;//表示这是倒数第几位,也相当于多少的数量级,比如l=1时,表示这是个位
z=k%10;//用z提取数字的最后一位
for(int i=1;i<=9;i++)//先判1~9的数
{
ans[i]=ans[i]+f[i][l-1]*z;
if(i<z)
{
ans[i]=ans[i]+o[l];
}
if(i==z)
{
ans[i]=ans[i]+1+r[l];
}
}
k=k/10;
r[l+1]=r[l]+z*o[l];
}
int t[10]={},s;
s=n;
for(int i=c;i>=1;i--)
{
if(i==c)
{
t[i]=0;
}
if(i<c&&i!=1)
{
if(s/o[i]>=1)
{
t[i]=(n/o[i+1])*o[i];
}
if(s/o[i]==0)
{
t[i]=(n/o[i+1])*o[i]-o[i]+r[i]+1;
}
}
if(i==1)
{
t[i]=n/o[i+1];
}
s=s%o[i];
}//读取每一位上的数字
for(int i=1;i<=c;i++)
{
ans[0]=ans[0]+t[i];
}
if(c<=2)//数据小的话就直接枚举
{
int h[10]={};
for(int i=1;i<=n;i++)
{
int m=i;
while(m>0)
{
int v;
v=m%10;
for(int j=0;j<=9;j++)
{
if(v==j) h[j]++;
}
m=m/10;
}
}
for(int i=0;i<=9;i++)
{
cout<<h[i]<<endl;
}
}
if(c>=3)
{
for(int i=0;i<=9;i++)
{
cout<<ans[i]<<endl;
}
}
return 0;
}
码风较乱,算法很菜。 但是有一点,用这个程序去做p2602(紫题)(2021.1.21考古惊奇发现题号打错以及题目降蓝色),它问的是区间[a,b]里每个数字出现多少次,所以我就用b中每个数字出现的次数减去a-1中每个数出现的次数,A掉了一道紫题。 这样一来,我做一道黄题的时间,其实也相当于花在了一道紫题上了(仍然不能改变蒟蒻的现实)