算法【数位dp】

数位dp的尝试方式并不特殊,绝大多数都是线性展开,类似从左往右的尝试。之前的文章已经讲过大量在数组上进行线性展开的题目,数位dp是在数字的每一位上进行线性展开而已。不同的题目有不同的限制,解题核心在于:可能性的整理、排列组合的相关知识。

解决数位dp的问题推荐使用记忆化搜索的方式,可能性的展开会很好写,不必刻意追求进一步改写,递归写出来问题就解决了,位数多就挂缓存,位数不多甚至不挂缓存也能通过。

下面通过题目加深理解。

题目一

测试链接:357. 统计各位数字都不同的数字个数 - 力扣(LeetCode)

分析:这道题的思路还是比较简单的,对于1位有0到9,10种取法;对于2位第1位因为不能取0,所以有9种取法,第二位也有9种取法;对于3位第1位有9种取法,第2位有9种取法,第3位有8种取法。以此类推可以根据数据量做出一个表进行查表。代码如下。

class Solution {
public:
    int res[9] = {
        1,
        10,
        10+9*9,
        10+9*9+9*9*8,
        10+9*9+9*9*8+9*9*8*7,
        10+9*9+9*9*8+9*9*8*7+9*9*8*7*6,
        10+9*9+9*9*8+9*9*8*7+9*9*8*7*6+9*9*8*7*6*5,
        10+9*9+9*9*8+9*9*8*7+9*9*8*7*6+9*9*8*7*6*5+9*9*8*7*6*5*4,
        10+9*9+9*9*8+9*9*8*7+9*9*8*7*6+9*9*8*7*6*5+9*9*8*7*6*5*4+9*9*8*7*6*5*4*3
    };
    int countNumbersWithUniqueDigits(int n) {
        return res[n];
    }
};

题目二

测试链接:902. 最大为 N 的数字组合 - 力扣(LeetCode)

分析:首先确定数字的位数,然后从第1位开始到最后一位进行递归,对于第i位的可能性展开分为i位之前是否取过数字以及来到第i位时是否比数字小。如果之前没有取过数字,则可以对第i位也不取数字,继续往下递归。如果第i位之前和数字相等,那么对于可以取的数字,如果小于当前位则递归时需要注明小于数字;如果取的数字和当前位相同,则注明等于当前数字。如果已经小于当前数则对于所有的数字都可以取。下面是直接递归的版本,当然也可以挂记忆化搜索,不过这道题纯递归已经可以过了。代码如下。

class Solution {
public:
    int digit[9];
    int f(int len, int length, int less, int used, int n, int offset){
        if(len == 0){
            return used == 0 ? 0 : 1;
        }
        int ans = 0;
        if(used == 0){
            ans += f(len-1, length, 1, 0, n, offset/10);
        }
        int cur = (n / offset) % 10;
        if(less == 0){
            for(int i = 0;i < length;++i){
                if(digit[i] < cur){
                    ans += f(len-1, length, 1, 1, n, offset/10);
                }else if(digit[i] == cur){
                    ans += f(len-1, length, 0, 1, n, offset/10);
                    break;
                }else if(digit[i] > cur){
                    break;
                }
            }
        }else{
            ans += (length * f(len-1, length, 1, 1, n, offset/10));
        }
        return ans;
    }
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        int length = digits.size();
        int len = 1;
        int offset = 1;
        int temp = n;
        temp /= 10;
        while (temp)
        {
            ++len;
            offset *= 10;
            temp /= 10; 
        }
        for(int i = 0;i < length;++i){
            digit[i] = digits[i][0] - '0';
        }
        return f(len, length, 0, 0, n, offset);
    }
};

其中,f方法返回在还有len位,总位数为length,less代表是否小于数字,used代表是否取过数字的情况下满足条件的个数。

注意到,如果已经小于数字,那后面的种数就是可以取的数字进行次方计算,因此可以查表优化。代码如下。

