AcWing 338. 计数问题

\(AcWing\) \(338\). 计数问题

一、题目描述

给定两个整数 \(a\)\(b\),求 \(a\)\(b\) 之间的所有数字中 \(0∼9\) 的出现次数。

例如,\(a=1024,b=1032\),则 \(a\)\(b\) 之间共有 \(9\) 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 \(0\) 出现 \(10\) 次,\(1\) 出现 \(10\) 次,\(2\) 出现 \(7\) 次,\(3\) 出现 \(3\) 次等等…

输入格式
输入包含多组测试数据。

每组测试数据占一行,包含两个整数 \(a\)\(b\)

当读入一行为 0 0 时,表示输入终止,且该行不作处理。

输出格式
每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示 \(0\) 出现的次数,第二个数字表示 \(1\) 出现的次数,以此类推。

数据范围
\(0<a,b<100000000\)

输入样例

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247

二、暴力法

#include <bits/stdc++.h>

using namespace std;
const int N = 10;

// 暴力法获取从1开始到n,有多少个指定的x,[类似于前缀和的思路]
// 从0到10的8次方,就是枚举每一位,一个测试点是 8*10^8,会超时,不可取
int force_count(int n, int x) {
    int res = 0;
    for (int i = 1; i <= n; i++) {
        int t = i;
        while (t) {
            if (t % 10 == x) res++;
            t /= 10;
        }
    }
    return res;
}

int main() {
    int a, b;
    //当读入一行为0 0时,表示输入终止,且该行不作处理,注意这里 a||b的使用方法
    while (cin >> a >> b, a || b) {
        if (a > b) swap(a, b); // 这题还玩小的在后,大的在前,需要我们用代码判断,shit!
        //计算0--9的每一个数出现的次数
        for (int i = 0; i <= 9; i++)
            cout << force_count(b, i) - force_count(a - 1, i) << ' ';
        cout << endl;
    }
    return 0;
}

但是,本题数据范围太大了,\(0<a,b<100000000\)

两个数字\(a-b\)之间,最多就有\(10^8\)个数字,每个数需要遍历每一位,就是一个数字需要遍历\(8\)次最多,一次的时间复杂度最高是:\(10^8*8\),而且有多组测试数据,不出意外会\(TLE\)

三、思考过程

如计算 \(0 \sim 78501\)\(5\) 出现的次数。 【答案:\(41502\)

枚举\(5\)的位置

1、如果\(5\)位于倒数第\(1\)位,形如:xxxx5,由于\(7850\)\(1\)当前数位\(1\)小于\(5\),前面只能取\(0\sim 7849\)

\(5\)为末位共\(7850\)

2、如果\(5\)位于倒数第\(2\)位,形如:xxx5x,由于\(785\)\(0\)\(1\)当前数位\(0\)小于\(5\),前面只能取\(0\sim 784\),末位就可以是\(0\sim 9\),再加\(10\)

\(5\)为末二位共\(785*10^1=7850\)个【乘法原理】

3、如果\(5\)位于倒数第\(3\)位,形如:xx5xx,由于\(78\)\(5\)\(01\)当前数位\(5\)等于\(5\):

  • 前面取\(0\sim 77\),共\(78\)个;后面是\(00 \sim 99\),共\(100\)个。乘法原理,就是\(78*100=7800\)个;
  • 前面取\(78\),后面取“\(00\),\(01\)”;共\(2\)个。
\(5\)为末三位共\(78*10^2+2=7802\)

4、如果\(5\)位于倒数第\(4\)位,形如:x5xxx,由于\(7\)\(8\)\(501\)当前数位\(8\)大于\(5\),前面可取\(0\sim 7\)(共\(8\)个),后面\(0 \sim 999=1000\). 小计:\(8*1000=8000\)

\(5\)为末四位共\(8*10^3=8000\)个。

5、如果\(5\)位于倒数第\(5\)位,形如:5xxxx,由于\(7\)\(8501\)当前数位\(7\)大于\(5\),后面\(0 \sim 9999\). 小计:\(10000\)

\(5\)为末五位共\(1*10^5=10000\)个。
合计: \(7850+7850+7802+8000+10000=41502\)

总结:枚举数字\(x\)出现的位置,按\(x\)\(n\)在该位上的大小关系,分为大于、小于、等于三类讨论。

  • 数字x大于当前位上的数字

    x前面数字 乘以 \(pow(10,\)后面剩余位数\()\)

  • 数字x小于当前位上的数字

    x前面数字加\(1\) 乘以 \(pow(10,\)后面剩余位数\()\)

  • 数字x等于当前位上的数字

    x前面数字加\(1\) 乘以 \(pow(10,\)后面剩余位数\()\) 再加上 后面的剩余数字再加\(1\)

四、实现代码

#include <bits/stdc++.h>

using namespace std;
int a, b;

