超级详细的计数问题的解法

什么是计数问题

计数问题是统计一个数字x,统计他从1到 x,中某一个数字在每一位中,总共出现过的次数;

例如我统计11,计算1在1-11中出现过的次数,答案就是1中1个,10有一个,11有两个,总共1-11中1这个数字总共出现了1+1+2=4次

我的话

这里采用了数学的解法和dp的解法,这两种解法 在速度上的差距并不大,没有哪个更加好的说法,两个都讲是为了有更多的道路来解题,让解题思路不会局限在一种解法之中,但是他们都有一个共同点,就是思想都是分别统计每一位中出现目标数字的次数,再把他们加起来

过程可能会有错误,还望大佬提醒。

方法一:数学方法

思路讲解

这种类型是我还在没有接触dp时候见到的一个解法,这种是通过找规律来推出公式,其实过程十分简单并且后面千篇一律,简单来说思路都懂,就是统计每一位中出现数字的次数,最后再把他们加起来,但是实现起来就有点麻烦了;

首先统计个位的,我画了张图片帮助理解,这张图是计算个位数1出现的次数

请添加图片描述

为了方便讲解我们假设x的每一位分别是abcdefg

我们发现,当所求x的个位数g==1的时候,十位到最高位的数字(abcdef)是多少,我们的答案就是abcdef+1;

假如g<1的,那最后就不会统计到abcdef1这个数字,答案就是abcdefg;

如果g>1,那么答案也是abcdef+1;

所以统计个位数的规律我们可以化为

n/10+(n%10>=1)

然后统计十位的出现1的次数

请添加图片描述

我们这里用一个表格来统计1-x的十位数上有多少个0

x10020030010001600161a(9=>a>=0)1650
n102030100160160+(a+1)160+10

通过规律我们可以发现十位数上的总数取决于最高位到百位的数字,十位和个位的数字所以规律可以总结为

(n/100)*10+min(10,max(0,n%100-10*1+1))

同样我们统计百位上的数字,这次数字太大,我就画了几个,不然会累死

请添加图片描述

统计表格后

x1000200030001000016000161ab16500
n100200300100016001600+(ab+1)1600+100

所以可以得到公式

(n/1000)*100+min(100,max(0,n%100-10*1+1))

但是有个小问题,当所求数字是0时,上面这一套就行不通了,涉及到了一个前导0的问题

前导0的处理

现在我们来分析一下为什么所求数字是0时上面行不同,这里还是拿图片来举例子

请添加图片描述

这里我们发现000-099这里的百位数的0是不能算进去的,而当情况为1时,能算进取,结果比情况0多了100

所以只要减去100即可

代码

//计算10的n次方
int power_10(int n){
    int ans=1;
    while(n--){
        ans*=10;
    }
    return ans;
}

int countTime(int n,int x){
    //统计个位数的出现x的次数
    int ans=0,num=n;
    ans+=n/10+(n%10>=x);
    num/=10;
    
    //统计其他位数出现x的次数
    int cnt=1;
    while (num){
        int p=power_10(cnt++);
        ans+=(n/(10*p))*p+min(p,max(0,(n%(10*p)-x*p+1)));
        //如果情况为0,减去相应的个数
        if(!x) ans-=p;
        num/=10;
    }
    
    return ans;
}

方法二:动态规划解法

思路讲解

dp解法的思路是将位数左右分开来计算左右位数可能的结果,在相乘起来;

比如求数字abcdefg中d位数上1出现的次数

假设我们求数字为1的情况,计算第四位的时候,即1<=xxx1yyy<=abcdefg,所有可能的情况;

(1)当000<=xxx<abc,即数字为xxx1yyy,左边可能的组合就是000-abc-1,一共abc个,右边可能是000-999,一共有1000个,这种情况加起来有abc*1000种结果;

(2)当xxx=abc时,此时数字为abc1yyy<=abcdefg

​ (2.1)当d<1,总共有0个

​ (2.2)当d==1,yyy的情况可以有000-efg,总共有efg+1个

​ (2.3)当d>1,yyy的情况可以有000-efg,总共1000个

同样还需要分析情况0时,前导0的情况

前导0的处理

我们注意到,当xxx=abc时候,不管d是不是情况0,我们都只用看后面efg的情况,即我们只用分析000<=xxx<abc情况的不同就行了

请添加图片描述

当xxx==000,时候当d=0,此时不管yyy为什么数字其实都不能把这种情况算进去;而d!=0时候是可以的

这就是需要注意的地方,所以当d==0时候,需要额外减去右边可能的组成情况,这里是000-999,1000种;

所以情况0其实是从001到abc的

代码

//获取下标l到r位置的数字
int getNum(vector<int>& nums,int l,int r){
    int ans=0;
    for(int i=l;i>=r;i--){
        ans=(ans*10)+nums[i];
    }
    return ans;
}
//返回10的n次方
int power_10(int n){
    int ans=1;
    while(n--){
        ans*=10;
    }
    return ans;
}
//计算1到n中,x出现的次数
int countTime(int n,int x){
    vector<int> nums;
    int num=n;
    //倒序存储数字
    while(num){
        nums.push_back(num%10);
        num/=10;
    }
    
    int ans=0,numLen=nums.size();
    
    //从最高位开始分析,如果x==0,需要从第二高位开始分析,因为最高位是不会出现0的情况的
    for(int i=numLen-1-!x;i>=0;i--){
        //当000<=xxx<abc
        //判断下左边是否有数字
        if(i<numLen-1){
            ans+=getNum(nums,numLen-1,i+1)*power_10(i);
            if(!x) ans-=power_10(i);
        }
        
        //当xxx==abc
        //当这位的数字小于x,结果为0,索性直接不写了
        if(nums[i]==x) ans+=getNum(nums,i-1,0)+1;
        else if(nums[i]>x) ans+=power_10(i);
    }
    
    return ans;
}
  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值