寒训2G 数字计数

题意

给定两个正整数 ,求在 中的所有整数中,每个数码(digit)各出现了多少次。

样例

输入

输出

1 99

9 20 20 20 20 20 20 20 20 20

思路

本题是正儿八经的数位DP板子题。对于区间,我们可以转化为,所以关键就是要求,而说到这里数位DP就坐不住了!而所谓数位DP正是会者不难难者不会的东西,所以我们直接来到数位DP的环节吧!这里采用记忆化搜索来实现数位DP。

在记忆化搜索前,我们要先对这个数进行一些处理——包括拆分成若干数组成的数组并反转它(这这样可以直接从高位开始搜起,不用拐太多弯路):

long long part(long long x) {
    len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    memset(dp, -1, sizeof(dp));
    reverse(a + 1, a + 1 + len);
    return dfs(1, 0, 1, 1);
}

搜索就直接从高位往低位搜:

long long dfs(int pos, long long ans, int limit, int lead) {
    if (pos > len)
        return ans;        //不能再搜下去了
    long long u = 0;
    int res = limit ? a[pos] : 9;        //如果无最高位限制,那么就可以取到9
    for (int i = 0; i <= res; ++i) {
        //有前导0且当前位是0
        if ((i == 0) && lead)
            u += dfs(pos + 1, ans, limit && (i == res), 1);
        else
            u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
    }
    return u;    //记忆化
}

那怎么个“记忆化”呢?打表数数时可以发现我们数了很多重复的部分,比如11-20里的5的出现次数和21-30的出现次数一样,或者说我们在数11-100的时候其实除了数了一次十位,还数了好多次个位(而这是没有必要的!)那么可以找一个dp数组来记录本次数数环节得到的答案,dp的状态便是本次搜索的有关参数(多加点参数有利于不认错!),并且在检测到记忆的时候直接返回对应的值,可以大大减少搜索时间!于是搜索过程变成了这样:

long long dfs(int pos, long long ans, int limit, int lead) {
    if (pos > len)
        return ans;        //不能再搜下去了
    if (dp[pos][ans][limit][lead] != -1)
        return dp[pos][ans][limit][lead];    //检测到记忆
    long long u = 0;
    int res = limit ? a[pos] : 9;        //如果无最高位限制,那么就可以取到9
    for (int i = 0; i <= res; ++i) {
        //有前导0且当前位是0
        if ((i == 0) && lead)
            u += dfs(pos + 1, ans, limit && (i == res), 1);
        else
            u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
    }
    return (!limit && !lead) ? dp[pos][ans][limit][lead] = u : u;    //记忆化
}

代码里有两个不可忽视的细节:lead和limit标记!

Lead标记

lead标记即前导0标记,这个标记可以使代码不会搜到000001 000003 001002 这样的数,其原理变是在发现自己是最高位时不计入,并且给后续的数也打上”前导0标记“。

Limit标记

limit标记记录目前数字是否已经搜到了”极限“(即原来的数对应位的最大值)。假设对97进行搜索,那么最高位是9,我们可以直接从1到9遍历一次,往下的时候就会习惯性地来一发记忆化:1开头的时候个位有1个8,2开头的时候个位有1个8,3开头的时候......8开头的时候个位有1个8,9开头的时候个位有1个8。于是悲剧就发生了——而悲剧就发生在我们在搜到9的时候还继续看作一般情况,因此我们需要这个标记来知道该位的搜索范围,也同时避免了盲目记忆化带来的问题。

最后就是实现环节啦~

代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int len, digit;
long long dp[20][20][2][2];        // dp[pos][last][limit][lead]
int a[25];
long long l, r;

long long dfs(int pos, long long ans, int limit, int lead) {
    if (pos > len)
        return ans;        //不能再搜下去了
    if (dp[pos][ans][limit][lead] != -1)
        return dp[pos][ans][limit][lead];    //检测到记忆
    long long u = 0;
    int res = limit ? a[pos] : 9;        //如果无最高位限制,那么就可以取到9
    for (int i = 0; i <= res; ++i) {
        //有前导0且当前位是0
        if ((i == 0) && lead)
            u += dfs(pos + 1, ans, limit && (i == res), 1);
        else
            u += dfs(pos + 1, ans + (i == digit), limit && (i == res), 0);
    }
    return (!limit && !lead) ? dp[pos][ans][limit][lead] = u : u;    //记忆化
}

long long part(long long x) {
    len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    memset(dp, -1, sizeof(dp));
    reverse(a + 1, a + 1 + len);
    return dfs(1, 0, 1, 1);
}

int main() {

    cin >> l >> r;
    for (int i = 0; i <= 9; ++i) {
        digit = i;
        cout << part(r) - part(l - 1) << " ";
    }
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值