【C++】数位DP的模板(找到小于n的数字的每位组成)

整理自Leetcode大佬灵神(灵茶山艾府)的板子,感谢大佬的题解ψ(`∇´)ψ
大佬

1 思路讲解

以力扣的2376题为例:
【C++】数位DP的模板(找到小于n的数字的每位组成)_第1张图片
我们先去看当n = 123为例子时的思路,可以把问题看作是f(i, mask)然后一共有三个位置i,往三个位置填数字(mask是为了防止位上的数字出现重复的约束条件,本文为了能够记忆化搜索,将mask用10个位的int数字1024来代替vectorcnt(10,false))
【第三个参数】我们顺着思路向下分析:
因此,当i = 0,这个位置的数字可以是0, 1;当i = 1,这个位置的数字可以是0,1,2或者0-9;当i=2时,这个位置的数字可以是0, 1, 2或者0-9;`

  • i=1时能否直接0-9都是根据i=0是否贴合n相应位的大小(题中为1)来决定的。
  • 同样,当i=2时能否直接0-9时根据i=0i=1是否贴合n相应位的大小(题中为2)来决定的。

故,我们需要第三个参数,来表示当前的i是否要收到限制。因此,引入bool型的isLimit参数。
【第四个参数】与本题约束条件相碰撞的一种情况:前导零!题目中要求数字不允许重复,但是当010出现时,明明是合法的,但是会被约束条件所剪枝。因此我们引入一个新的布尔参数isNum,其表示i前面是否填了数字:

  • 如果为false,代表当前为可以跳过(继续保持false),或者填入至少为1的数字。
  • 如果为true,代表前面已经有数字了,因此只能填0-9的数字

因此,思路可以分为下面:

  1. 因为is_limit涉及到了数和位的比较,因此需要将n转化成string,并且加入到dp递归的参数中。
  2. 首先先确定结束关系,当位置i到了n的长度了,那么就到了终点了,此时返回is_num因为is_num=0意味着没有取过数,自然方法数为0。
  3. 然后是记忆化步骤,判断该dp里面的值是不是改变了,如果改变了,直接返回该值即可
  4. 接下来初始化答案值,开始下面的递归各类情况求值。
  5. 判断is_num是否跳过,先把跳过该位的情况递归求出来
  6. 判断is_limit,确定接下来回溯选择的范围的上界
  7. 因为第五步无论是否跳过,都需要继续计算上没跳过的情况,因此需要根据is_num来决定回溯选择的范围的下界
  8. 配合约束条件,将答案值在回溯选择的过程中加起来即可。

【初始dp的情况】注意一开始的is_limit应为true(因为第一位一定不是0-9,除非n的最高位是9,但这也是收到限制),而is_num应为false(因为第一位也可以是0,进行跳过)。

class Solution {
public:
    //这里的第一个10是因为n的范围不超过1e9
    //第二个1024代表10个位,正好数字的范围是0-9
    int arr[10][1024][2][2];
    int countSpecialNumbers(int n) {
        string n_s = to_string(n);
        return dp(0, 0, true, false, n_s);
    }
    int dp(int i, int mask, bool is_limit, bool is_num, string n) {
        //1.结束条件
        if(i == n.size()) return is_num;
        //2.记忆化搜索
        if(arr[i][mask][is_limit][is_num] != 0) return arr[i][mask][is_limit][is_num];
        //3.前面是否已经有数字了,没有才能跳过
        int ans = 0;
        //因为跳过了,因此后面既不受限制,前面也没出现过数字
        if(!is_num) ans += dp(i+1, mask, false, false, n);
        //4.确定该位数的上下界
        int up = 9, down = 0;
        if(is_limit) up = n[i] - '0';
        if(!is_num) down = 1;
        //开始尝试遍历填入这个位置的值
        for(int j = down; j <= up; ++j) {
            //dp里的第二个参数是将mask写入,防止重复
            //第三个参数,只有前面都是限制的,这次也到上限才变
            //这次因为赋数了,因此第四个参数为true
            if(((mask >> j) & 1) == 0) ans += dp(i+1, mask|(1<<j),is_limit && (j == up), true, n);
        }
        //做记录
        arr[i][mask][is_limit][is_num] = ans;
        return ans;
    }
};

2 例题

2.1 没有约束条件

【C++】数位DP的模板(找到小于n的数字的每位组成)_第2张图片
这个题没有约束条件,因此可以不使用mask。其实只是需要修改回溯遍历那里,因为那里选择的数字只能是digits数组里的数字,并不是0-9任意选

class Solution {
public:
    //因为n的范围是1e9
    int arr[10][2][2];
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        string n_s = to_string(n);
        return dp(0, true, false, n_s, digits);
    }
    int dp(int i, bool is_limit, bool is_num, string n, vector<string>& digits) {
        //1
        if(i == n.size()) return is_num;
        //2
        if(arr[i][is_limit][is_num] != 0) return arr[i][is_limit][is_num];
        //3
        int ans = 0;
        if(!is_num) ans += dp(i+1, false, false, n, digits);
        //4确定该位的上下限
        int up = 9, down = 0;
        if(is_limit) up = n[i] - '0';
        if(!is_num) down = 1;
        for(auto &ch: digits) {
            //仅这里需要选择,只能选择digit数组里面的数字,并且要保证在上下界里面
            int num = stoi(ch);
            if(num <= up && num >= down)ans += dp(i+1, is_limit && (num == up), true, n, digits);
        }
        arr[i][is_limit][is_num] = ans;
        return ans;
    }
};

2.2 统计数位中的数字个数(多个记录参数,少两个没用的参数)

【C++】数位DP的模板(找到小于n的数字的每位组成)_第3张图片
首先,因为没有约束条件,因此is_num参数(前面有没有0都无所谓)和mask参数是不需要的。
这样,只有两个变量了,一个是统计填数的位置,另一个是is_limit,然而只靠这两个是没办法实现记忆化的,那么就会导致速度大大降低!

由于是统计1的个数,因此我们需要多一个变量来记录1 的个数。这样从后往前递归,就能记住当递归到该i位置时,已有cnt个,最后将有ans个,下一次,再到i时,已有cnt个就不用再往后递归了,因为肯定时之前记录的ans个。

class Solution {
public:
    //此处的32是因为n最多只有31个1
    int arr[10][2][32];
    int countDigitOne(int n) {
        return dp(0, true, 0, to_string(n));
    }
    int dp(int i, bool is_limit, int cnt, string n) {
        //1
        if(i == n.size()) return cnt; 
        //2
        if(arr[i][is_limit][cnt] != 0) return arr[i][is_limit][cnt];
        //4
        int up = 9, ans = 0;
        if(is_limit) up = n[i] - '0';
        for(int j = 0; j <= up; ++j) {
            //如果j是1,则统计1的个数的变量cnt加1
            ans += dp(i+1, is_limit && (j == up), cnt+(j==1), n);
        } 
        arr[i][is_limit][cnt] = ans;
        return ans;
    }
};

你可能感兴趣的:(算法,c++,算法,leetcode)