[ZJOI2010]数字计数,洛谷P2606,数位Dp(注意前导零带来的影响)

本文介绍了一种使用数位DP解决特定类型问题的方法。通过避免暴力枚举,利用数位DP来减少时间复杂度。文章详细解释了如何通过存储中间结果和预处理后缀和来优化算法,并提供了一个具体的实现示例。

正题

      这题我们看道题面,看上去很简单,但是如果暴力枚举的时间复杂度是很高的。

      所以我们就会想到用数位Dp来去求解。

      关于数位Dp,其实是一种处理数字局限性的计算方法。

      关键就在于怎么样让这个“局限性”体现出来的同时,加快我们程序运行的速度。

      那么对于这题,它要求解的目标是每个数码有多少个。

      我们会想到00~99处理出来的数字都是一样的,在此中0有10个,1有20个……

      所以对于2XX,和3XX,他们后面的数位是不用重复求解的,这是我们可以把它们存起来。

      定义solve[i]表示填到第i位到最后一位的状态总和(只计算000..0~999..9)是多少。

      当前这个位置的数码,也要进行存储,如果后面可以填000~999那么就是k*10^n(其中k为当前枚举的数字,10^n表示这一位到最后共有n位),否则如果后面的数有限制(比如说547,当前枚举到5这一位,后面只能填00~47),那么5就会被计算(47+1)=48次,所以还要预处理一个后缀和g。

      然后就完成了这一道大水题。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;

long long x,y;
long long solve[100];//无限制且没有前导0时第i位到最后一位的状态总和
long long val[100];//表示第i位的值
long long ci[100];//表示10的i次方
long long g[100];/表示后缀

long long Dp(int pos,int tf,int k,int all){
    if(pos==0) return 0;//枚举完就返回,不产生价值
    if(!tf && !all && solve[pos]!=-1) return solve[pos];//如果已经处理过就返回已经处理过的值
    long long res=0;//否则就计算
    int end=tf?val[pos]:9;//枚举的上限根据前面的有没有“满”来决定
    for(int i=0;i<=end;i++)	{
        res+=Dp(pos-1,tf&&i==end,k,all&&i==0);//往下搜
        if(!(all && i==0) && i==k) res+=((i==end&&tf)?g[pos-1]+1:ci[pos-1]);//如果前面没有前导零而且i等于当前所求解的k数码,那么他就会
    }//前所求解的k数码,那么他就会带来相应的价值
    if(!all && !tf) solve[pos]=res;//记忆化
    return res;
}

long long gett(long long v,int k){
    memset(solve,-1,sizeof(solve));
    int len=0;
    while(v!=0){
        val[++len]=v%10;
        v/=10;
        g[len]=g[len-1]+val[len]*ci[len-1];//预处理后缀
    }
    return Dp(len,1,k,1);
}

int main(){
    ci[0]=1;
    for(int i=1;i<=15;i++) ci[i]=ci[i-1]*10;
    long long tot=0;
    scanf("%lld %lld",&x,&y);
    x--;
    for(int i=0;i<=9;i++)//根据每一个数码进行求解
        printf("%lld ",gett(y,i)-gett(x,i));
}

 

### 数位DP数字游戏中的应用 数位动态规划(数位DP)作为一种特定形式的动态规划,在处理涉及数字各个数位上的特性时非常有效。这类问题通常出现在算法竞赛中,尤其是在蓝桥杯等赛事里频繁现身[^1]。 #### 定义状态 对于大多数基于数位的游戏或挑战来说,定义合适的状态至关重要。一般而言,会采用一个三维数组`dp[pos][state][limit]`来表示当前位置、当前状态下是否存在某种属性以及是否受到前导影响等问题下的最优解。其中: - `pos`: 当前正在处理数位位置; - `state`: 可能代表多种含义,比如奇偶性计数器或其他与题目条件紧密关联的信息; - `limit`: 表明前面所有的数位都紧贴给定范围的最大值边界;如果为真,则意味着本层的选择也受限于该最大值对应的这一位是什么。 ```cpp int dp[20][100]; // 假设最多有20位十进制整数, state的具体意义取决于具体问题 bool limit; ``` #### 状态转移方程 当明确了上述三个维度之后,就可以通过递归来实现自顶向下的求解过程,并借助记忆化技术加速运算效率。每次迭代过程中,尝试填充每一位可能取到的所有合法数值,并更新相应的子问题答案直到遍历完整个字符串为止。此时需要注意的是要根据实际情况调整`state`参数传递逻辑以适应不同场景需求。 例如在一个简单的例子中,假设目标是从区间\[L,R\]内找出满足某些特殊性质(如不含连续相同字符)的正整数数量。那么可以这样构建状态转移关系: ```cpp // 记忆化搜索函数模板 long long dfs(int pos, int pre, bool same, bool leadZero, bool isLimit) { if (pos == length_of_number) return !leadZero; // 边界情况判断 if (!isLimit && ~dp[pos][pre]) return dp[pos][pre]; long long res = 0; int up = isLimit ? digits[pos] : 9; // 如果受限制则最高只能选到digit[pos], 否则可自由选择至9 for (int i = 0; i <= up; ++i) { // 枚举当前位放置什么数字 if ((same && i == pre)) continue; // 跳过违反规则的情况 res += dfs(pos + 1, i, i==pre&&(!leadZero), leadZero&&(i==0), isLimit && i == up); } if (!isLimit) dp[pos][pre] = res; return res; } ``` 此段伪代码展示了如何利用数位DP框架去解答一类具有约束性的组合计数类问题。当然,实际编码还需依据具体的业务背景做适当修改和完善[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值