// 统计数字n中存在x=[1,9]的个数情况
// 建议结合题解中 78501示例的流程进行代码理解和记忆
int count_x(int n, int x) {
    int res = 0;  // 结果
    int t = n;    // n的副本
    int base = 1; // base=pow(10,?) 基数
    while (t) {   // 数位分离过程中讨论大小关系
        // ① x大于当前数位
        if (t % 10 < x) res += t / 10 * base; // t:最高位到当前位,t/10:最高位到当前位前一位
        // ② x小于当前位,加1
        else if (t % 10 > x)
            res += (t / 10 + 1) * base;
        // ③ x等于当前位
        else
            res += t / 10 * base + (n % base + 1); // 后面的剩余数字+1

        // 数位分离
        t /= 10;    // 前一位
        base *= 10; // 变基
    }
    return res;
}

/**
对于0,稍作修改,
此时只需分成两类,因为不存在当前为小于0的情况,不过每次的最高位要排除全0的情况。
*/
int count_0(int n) {
    int res = 0;  // 结果
    int t = n;    // n的副本
    int base = 1; // base=pow(10,?)
    while (t) {   // 数位分离过程中讨论大小关系
        // ① 当前位等于0
        if (t % 10 == 0) res += (t / 10 - 1) * base + (n % base + 1);
        // ② 当前位大于0
        else
            res += (t / 10) * base;

        // 数位分离
        t /= 10;    // 前一位
        base *= 10; // 变基
    }
    return res;
}

int main() {
    // 输入多组数据,以a=0,或b=0视为终点
    while (cin >> a >> b, a || b) {
        if (a > b) swap(a, b);                      // 这题还可以输入反着的,无聊~
        cout << count_0(b) - count_0(a - 1) << " "; // 单独计算数字0计算结果
        for (int i = 1; i <= 9; i++)                // 输出数字1~9的计算结果
            cout << count_x(b, i) - count_x(a - 1, i) << " ";
        cout << endl;
    }
    return 0;
}

五、数位\(DP\) 模板

猛击这里

六、\(dfs\)实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 32; // 2^{32}足够int用了
int a[N], al;     // 数位分离拆开用的数组
int f[N][N];      // 第一维:第几位数;第二维:走到当前数位,已经取得了多少个
int n;            // 当前枚举到的是哪个数

/**
 u      :从高到低,现在是第几位数
 lead   :是否考虑前导零
 st     :到当前深度已经出现n的个数
 op     :是否贴上界
 返回值 :从当前数位u出发,在当前lead,st,op的前提下,可以得到多少个符合题意的数字
*/
int dfs(int u, int lead, int st, int op) {
    if (!u) return st;                              // 递归出口,u==0时,所有数位计算完毕,al是从1开始计数的
    if (!lead && !op && ~f[u][st]) return f[u][st]; // 非前导0 + 不贴上界 + 算过

    // u位上的最大值
    int up = op ? a[u] : 9; // 如果贴上界,则到op,否则可以全部取到

    int res = 0; // 按上面三个条件lead,st,op走到u这个数位时,最终可以获取到多少个数呢?
    for (int i = 0; i <= up; i++) {
        int sum = st;
        // ① 前面出现过非0数字 或者 本位置非0
        // ② 当前数位是要查找的数字
        if ((!lead || i > 0) && i == n) sum++;
        // 如果原来是贴上界,现在继续贴上界,那么贴上界继续
        res += dfs(u - 1, lead && !i, sum, op && i == a[u]);
    }

    // 记忆化
    if (!lead && !op) f[u][st] = res;
    return res;
}

int calc(int x) {
    al = 0;
    while (x) a[++al] = x % 10, x /= 10; // 高位在右,低位在左
    // al    :从al位开始
    // lead  :存在前导0
    // st    :前面填的数中数字n的个数是0个
    // op    :贴上界
    return dfs(al, 1, 0, 1);
}

int main() {
    int l, r;
    while (cin >> l >> r, l + r) {    // l+r用的漂亮,只有两个都是0时,l+r才能是0,等同于 l || r
        if (l > r) swap(l, r);        // 谁大谁小还不一定,这题真变态
        for (n = 0; n <= 9; n++) {    // 0,1,2,3,...个数都有多少个
            memset(f, -1, sizeof(f)); // 每轮需要初始化dp数组
            cout << calc(r) - calc(l - 1) << ' ';
        }
        cout << endl;
    }
    return 0;
}

七、答疑解惑

\(Q\):这句话怎么理解,为什么是返回\(st\), 而不是返回\(1\)呢?

if (!u) return st;

:当 \(u\)\(0\)时,表示所有数位都已经递归完毕。此时, \(st\) 记录了已经取得了多少个要查找的数字,也就是最终的答案。

因此,此时需要将 \(st\) 作为函数的返回值,表示当前的递归子树所能取得的符合条件的数的个数。

在此题中,需要在数列中查找某个数字出现的次数,因此返回 \(st\) 表示查询到的该数字在当前子树中出现的次数。

如果直接返回\(1\),则表示该数字出现了一个,而没有考虑出现的次数,与题目要求不符

posted @ 2021-10-25 13:58  糖豆爸爸  阅读(337)  评论(2编辑  收藏  举报
Live2D