什么是计数问题
计数问题是统计一个数字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
x | 100 | 200 | 300 | 1000 | 1600 | 161a(9=>a>=0) | 1650 |
---|---|---|---|---|---|---|---|
n | 10 | 20 | 30 | 100 | 160 | 160+(a+1) | 160+10 |
通过规律我们可以发现十位数上的总数取决于最高位到百位的数字,十位和个位的数字所以规律可以总结为
(n/100)*10+min(10,max(0,n%100-10*1+1))
同样我们统计百位上的数字,这次数字太大,我就画了几个,不然会累死
统计表格后
x | 1000 | 2000 | 3000 | 10000 | 16000 | 161ab | 16500 |
---|---|---|---|---|---|---|---|
n | 100 | 200 | 300 | 1000 | 1600 | 1600+(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;
}