【数位DP】

【数位DP】

数字位数,把每一位拆开、关注每一位数字
image

特征

1.最终目的为计数:统计满足一定条件的数的数量
2.问题经过转化后,可以使用 「数位」 的思想去理解和判断
3.输入:提供数字区间/上界 来作为统计的限制
4.上界很大\(10^{18}\)

思考方式

相似的计数过程进行归并->这些过程产生的计数答案存在一个通用数组

通常会使用常见计数技巧:
eg 前缀和计数:\(ans_{[l,r]}=ans_{[0,r]}-ans_{[0,l-1]}\)

【统计答案】 记忆化搜索/循环迭代递推
从高到低枚举每一位,再考虑每一位都可以填哪些数字,最后利用通用答案数组统计答案。

例题

题目积累

最大为 N 的数字组合

https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/description/
题目描述
image
思路

三种情况
(1)位数少
(2)[小]xxx
(3)[相同数/小][相同数/小][相同数/小]xxx

法一

class Solution {
public:
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        vector<int> num(digits.size(),0);
        int cnt=0;
        for(auto digit : digits){
            num[cnt++]=digit[0]-'0';
        }
        //对n进行操作:方便每一位提取数字
        int tmp=n/10;
        int len=1;
        int offset=1;//方便提取n中某一位数字:辅助len
        while(tmp>0){
            tmp/=10;
            len++;
            offset*=10;
        }
        return f1(num,n,offset,len,0,0);
    }
    /*
    【思路一:递归】
    还剩下len位没决定
    free
    (1)如果之前的位比num小->free==1 接下来的数可以自由选择
    (2)如果之前的位比num一样->free==0 接下来的数不能大于num当前位
    fix
    (1)之前的位没有使用过数字->fix==0
    (2)之前的位有使用过数字->fix==1
    */
    int f1(vector<int> digits,int num,int offset,int len,int free,int fix){
        if(len==0){
            return fix==1?1:0;
        }
        int ans=0;
        //num在当前位的数字
        int cur=(num/offset)%10;
        if(fix==0){
            //之前从来没有选过任何数字
            //当前依然不需要任何数字,考虑后续可能性
            ans+=f1(digits,num,offset/10,len-1,1,0);
        }
        if(free==0){
            //不能自由选择
            for(int i:digits){
                if(i<cur){
                    ans+=f1(digits,num,offset/10,len-1,1,1);
                }
                else if(i==cur){
                    ans+=f1(digits,num,offset/10,len-1,0,1);
                }
                else{//i>cur 因为digits有序
                    break;
                }
            }
        }
        else{
            //可以自由选择
            ans+=digits.size()*f1(digits,num,offset/10,len-1,1,1);
        }
        return ans;
    }
};

法二
对于已经确定小于的情况->可以任选->直接打表计算

class Solution {
public:
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        vector<int> num(digits.size(),0);
        int cntt=0;
        int m=digits.size();
        for(auto digit : digits){
            num[cntt++]=digit[0]-'0';
        }
        //对n进行操作:方便每一位提取数字
        int tmp=n/10;
        int len=1;
        int offset=1;//方便提取n中某一位数字:辅助len
        while(tmp>0){
            tmp/=10;
            len++;
            offset*=10;
        }
        //前缀已经小了 后续可以直接加上去 不用递归算了
        //cnt[i]:已知前缀比num小,剩下i位没有确定,在前缀确定的情况下,有多少种数字排列
        //cnt[0]=1 后续没有,前缀状况确定
        //cnt[1]=m
        //cnt[2]=m*m
        //cnt[3]=m*m*m
        //...
        vector<int> cnt(20,0);
        cnt[0]=1;
        int ans=0;
        for(int i=m,k=1;k<len;k++,i*=m){
            cnt[k]=i;
            ans+=i;
        }
        return ans+=f2(num,cnt,n,offset,len);
    }
    int f2(vector<int> digits,vector<int> cnt,int num,int offset,int len){
        if(len==0){
            return 1;
        }
        int cur=(num/offset)%10;
        int ans=0;
        for(auto digit:digits){
            if(digit<cur){//后面可以任意选->直接加
                ans+=cnt[len-1];
            }
            else if(digit==cur){
                ans+=f2(digits,cnt,num,offset/10,len-1);//往下接着找
            }
            else break;
        }
        return ans;
    }
};

参考:
https://oi-wiki.org/dp/number/

posted @ 2025-05-19 15:09  White_ink  阅读(6)  评论(0)    收藏  举报