方法一:
用dp[i]表示从000....0到999....9的i位数(例如,i=3,就是从000,001,到999)中,一个数出现的次数(由于0到9这十个数字地位等价,他们出现的次数是一样的)
则dp[i]=dp[i-1]*10+10的[i-1]次方,这是由分类讨论得到:
例如 999 ,
先看后两位 当最高位为0-9(共10个数)的时候,后两位从00到99,出现了子结构,所以出现的次数是10*dp[i-1]
再来看当最高位等于需要统计个数的数的时候,例如,需要统计的是9,则900-999共有100个数,在这100个数中,高位一直都是9,这个位上的9在前一种情况中没有算上,所以要加上10的(i-1)次方
然后来计算从0到一个数x的每个数的出现的次数,为了简化处理过程,先算上前导0,例如,当x=354时,统计从000到354,0到9每个数出现的次数
具体方法:从高位到低位一位一位处理,例如当前在最高位第三位,这一位上的数是3,那么从0到2,共有3个数,每个数其后都接的是00-99,因此从0到9,每个数的出现次数都应该加上,dp[3-1=2]*3(3是因为0-2有三个数)
然后看高位的从0到2,这三个数,当后面两位从00到99(共10的二次方个数)时,高位的这三个数出现的次数应该都加上10^2
最后看高位的3,其后从00到45,共46个数,所以3的出现次数应该加上46,这样,从000到345的三位数就统计完成
仿照以上,统计两位数(00到45)和一位数(0到5)中的0-9的出现次数
处理前导0,当统计3位数时,哪些情况被多统计了?000-099,共10^2个数,所以统计到第i位时,0出现的次数应该减去10^(i-1)个
复杂度略
#include<iostream>
#include<cstdio>
using namespace std;
#define N 15
#define ll long long
ll cnt1[15],cnt2[15],dp[20],ten[20];
void init(){
ten[0]=1;
for(int i=1;i<=15;i++){
ten[i]=ten[i-1]*10;
dp[i]=dp[i-1]*10+ten[i-1];
}
}
void solve(ll x,ll *cnt){
int digit[20],len=0;
while(x){
digit[++len]=x%10;
x/=10;
}
for(int i=len;i>=1;i--){
for(int j=0;j<=9;j++){
cnt[j]+=dp[i-1]*digit[i];
}
for(int j=0;j<digit[i];j++){
cnt[j]+=ten[i-1];
}
ll t=0;
for(int j=i-1;j>=1;j--){
t=t*10+digit[j];
}
cnt[digit[i]]+=t+1;
cnt[0]-=ten[i-1];
}
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);
init();
ll a,b;
cin>>a>>b;
//利用函数减少代码量,简化逻辑
solve(a-1,cnt1);
solve(b,cnt2);
for(int i=0;i<=9;i++){
cout<<cnt2[i]-cnt1[i]<<" ";
}
return 0;
}
方法二(推荐)
定义dp[pos][sum]表示,当前在第pos位,要统计的数在pos位之前出现的次数为sum时,从00....0到99....9,(共pos位)要统计的数的出现次数与sum的和
增加两个参数:limit和lead,limit=1代表当前有数位限制,lead=1代表当前有前导0,用函数dfs进行记忆化搜索,dfs(pos,sum,lead,limit),初始时lead=1,limit=1,pos=数字位数,sum=0,如果没有前导0并且没有数位限制,就把当前dfs计算出的答案当做dp[pos][sum]
dfs返回的是:当有无前导0(看lead),有无数位限制(看limit)时,当pos位之前,要统计的数出现了sum次时,pos位及其以后,要统计的数出现次数与sum的和
dfs过程中,如果有数位限制,则遍历从0到当前位的数值,否则,遍历该位从0到9的每一个数,如果当前lead=1,有前导0,就不能将当前位的0计入0出现的次数,此时递归:dfs(pos-1,sum,1,0),
当前位的从1-9的这些数不受有没有前导0的影响,如果从1-9的某个数和当前要统计的数一样,则递归dfs(pos-1,sum+1,0,<暂定>) ,如果不一样,则递归dfs(pos-1,sum,0,<暂定>),<暂定>的规则为,如果当前遍历到的数为最高位并且limit=1(受数位限制),则第四个参数limit=1,否则limit=0
dfs的边界条件是当pos=0,返回sum
25.3.13更新:把limit和lead作为dp数组的参数效率更优
复杂度略
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define int long long
int dp[15][15][2][2],num[15];
int now;
int dfs(int pos,int sum,int lead,int limit){
//守卫条件
if(pos==0) return sum;
if(dp[pos][sum][lead][limit]!=-1) return dp[pos][sum][lead][limit];
int up=(limit?num[pos]:9);
int ans=0;
for(int i=0;i<=up;i++){
if(lead==1 && i==0){
ans+=dfs(pos-1,sum,1,0);
}
else if(i==now){
ans+=dfs(pos-1,sum+1,0,i==up && limit==1);
}else {
ans+=dfs(pos-1,sum,0,i==up && limit==1);
}
}
dp[pos][sum][lead][limit]=ans;
return ans;
}
int solve(int x){
int len=0;
while(x){
num[++len]=x%10;
x/=10;
}
//初始化
memset(dp,-1,sizeof(dp)); //用0作为初始值也可以,但是-1更合规和保险
return dfs(len,0,1,1);
}
signed main()
{
/*
now=0;
solve(99);
cout<<dp[1][0];
*/
int a,b;cin>>a>>b;
for(int i=0;i<=9;i++){
now=i;
cout<<solve(b)-solve(a-1)<<" ";
}
return 0;
}