转自自己的关于落谷计数器【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掉了一道紫题。 这样一来,我做一道黄题的时间,其实也相当于花在了一道紫题上了(仍然不能改变蒟蒻的现实)

posted @ 2019-12-20 10:27  explorerxx  阅读(550)  评论(1编辑  收藏  举报