class Solution {
public:
    int digit[9];
    int table[10];
    int f(int len, int length, int less, int used, int n, int offset){
        if(len == 0){
            return used == 0 ? 0 : 1;
        }
        int ans = 0;
        if(used == 0){
            ans += f(len-1, length, 1, 0, n, offset/10);
        }
        int cur = (n / offset) % 10;
        if(less == 0){
            for(int i = 0;i < length;++i){
                if(digit[i] < cur){
                    ans += table[len-1];
                }else if(digit[i] == cur){
                    ans += f(len-1, length, 0, 1, n, offset/10);
                    break;
                }else if(digit[i] > cur){
                    break;
                }
            }
        }else{
            ans += table[len];
        }
        return ans;
    }
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        int length = digits.size();
        int len = 1;
        int offset = 1;
        int temp = n;
        temp /= 10;
        while (temp)
        {
            ++len;
            offset *= 10;
            temp /= 10; 
        }
        for(int i = 0;i < length;++i){
            digit[i] = digits[i][0] - '0';
        }
        table[0] = 1;
        for(int i = 1;i <= 9;++i){
            table[i] = table[i-1] * length;
        }
        return f(len, length, 0, 0, n, offset);
    }
};

题目三

测试链接:2719. 统计整数数目 - 力扣(LeetCode)

分析:这道题可以简化成从0到num2这个区间的好整数减去从0到num1-1这个区间的好整数,但是因为数字是用字符串形式给的,所以可以减去0到num1这个区间的好整数,然后判断num1是否为好整数。对于好整数的判断依旧是得到数的位数,以及来到第i位时前面位的累加和和是否小于数字进行可能性展开。下面是记忆化搜索的版本。

