洛谷P2602[ZJOI2010]数字计数 分析与解答

方法一:

用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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值