东方博宜OJ 2558:数字游戏 ← 数位DP + 不降数

【题目来源】
https://oj.czos.cn/p/2558

【题目描述】
科协里最近很流行数字游戏。某人命名了一种不降数,这种数字必须满足从左到右各位数字成小于等于的关系,如123,446。
现在大家决定玩一个游戏,指定一个整数闭区间 [a, b],问这个区间内有多少个不降数。有多组测试数据。每组只含两个数字 a,b,意义如题目描述。

【输入格式】
有多组测试数据。每组只含两个数字a,b,意义如题目描述。

【输出格式】
每行给出一个测试数据的答案,即[a, b]之间有多少不降数。

【输入样例】
1 9
1 19

【输出样例】
9
18

【数据范围】
对于全部数据,1≤a≤b≤2^31-1。

【算法分析】
● f[i][j] 表示长度为 i 最高位为 j 的满足条件的数字个数。​​​​​​​

● 核心状态解析
(一)初始化

for(int i=0; i<=9; i++) f[1][i]=1;

由于每个一位数字(0 ~ 9)都是“非严格递增”的数字,即不降数。因此,f[1][i]=1。
(二)动态规划的状态转移 (i>=2)

for(int i=2; i<N; i++)
    for(int j=0; j<=9; j++)
        for(int k=j; k<=9; k++)
            f[i][j]+=f[i-1][k];

(1)外层循环‌ for(int i=2; i<N; i++):从长度为 2 的数字开始,依次计算到最大长度 N-1。
(2)中层循环‌ for(int j=0; j<=9; j++):j 代表‌当前要计算的、长度为 i 的数字的最高位数字‌。
(3)内层循环‌ for(int k=j; k<=9; k++):这是状态转移的关键。
▆ k 的取值范围是 [j, 9]。这确保了在数字 j 之后的下一位数字 k 不小于 j,从而保证了数位的‌非严格递增性‌。
▆ f[i][j]+=f[i-1][k]:要构造一个长度为 i、最高位为 j 的不降数,可以在 j 后面拼接上一个长度为 i-1 的不降数,‌这个长度为 i-1 的数的最高位(即 j 的后一位)必须是 k‌,并且 k 必须满足 k>=j。

boyi2558

因此,将所有以 k 为最高位的、长度为 i-1 的不降数的可能数量 f[i-1][k] 累加起来,就得到了 f[i][j]。

●​​​​​​​ 核心代码功能详述

int cnt=0,pre=0;
for(int i=v.size()-1; i>=0; i--) {
    int x=v[i];
    for(int j=pre; j<x; j++) {
        cnt+=f[i+1][j];
    }
    if(x<pre) break;
    pre=x;

    if(i==0) cnt++;
}

(一)变量初始化

int cnt=0,pre=0;

cnt:最终要返回的满足条件的数字总数。
pre:记录在‌当前已处理的高位部分‌中,最后一位(最低位)的数字值。初始为 0,因为尚未处理任何高位,可以认为“虚拟前缀”的最后一位是 0,这确保了第一位可以取 0 到 9 的任何数字。
 (二)逐位处理循环

for(int i=v.size()-1; i>=0; i--) {
    int x=v[i];

循环从最高位(v.size()-1)向最低位(0)遍历数字 n 的每一位。x 是当前位在原数 n 中的实际值。
 (三)核心计数:处理当前位小于原数值的情况

for(int j=pre; j<x; j++) {
    cnt+=f[i+1][j];
}

这是算法的关键步骤,用于累加‌所有当前位小于 x 且满足递增条件‌的数字数量。
(1)j 的取值范围‌:从 pre(已确定前缀的最后一位)到 x-1。这保证了新选的数字 j 不小于前缀的最后一位 pre,从而维持了数位的非严格递增性。
(2)f[i+1][j] 的含义‌:i+1 表示从当前位开始(包括当前位)到最低位的总剩余位数。f[i+1][j] 表示构造一个长度为 i+1 位、‌最高位为 j‌ 且所有数位非严格递增的数字的‌全部可能数量‌。这个值已在 init() 函数中预先计算好。
(3)累加逻辑‌:对于每一个可能的 j(j<x),由于当前位已经固定为小于原数 n 对应位的值,因此‌剩余的所有低位(i 位)可以任意填充‌,只要满足从 j 开始的递增条件即可。这些“任意填充”的全部可能性总数,正是 f[i+1][j]。将它们全部累加到 cnt 中,就计入了所有‌当前位小于 x‌ 的、且小于 n 的合法数字。
(四)处理当前位等于原数值的情况与边界检查

if(x<pre) break;
pre=x;

(1)if(x<pre) break;‌:这是一个‌关键的中断条件‌。它检查原数 n 的当前位 x 是否小于已确定前缀的最后一位 pre。如果是,说明数字 n 本身在当前位置就破坏了“非严格递增”的条件。由于我们接下来要尝试固定当前位为 x 并继续处理低位,但 x<pre 已经导致递增性被破坏,所以后续所有情况(固定当前位为 x 并继续)都不可能构成递增数。因此直接跳出循环,不再处理更低的位。
(2)pre=x;‌:如果 x>=pre,说明到目前为止原数 n 的前缀部分本身是满足递增条件的。于是我们将 pre 更新为当前位的值 x,然后进入下一位的处理。这相当于我们“选择”了当前位等于原数 n 的对应值,继续向下试探。
(五)处理完所有位后的计数

if(i==0) cnt++;

当循环成功处理到最低位(i==0)且没有因 x<pre 而中途跳出时,说明原数字 n 的每一位都满足非严格递增的条件(x>=pre 始终成立)。因此,数字 n 本身也是一个合法的递增数,需要将其计入总数,故 cnt++。

【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int N=12;
int f[N][10]; //f[i][j]表示长度为i最高位为j的满足条件的数字个数

void init() {
    for(int i=0; i<=9; i++) f[1][i]=1;
    for(int i=2; i<N; i++)
        for(int j=0; j<=9; j++)
            for(int k=j; k<=9; k++)
                f[i][j]+=f[i-1][k];
}

int dp(int n) {
    if(n==0) return 1;
    vector<int> v;
    while(n) {
        v.push_back(n%10);
        n/=10;
    }

    int cnt=0,pre=0;
    for(int i=v.size()-1; i>=0; i--) {
        int x=v[i];
        for(int j=pre; j<x; j++) {
            cnt+=f[i+1][j];
        }
        if(x<pre) break;
        pre=x;

        if(i==0) cnt++;
    }
    return cnt;
}

int main() {
    init();
    int le,ri;
    while(cin>>le>>ri) {
        cout<<dp(ri)-dp(le-1)<<endl;
    }

    return 0;
}

/*
in:
1 9
1 19

out:
9
18
*/





【参考文献】
https://blog.csdn.net/hnjzsyjyj/article/details/108507656
https://blog.csdn.net/hnjzsyjyj/article/details/156048397
https://blog.csdn.net/hnjzsyjyj/article/details/156039366
https://blog.csdn.net/hnjzsyjyj/article/details/156267002
https://blog.csdn.net/hnjzsyjyj/article/details/156011817
https://blog.csdn.net/WhereIsHeroFrom/article/details/148437243
https://www.acwing.com/solution/content/245183/





posted @ 2025-12-26 21:50  Triwa  阅读(3)  评论(0)    收藏  举报