class Solution {
public:
    int MOD = 1000000007;
    int dp[24][401][2];
    int f(string num, int min_num, int max_num){
        int len = num.size();
        for(int i = 0;i <= len;++i){
            for(int j = 0;j <= max_num;++j){
                for(int k = 0;k < 2;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
        return ff(len, num, 0, min_num, max_num, 0);
    }
    int ff(int len, string num, int sum, int min_num, int max_num, int less){
        if(sum + len * 9 < min_num){
            return 0;
        }
        if(dp[len][sum][less] != -1){
            return dp[len][sum][less];
        }
        if(len == 0){
            if(sum >= min_num){
                dp[len][sum][less] = 1;
                return 1;
            }else{
                dp[len][sum][less] = 0;
                return 0;
            }
        }
        int ans = 0;
        int cur = num[num.size()-len] - '0';
        if(less == 0){
            for(int i = 0;i < cur && sum + i <= max_num;++i){
                ans = (int)(((long long)ans + ff(len-1, num, sum+i, min_num, max_num, 1)) % MOD);
            }
            if(sum + cur <= max_num){
                ans = (int)(((long long)ans + ff(len-1, num, sum+cur, min_num, max_num, 0)) % MOD);
            }
        }else{
            for(int i = 0;i < 10 && sum + i <= max_num;++i){
                ans = (int)(((long long)ans + ff(len-1, num, sum+i, min_num, max_num, 1)) % MOD);
            }
        }
        dp[len][sum][less] = ans;
        return ans;
    }
    int num1Is(string num, int min_num, int max_num){
        int sum = 0;
        for(int i = 0;i < num.size();++i){
            sum += (num[i] - '0');
        }
        return sum >= min_num && sum <= max_num ? 1 : 0;
    }
    int count(string num1, string num2, int min_sum, int max_sum) {
        return (f(num2, min_sum, max_sum) - f(num1, min_sum, max_sum) + num1Is(num1, min_sum, max_sum) + MOD) % MOD;
    }
};

其中,f方法返回0到num满足条件的好整数个数;ff方法返回在还有len位,前面位累加和为sum,和数字大小关系为less的情况下满足条件的个数。

题目四

测试链接:2376. 统计特殊整数 - 力扣(LeetCode)

分析:首先得到这个n的位数,用一个数组存储起来,然后对于小于n的位数的数直接查表累加起来,然后再计算等于n的位数的满足条件的数有多少个。然后因为第1位和后面的位不一样,第1位不能取0,后面的位可以取0,所以对于第1位可以进行单独计算。即如果第1位取得数比数字的第1位小,那么后面位数就可以随便取;如果取的数和数字的第1位相同,那么进行标记过后,往下递归。代码如下。

class Solution {
public:
    int table1[10] = {
        1,
        9,
        9*9,
        9*9*8,
        9*9*8*7,
        9*9*8*7*6,
        9*9*8*7*6*5,
        9*9*8*7*6*5*4,
        9*9*8*7*6*5*4*3,
        9*9*8*7*6*5*4*3*2
    };
    int table2[10] = {9, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    int bit[10];
    int bit_len;
    bool used[10] = {false};
    int f(int len){
        int ans = 0;
        int buff = 1;
        for(int i = bit_len-len+1;i < bit_len;++i){
            buff *= table2[i];
        }
        if(bit[0] > 1){
            ans += (bit[0]-1) * buff;
        }
        used[bit[0]] = true;
        ans += ff(len-1);
        return ans;
    }
    int ff(int len){
        if(len == 0){
            return 1;
        }
        int ans = 0;
        int buff = 1;
        for(int i = bit_len-len+1;i < bit_len;++i){
            buff *= table2[i];
        }
        for(int i = 0;i < bit[bit_len-len];++i){
            if(!used[i]){
                ans += buff;
            }
        }
        if(!used[bit[bit_len-len]]){
            used[bit[bit_len-len]] = true;
            ans += ff(len-1);
        }
        return ans;
    }
    int countSpecialNumbers(int n) {
        int len = 0;
        int offset = 1;
        int temp = n;
        bit[len++] = temp % 10;
        temp /= 10;
        while (temp)
        {
            bit[len++] = temp % 10;
            offset *= 10;
            temp /= 10;
        }
        bit_len = len;
        for(int i = 0;i < bit_len/2;++i){
            temp = bit[i];
            bit[i] = bit[bit_len-1-i];
            bit[bit_len-1-i] = temp;
        }
        int ans = 0;
        for(int i = 1;i < len;++i){
            ans += table1[i];
        }
        return ans + f(len);
    }
};

其中,f方法是在单独算第1位;ff方法是对后面的位进行计算。

题目五

测试链接:1012. 至少有 1 位重复的数字 - 力扣(LeetCode)

分析:这道题和上道题是一样的,上道题是不存在重复的位,这道题就是将所有个数减去上道题的个数就是至少有一位重复数字的个数。代码如下。

class Solution {
public:
    int table1[10] = {
        1,
        9,
        9*9,
        9*9*8,
        9*9*8*7,
        9*9*8*7*6,
        9*9*8*7*6*5,
        9*9*8*7*6*5*4,
        9*9*8*7*6*5*4*3,
        9*9*8*7*6*5*4*3*2
    };
    int table2[10] = {9, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    int bit[10];
    int bit_len;
    bool used[10] = {false};
    int f(int len){
        int ans = 0;
        int buff = 1;
        for(int i = bit_len-len+1;i < bit_len;++i){
            buff *= table2[i];
        }
        if(bit[0] > 1){
            ans += (bit[0]-1) * buff;
        }
        used[bit[0]] = true;
        ans += ff(len-1);
        return ans;
    }
    int ff(int len){
        if(len == 0){
            return 1;
        }
        int ans = 0;
        int buff = 1;
        for(int i = bit_len-len+1;i < bit_len;++i){
            buff *= table2[i];
        }
        for(int i = 0;i < bit[bit_len-len];++i){
            if(!used[i]){
                ans += buff;
            }
        }
        if(!used[bit[bit_len-len]]){
            used[bit[bit_len-len]] = true;
            ans += ff(len-1);
        }
        return ans;
    }
    int numDupDigitsAtMostN(int n) {
        int len = 0;
        int offset = 1;
        int temp = n;
        bit[len++] = temp % 10;
        temp /= 10;
        while (temp)
        {
            bit[len++] = temp % 10;
            offset *= 10;
            temp /= 10;
        }
        bit_len = len;
        for(int i = 0;i < bit_len/2;++i){
            temp = bit[i];
            bit[i] = bit[bit_len-1-i];
            bit[bit_len-1-i] = temp;
        }
        int ans = 0;
        for(int i = 1;i < len;++i){
            ans += table1[i];
        }
        return n - ans - f(len);
    }
};

题目六

测试链接:[SCOI2009] windy 数 - 洛谷

分析:首先也是将其转化成从0到b之间的windy数减去从0到a-1之间的windy数就是a到b之间的windy数。可能性的展开就是,如果没有取数依旧可以不取数,向下一位递归。如果之前位和数字相等,那么如果没取过数字,这一位可以随便取,但是不能超过数字的当前位;如果取过数字,则需注意对于数字的选择,需要满足条件。如果已经小于数字,对于没取过时,如果还剩一位,则有9种,如果比一位多,则可以随便取;如果取过数字,则遍历0到9,选择满足条件的数字进行递归。下面是记忆化搜索的版本,代码如下。

#include <iostream>
using namespace std;
int a, b;
int dp[11][2][2][11];
int ff(int len, int num, int offset, int less, int used, int last){
    if(dp[len][less][used][last] != -1){
        return dp[len][less][used][last];
    }
    if(len == 0){
        dp[len][less][used][last] = used == 0 ? 0 : 1;
        return dp[len][less][used][last];
    }
    int ans = 0;
    int cur = (num / offset) % 10;
    if(used == 0){
        ans += ff(len-1, num, offset/10, 1, 0, -1);
    }
    if(less == 0){
        if(used == 0){
            for(int i = 1;i < cur;++i){
                ans += ff(len-1, num, offset/10, 1, 1, i);
            }
            ans += ff(len-1, num, offset/10, 0, 1, cur);
        }else{
            for(int i = 0;i < cur;++i){
                if(abs(i - last) >= 2){
                    ans += ff(len-1, num, offset/10, 1, 1, i);
                }
            }
            if(abs(cur - last) >= 2){
                ans += ff(len-1, num, offset/10, 0, 1, cur);
            }
        }
    }else{
        if(used == 0){
            if(len == 1){
                ans += 9;
            }else{
                for(int i = 1;i <= 9;++i){
                    ans += ff(len-1, num, offset/10, 1, 1, i);
                }
            }
        }else{
            for(int i = 0;i <= 9;++i){
                if(abs(i - last) >= 2){
                    ans += ff(len-1, num, offset/10, 1, 1, i);
                }
            }
        }
    }
    dp[len][less][used][last] = ans;
    return ans;
}
int f(int num){
    if(num == 0){
        return 0;
    }
    int len = 1;
    int offset = 1;
    int temp = num / 10;
    while (temp)
    {
        ++len;
        offset *= 10;
        temp /= 10;
    }
    if(len == 1){
        return num;
    }
    for(int i = 0;i <= len;++i){
        for(int j = 0;j < 2;++j){
            for(int k = 0;k < 2;++k){
                for(int l = 0;l <= 10;++l){
                    dp[i][j][k][l] = -1;
                }
            }
        }
    }
    return ff(len, num, offset, 0, 0, 10); 
}
int main(void){
    scanf("%d%d", &a, &b);
    printf("%d", f(b)-f(a-1));
    return 0;
}

题目七

测试链接:https://www.luogu.com.cn/problem/P3413

分析:依旧是先简化题目。这道题可以反向求出不是萌数的个数,然后再求出萌数的个数。注意到如果一个数不是萌数,那么它的第i位一定和i-1位和i-2位不一样。这就是可能性的展开。如果没有取数,当前位仍然可以不取。如果之前位和数字相等,对于没有取数的情况,可以不需要考虑条件;如果取过,则需要考虑和前两位的相等关系。如果小于当前数,对于没有取过的情况,可以从1到9随便取;如果取过数,则需要考虑和前两位的相等关系。下面是记忆化搜索的版本,代码如下。

#include <iostream>
using namespace std;
int MOD = 1000000007;
int dp[1001][11][11][2];
int sLen[2];
int sLen_index = 0;
int ff(int len, char s[], int p, int pp, int less, int length){
    if(dp[len][p][pp][less] != -1){
        return dp[len][p][pp][less];
    }
    if(len == 0){
        dp[len][p][pp][less] = p == 10 && pp == 10 ? 0 : 1;
        return dp[len][p][pp][less];
    }
    int ans = 0;
    int cur = s[length-len] - '0';
    if(p == 10 && pp == 10){
        ans = (int)(((long long)ans + ff(len-1, s, 10, 10, 1, length)) % MOD);
    }
    if(less == 0){
        if(p == 10 && pp == 10){
            for(int i = 1;i < cur;++i){
                ans = (int)(((long long)ans + ff(len-1, s, i, 10, 1, length)) % MOD);
            }
            ans = (int)(((long long)ans + ff(len-1, s, cur, 10, 0, length)) % MOD);
        }else{
            for(int i = 0;i < cur;++i){
                if(i != p && i != pp){
                    ans = (int)(((long long)ans + ff(len-1, s, i, p, 1, length)) % MOD);
                }
            }
            if(cur != p && cur != pp){
                ans = (int)(((long long)ans + ff(len-1, s, cur, p, 0, length)) % MOD);
            }
        }
    }else{
        if(p == 10 && pp == 10){
            for(int i = 1;i < 10;++i){
                ans = (int)(((long long)ans + ff(len-1, s, i, 10, 1, length)) % MOD);
            }
        }else{
            for(int i = 0;i < 10;++i){
                if(i != p && i != pp){
                    ans = (int)(((long long)ans + ff(len-1, s, i, p, 1, length)) % MOD);
                }
            }
        }
    }
    dp[len][p][pp][less] = ans;
    return ans;
}
int f(char s[]){
    if(s[0] == '0'){
        return 0;
    }
    int len = 0;
    while (s[len] != '\0')
    {
        ++len;
    }
    sLen[sLen_index++] = len;
    long long all = 0;
    long long base = 1;
    for(int i = len-1;i >= 0;--i){
        all = (all + (s[i]-'0') * base) % MOD;
        base = (base * 10) % MOD;
    }
    for(int i = 0;i <= len;++i){
        for(int j = 0;j <= 10;++j){
            for(int k = 0;k <= 10;++k){
                for(int l = 0;l < 2;++l){
                    dp[i][j][k][l] = -1;
                }
            }
        }
    }
    return (int)((all - ff(len, s, 10, 10, 0, len) + MOD) % MOD);
}
int lIs(char s[]){
    if(sLen[1] == 1){
        return 0;
    }
    if(sLen[1] == 2){
        return s[0] == s[1] ? 1 : 0;
    }
    if(s[0] == s[1]){
        return 1;
    }
    for(int i = 2;i < sLen[1];++i){
        if(s[i] == s[i-1] || s[i] == s[i-2]){
            return 1;
        }
    }
    return 0;
}
int main(void){
    char l[1000];
    char r[1000];
    scanf("%s%s", &l, &r);
    printf("%d", (int)(((long long)f(r) - f(l) + MOD + lIs(l)) % MOD));
    return 0;    
}

其中,ff函数没用到used是因为p和pp共同可以起到used的作用。

题目八

测试链接:600. 不含连续1的非负整数 - 力扣(LeetCode)

分析:这道题先将n化为二进制用数组存储起来。可能性的展开就是是否小于n以及上一位的数字。如果之前位和n相等,那么对于n的当前位为0和1进行分情况讨论递归。如果已经小于n,那么不需要管n的当前位,只需要考虑之前位是否取1。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[33][2][2];
    int bit[32];
    int f(int len, int less, int last){
        if(dp[len][less][last] != -1){
            return dp[len][less][last];
        }
        if(len == 0){
            dp[len][less][last] = 1;
            return 1;
        }
        int ans = 0;
        int cur = bit[32-len];
        if(less == 0){
            if(cur == 0){
                ans += f(len-1, 0, 0);
            }else{
                if(last == 1){
                    ans += f(len-1, 1, 0);
                }else{
                    ans += f(len-1, 1, 0);
                    ans += f(len-1, 0, 1);
                }
            }
        }else{
            if(last == 1){
                ans += f(len-1, 1, 0);
            }else{
                ans += f(len-1, 1, 1);
                ans += f(len-1, 1, 0);
            }
        }
        dp[len][less][last] = ans;
        return ans;
    }
    int findIntegers(int n) {
        for(int i = 0;i < 32;++i){
            bit[31-i] = (n & (1 << i));
        }
        for(int i = 0;i <= 32;++i){
            for(int j = 0;j < 2;++j){
                for(int k = 0;k < 2;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
        return f(32, 0, 0);
    }
};

其中,f方法返回在还剩len位,和n的大小情况为less,上一位取lsat的情况下满足条件的个数。

题目九

测试链接:[ZJOI2010] 数字计数 - 洛谷

分析:依旧是先简化题目即1到b上出现的次数减去1到a-1上出现的次数。这道题是对0到9进行统计,那么可以拆分为对不同的数字进行统计,然后从0到9遍历。对一个数字进行统计时,可以从低位到高位统计,在每一位时有多少种。当数字不为0时,有三种情况,即当前位大于等于小于数字。对于每种情况去计算在每一位时左边和右边的种数;当数字为0时,只有两种情况即大于等于,此时要注意左边的个种数是排除全为0的情况。代码如下。

#include <iostream>
using namespace std;
long long ans[10] = {0};
void f(long long num, int flag){
    if(num == 0){
        return;
    }
    if(num < 10){
        for(int i = 1;i <= num;++i){
            ans[i] += flag;
        }
        return;
    }
    long long right_part1 = 1;
    long long right_part2 = 0;
    long long left_part = num / 10;
    int cur;
    while (left_part != 0)
    {
        cur = (int)((num / right_part1) % 10);
        if(cur == 0){
            ans[0] += flag * ((left_part - 1) * right_part1 + (right_part2 + 1));
        }else{
            ans[0] += flag * (left_part * right_part1);
        }
        left_part /= 10;
        right_part1 *= 10;
        right_part2 = num % right_part1;
    }
    for(int i = 1;i < 10;++i){
        right_part1 = 1;
        right_part2 = 0;
        left_part = num / 10;
        while (right_part2 != num)
        {
            cur = (int)((num / right_part1) % 10);
            if(cur < i){
                ans[i] += flag * (left_part * right_part1);
            }else if(cur > i){
                ans[i] += flag * ((left_part + 1) * right_part1);
            }else{
                ans[i] += flag * (left_part * right_part1 + right_part2 + 1);
            }
            left_part /= 10;
            right_part1 *= 10;
            right_part2 = num % right_part1;
        }
    }
}
int main(void){
    long long a, b;
    scanf("%ld%ld", &a, &b);
    f(b, 1);
    f(a-1, -1);
    for(int i = 0;i < 9;++i){
        printf("%ld ", ans[i]);
    }
    printf("%ld", ans[9]);
    return 0;
}

其中,f函数因为0的特殊性,是先计算0的情况,再从1到9进行遍历。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

还有糕手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值