数位DP

数位DP

TODO

引入

视频讲解:数位dp_哔哩哔哩

什么是数位

数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 09,其他进制可类比十进制。

用来解决什么问题

数位 DP 常用来解决找出一定区间内 满足一定条件 的数,一般是用来计数(不排除求和、求积的可能)

例题

233. 数字 1 的个数 - 力扣

[0,n] 区间内,数码 1 出现了多少次

数据范围:0n109

循环

最常规的做法,遍历每一个数,对该数字出现的数码进行判断,代码如下:

点击查看代码
class Solution {
    int count(int n) {
        int ans = 0;
        while (n != 0) {
            if (n % 10 == 1) ++ans;
            n /= 10;
        }
        return ans;
    }

    public int countDigitOne(int n) {
        int ans = 0;
        for (int i = 0; i <= n; ++i) {
            ans += count(i);
        }
        return ans;
    }
}

对每个数字出现的数码进行统计,时间复杂度 O(i=0nlen(i))=O(i=0nlog10i)=O(nlog n)

根据计算机 1 秒大约处理 107 次运算,会超时

数位搜索

换一种思路,不从 0 枚举到 n

选择按照从高位到低位,每一个数位从 0 枚举到 9,进行 DFS

对于上界为 r=900 的数,最高位枚举 1 时(也就是 1??),该位置这个数码 1,在答案中应该加后面能选择的方案数个,可见后面能选择 099100 种方案,则 ans[1] 应该加 100

但是如何确保我所选择的数字不会大于 n 呢?

我们需要进行判断处理:

  • 若一直是紧贴上界的,则当前位置就只能从 0 枚举到当前位置的上界了
  • 若没有紧贴上界,则当前位置可以从 0 枚举到 9,不受影响

r=789 为上界为例:

最高位可以枚举 07 之间的数

当枚举到 7 时, 第二位就只能枚举 08 了,因为选择 9 会达到 79? 越界了

有的同学可能注意到了,上文说的最高位可以枚举 07,这不对。

确实不对,所以我们需要对 0 的选择进行特判

具体实现如下:

点击查看代码
class Solution {
    int[] LIMIT;
    int ans = 0;
    /**
     * @param now   当前要选择第几位的数字
     * @param limit 是否被限制(是否紧贴上界)
     * @param have  前面是否有数字(是否为前导位置)
     * @return 当前位置的后面能选择的数字总数
     **/
    int dfs(int now, boolean limit, boolean have) {
        if (now == -1) return 1;
        int cnt = 0;
        // 前面没有数字,说明是前导位置,可以选择不填数字
        // 不填数字肯定就不是紧贴上界,下一位置就没有限制
        if (!have) cnt += dfs(now - 1, false, false);
        // 如果有限制,就只能选择该位置的上界值,否则可以选到9
        int high = limit ? LIMIT[now] : 9;
        // 如果前面有数字,则这里可以从0开始填,否则从1开始填
        for (int i = have ? 0 : 1; i < high; ++i) {
            // 不贴上界,肯定不会有限制
            int t = dfs(now - 1, false, true);
            // 如果是1,后面可以选多少种数字,当前位置的1就对答案有多少贡献
            if (i == 1) ans += t;
            cnt += t;
        }
        // 当前选择了最大值,如果现在被限制了,那么下一位置也是被限制了的
        int t = dfs(now - 1, limit, true);
        if (high == 1) ans += t;
        cnt += t;
        return cnt;
    }

    public int countDigitOne(int n) {
        if (n == 0) return 0;
        // 计算 n 的位数
        int len = (int) Math.log10(n) + 1;
        LIMIT = new int[len];
        // 将数字 n 进行拆分,0为最低位
        for (int i = 0; n != 0; n /= 10) LIMIT[i++] = (int) (n % 10);
        dfs(len - 1, true, false);
        return ans;
    }
}

数位DP

由于 遍历了每一个数字的每一位,因此时间复杂度和上述相同,仍为 O(nlog n)

那这有啥区别?我学了个寂寞

我们发现每 10i 个数间,做的事情是相同的,以上界为 r=987 为例:

  • 当计算 1?? 时,后面两位的选择为 0099
  • 当计算 2?? 时,后面两位的选择为 0099
  • 当计算 3?? 时,后面两位的选择为 0099
  • 当计算 4?? 时,后面两位的选择为 0099
  • 当计算 5?? 时,后面两位的选择为 0099
  • 当计算 6?? 时,后面两位的选择为 0099
  • 当计算 7?? 时,后面两位的选择为 0099
  • 当计算 8?? 时,后面两位的选择为 0099

上面的情况都是一样的,只有 0??9?? 不同

0?? 因为是前导 0,第二位不能选择 0,即不能选择 00?

9?? 因为是紧贴上界的,第二位不能选择 9,即不能选择 99?

根据记忆化搜索的想法,选择将普遍、大众的情况记录下来,也就是记录 00991 出现的次数,下次计算到的时候直接返回表中数据即可

当前位置后面所选择的方案数也需要记忆化,不然上面直接退出了,就得不到当前位置的数对答案有多少贡献了

实际上所选择的方案数是 10i,如上述 1??00 99 就是 102

点击查看代码
class Solution {
    // dp0[i] 当前位置的后面能选择的数字总数 
    // dp1[i] 当前位置的后面能选择的数字中1出现的次数
    int[] LIMIT, dp0, dp1;
    /**
     * @param now   当前要选择第几位的数字
     * @param limit 是否被限制(是否紧贴上界)
     * @param have  前面是否有数字(是否为前导位置)
     * @return [0]当前位置的后面能选择的数字总数 
     *         [1]当前位置的后面能选择的数字中1出现的次数
     *         这个1出现的次数一定要传回来,不然会少计算
     **/
    int[] dfs(int now, boolean limit, boolean have) {
        if (now == -1) {
            if (have) return new int[]{1,0};
            return new int[]{0,0};
        }
        // 如果没有限制,也不是前导位置,并且表不是初始值,直接返回
        if (!limit && have && dp0[now] != -1) return new int[]{dp0[now], dp1[now]};

        int[] ans = new int[]{0, 0};
        if (!have) ans = dfs(now - 1, false, false);
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i < high; ++i) {
            int[] t = dfs(now - 1, false, true);
            ans[0] += t[0];
            ans[1] += t[1];
            if (i == 1) ans[1] += t[0];
        }
        int[] t = dfs(now - 1, limit, true);
        ans[0] += t[0];
        ans[1] += t[1];
        if (high == 1) ans[1] += t[0];

        // 如果没被限制,且不是前导位置,则记忆化
        if (!limit && have) {
            dp0[now] = ans[0];
            dp1[now] = ans[1];
        }
        return ans;
    }
    public int countDigitOne(int n) {
        if (n == 0) return 0;
        int len = (int)Math.log10(n) + 1;
        dp0 = new int[len];
        dp1 = new int[len];
        Arrays.fill(dp0, -1);
        LIMIT = new int[len];
        for (int i = 0; n !=0; n/=10) LIMIT[i++] = n % 10;
        int ans = dfs(len - 1, true, false)[1];
        return ans;
    }
}

这就变成了数位 DP

时间复杂度 = 状态个数 × 转移个数

状态个数(dp=len(n)=log(n)

转移个数(循环)= 每个位置取值范围 09

因此时间复杂度为 O(10log(n))

总结

我们通常使用记忆化搜索来实现数位 DP

不止十进制能数位 DP,二进制等也可以如 600. 不含连续1的非负整数 - 力扣

下面是数位 DP 的基本步骤:

  1. 将给出的区间转化为两部分,如区间 [l,r] 可转化为 [0,l1][0,r] 或者 [1,l1][1,r]

    最后调用一个方法,进行去重(一般是减法去重,如 ans[l,r]=ans[0,r]ans[0,l1]

  2. 根据数位从高位向低位枚举(前导 0 是否对结果有影响,若无影响则不需要 have 标记)

  3. 思考 10i 个数间的关系,是否有重复部分

  4. 重复部分进行记忆化

注意

  • 若要记忆化,后续的值一定要回溯带回来,不然记忆化部分会缺少
  • 记忆化只在没有限制的普遍情况才记忆(也就是 !limit && have,若前导 0 无影响,可改为 !limit

题目

面试题 17.06. 2出现的次数 - 力扣

题目链接

例题中的 1 改成 2 就过了

点击查看代码
class Solution {
    // dp0 存数的个数,dp1存2的个数
    int[] LIMIT, dp0, dp1;
    int[] dfs(int now, boolean limit, boolean have) {
        if (now == -1) {
            if (have) return new int[]{1,0};
            return new int[]{0,0};
        }
        if (!limit && have && dp0[now] != -1) return new int[]{dp0[now], dp1[now]};
        int[] ans = new int[]{0, 0};
        if (!have) ans = dfs(now - 1, false, false);
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i <=high; ++i) {
            int[] t = dfs(now - 1, limit && i == high, true);
            ans[0] += t[0];
            ans[1] += t[1];
            if (i == 2) ans[1] += t[0];
        }
        if (!limit && have) {
            dp0[now] = ans[0];
            dp1[now] = ans[1];
        }
        return ans;
    }
    public int numberOf2sInRange(int n) {
           if (n <= 1) return 0;
        int len = (int)Math.log10(n) + 1;
        dp0 = new int[len];
        dp1 = new int[len];
        Arrays.fill(dp0, -1);
        LIMIT = new int[len];
        for (int i = 0; n !=0; n/=10) LIMIT[i++] = n % 10;
        int ans = dfs(len - 1, true, false)[1];
        return ans;
    }
}

600. 不含连续1的非负整数 - 力扣

题目链接

一直都是枚举十进制

这题要求二进制下没有连续的 1

十进制下,每 10k 个数间没有关系

应该枚举二进制

学到了 😪

错解
class Solution {
    int[] LIMIT, dp, POW10;
    int dfs(int now, int mark, boolean limit, boolean have) {
        if (now == -1) {
            if (have) {
                boolean flag = false;
                while (mark != 0) {
                    if ((mark & 1) == 1){
                        if (flag) return 0;
                        flag = true;
                    }else {
                        flag = false;
                    }
                    mark >>= 1;
                }
                return 1;
            }
            return 0;
        }
        if (!limit && have && dp[now] != -1) return dp[now];
        int ans = 0;
        if (!have) ans = dfs(now - 1, 0, false, false);
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i <= high; ++i) {
            ans += dfs(now - 1, mark + i * POW10[now], limit && i == high, true);
            System.out.println(mark + i * POW10[now]);
        }
        if (!limit && have) dp[now] = ans;
        return ans;
    }
    public int findIntegers(int n) {
        if (n == 0) return 1;
        int len = (int)Math.log10(n) + 1;
        dp = new int[len];
        Arrays.fill(dp, -1);
        LIMIT = new int[len];
        for (int i = 0; n != 0; n /= 10) LIMIT[i++] = n % 10;
        POW10 = new int[len];
        POW10[0] = 1;
        for (int i = 1; i < len; ++i) POW10[i] = POW10[i - 1] * 10;
        return dfs(len - 1, 0, true, false) + 1;
    }
}
正解
class Solution {
    char[] LIMIT;
    int[][] dp;
    int len;
    // mark记录当前位置的前面选的1还是0
    // 前导位选0和不选一个性质,不用have标记
    int dfs(int now, boolean mark, boolean limit) {
        if (now >= len) return 1;
        if (!limit && dp[mark ? 1 : 0][now] != -1) return dp[mark ? 1 : 0][now];
        int ans = 0;
        if (limit) {
            if (LIMIT[now] == '1') {
                if (mark) {
                    ans += dfs(now + 1, false, false);
                } else {
                    ans += dfs(now + 1, false, false) + dfs(now + 1, true, true);
                }
            } else {
                ans += dfs(now + 1, false, true);
            }
        } else {
            if (mark) {
                ans += dfs(now + 1, false, false);
            } else {
                ans += dfs(now + 1, false, false) + dfs(now + 1, true, false);
            }
        }
        if (!limit) dp[mark ? 1 : 0][now] = ans;
        return ans;
    }
    public int findIntegers(int n) {
        LIMIT = Integer.toBinaryString(n).toCharArray();
        len = LIMIT.length;
        dp = new int[2][len];
        Arrays.fill(dp[0], -1);
        Arrays.fill(dp[1], -1);
        return dfs(0, false, true);
    }
}

902. 最大为 N 的数字组合 - 力扣

题目链接

枚举的数发生变化,变为枚举给出的数字,而不是原来的 09

不用记忆化是因为每次没有限制的时候是在给出的 digit 中任意选的,方案数已经确定,预处理出来就行

点击查看代码
class Solution {
    int[] digit;
    int[] LIMIT;
    int[] pow;
    boolean zero;
    int len;

    int dfs(int now, boolean limit, boolean have) {
        if (now == -1) return have ? 1 : 0;
        if (!limit && have) return pow[now];
        int ans = 0;
        if (!have) ans = dfs(now - 1, false, false);
        int low = have ? 0 : (zero ? 1 : 0);
        for (int i = low;i < len; ++i) {
            if (limit && digit[i] > LIMIT[now]) break;
            ans += dfs(now - 1, limit && digit[i] == LIMIT[now], true);
        }
        return ans;
    }

    public int atMostNGivenDigitSet(String[] digits, int n) {
        len = digits.length;
        digit = new int[len];
        for (int i = 0; i < len; ++i) digit[i] = Integer.parseInt(digits[i]);
        if (digit[0] == 0) zero = true;
        int k = (int)Math.log10(n) + 1;
        LIMIT = new int[k];
        for (int i = 0; n != 0; n /= 10) LIMIT[i++] = n % 10;
        pow = new int[k];
        pow[0] = len;
        for (int i = 1; i < k; ++i) pow[i] = pow[i - 1] * len;
        return dfs(k - 1, true, false); 
    }
}

1012. 至少有 1 位重复的数字 - 力扣

题目链接

两个思路:

  1. 顺着题意
  2. 逆着题意

顺推

顺着题意就是推出至少有 1 位重复的数字个数

写这题的时候发现,状态与前面位置不重复数字的个数有关

have 不成立(高位数字不取的时候)的时候也可以记忆化

大大降低时间复杂度

获得新思路😀

点击查看代码
class Solution {
    //i位置之前存在重复数字的方案数和pow10有关,因为可以任意选了
    int[] pow;
    //dp[i][j] i位置之前存在j个不重复数字的方案数
    int[][] dp;
    int[] LIMIT;

    // 计算二进制中1的个数
    int oneNum(int n) {
        int ans = 0;
        while (n != 0) {
            n &= n - 1;
            ++ans;
        }
        return ans;
    }
    
    // mask 选择了哪些数字,mark 是否已经有重复数字了
    int dfs(int now, int mask, boolean mark, boolean limit, boolean have) {
        if (now == -1) return mark ? 1 : 0;
        int one = oneNum(mask);
        if (!limit) {
            if (mark) return pow[now + 1];
            if (dp[now][one] != -1) return dp[now][one];
        }
        int ans = 0;
        if (!have) ans = dfs(now - 1, 0, false, false, false);
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i <= high; ++i) {
            ans += dfs(now - 1, mask | (1 << i), mark || (mask >> i & 1) == 1, limit && i == high, true);
        }
        if (!limit) dp[now][one] = ans;
        return ans;
    }

    public int numDupDigitsAtMostN(int n) {
        int len = (int) Math.log10(n) + 1;
        pow = new int[len];
        pow[0] = 1;
        for (int i = 1; i < len; ++i) pow[i] = pow[i - 1] * 10;
        dp = new int[len][10];
        for (int i = 0; i < len; ++i) Arrays.fill(dp[i], -1);
        LIMIT = new int[len];
        for (int i = 0; n != 0; n /= 10) LIMIT[i++] = n % 10;
        return dfs(len - 1, 0, false, true, false);
    }
}

逆推

至少有 1 位重复的数字,反过来也就是用总个数减去 无重复数码的数

那无重复数码的数有啥性质呢?

可以发现,在没有限制的情况下

i 位前面有 k 个不重复的数字的情况,方案数都是相同的

比如,103?????123????? 的无重复数码的方案数是一样的

因此,记忆化 dp(i,k) 表示第 i 位前面有 k 个不重复的数字的方案数

点击查看代码
class Solution {
    // dp[i][k] 表示 i 前面有 k 个不重复数字的方案数
    int[][] dp;
    int[] LIMIT;
    int oneNum(int n) {
        int ans = 0;
        while (n != 0) {
            ++ans;
            n &= n - 1;
        }
        return ans;
    }
    
    // mask 选择了哪些数字
    // 返回无重复数码的数字方案数
    int dfs(int now, int mask, boolean limit, boolean have) {
        if (now == -1) return 1; // 把 0 算上最后单独判断,就可以不用每次都判断了
        int one = oneNum(mask);
        // 由于高位不选时,高位不重复的数字个数不会增加
        // 所以记忆化可以不用管 have 的真假
        if (!limit && dp[now][one] != -1) return dp[now][one];
        int ans = 0;
        if (!have) ans = dfs(now - 1, 0, false, false);
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i <= high; ++i) {
            // 如果已经选过了,就不能选了
            if ((mask >> i & 1) == 1) continue;
            ans += dfs(now - 1, mask | (1 << i), limit && i == high, true);
        }
        if (!limit) dp[now][one] = ans; 
        return ans;
    }

    public int numDupDigitsAtMostN(int n) {
        int len = (int) Math.log10(n) + 1;
        dp = new int[len][len + 1];
        for (int i = 0; i < len; ++i) Arrays.fill(dp[i], -1);
        LIMIT = new int[len];
        for (int i = 0, t = n; t != 0; t /= 10) LIMIT[i++] = t % 10;
        // dfs 的时候 1 也算不重复数字,多减了一个
        return n + 1 - dfs(len - 1, 0, true, false);
    }
}

2376. 统计特殊整数 - 力扣

题目链接

求 无重复数码的数字个数,和 1012. 至少有 1 位重复的数字 - 力扣 逆推思路一致

点击查看代码
class Solution {
    // 统计二进制中1的个数
    int oneNum(int n) {
        int ans = 0;
        while (n != 0) {
            n &= n - 1;
            ++ans;
        }
        return ans;
    }
    int[] LIMIT;
    // dp[now][one]表示now位置,目前选择了one个数字,的总方案
    int[][] dp;
    // mask每一个二进制位对应数字
    int dfs(int now, int mask, boolean limit, boolean have) {
        if (now == -1) return have ? 1 : 0;
        int one = oneNum(mask);
        if (!limit && dp[now][oneNum(mask)] != -1) return dp[now][one];
        int ans = 0;
        if (!have) {
            ans = dfs(now - 1, 0, false, false);
        }
        int high = limit ? LIMIT[now] : 9;
        for (int i = have ? 0 : 1; i <= high; ++i) {
            // 如果选过,则不选该数字
            if ((mask >> i & 1) == 1) continue;
            ans += dfs(now - 1, mask | (1 << i), limit && i == high, true);
        }
        if (!limit) dp[now][one] = ans;
        return ans;
    }
    public int countSpecialNumbers(int n) {
        int len = (int)Math.log10(n) + 1;
        dp = new int[len][10];
        for (int i = 0; i < len ; ++i) Arrays.fill(dp[i], -1);
        LIMIT = new int[len];
        for (int i = 0; n != 0; n /= 10) LIMIT[i++] = n % 10;
        return dfs(len - 1, 0, true, false);
    }
}

10164. 「一本通 5.3 例 2」数字游戏- LibreOJ

题目链接

求区间内不下降的数字

点击查看代码
import java.io.*;
import java.util.Arrays;

public class Main {
    // dp[i][j] 从i到最低位,第i位选j后的不降数个数
    static int[][] dp;
    static int[] num;

    // 选0和不填数字,对结果不影响,所以不用have
    static int dfs(int now, int before, boolean limit) {
        if (now == -1) return 1;
        if (!limit && dp[now][before] != -1) return dp[now][before];
        int ans = 0;
        int high = limit ? num[now] : 9;
        for (int i = before; i <= high; ++i) {
            ans += dfs(now - 1, i, limit && i == high);
        }

        if (!limit) dp[now][before] = ans;
        return ans;
    }

    // 0 到 n 间的不降数个数
    static int solve(int n) {
        if (n == 0) return 1;
        int cnt = 0;
        while (n != 0) {
            num[cnt++] = n % 10;
            n /= 10;
        }
        return dfs(cnt - 1, 0, true);
    }

    static void init() {
        int len = (int) (Math.log10(Integer.MAX_VALUE)) + 1;
        num = new int[len];
        dp = new int[len][10];
        for (int i = 0; i < len; ++i) {
            Arrays.fill(dp[i], -1);
        }
    }

    public static void main(String[] args) throws IOException {
        init();
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        String line;
        while ((line = in.readLine()) != null) {
            String[] s = line.trim().split(" ");
            System.out.println(solve(Integer.parseInt(s[1])) - solve(Integer.parseInt(s[0]) - 1));
        }
        out.close();
    }
}

P2657 windy 数 - 洛谷

题目链接

点击查看代码
import java.io.IOException;
import java.util.Arrays;
import java.util.Scanner;

public class Main {
    // dp[i][j] 从i到最低位,第i位选j后的windy数个数
    static int[][] dp;
    static int[] num;

    // 前导位置的0对结果有影响,因此需要have标记
    static int dfs(int now, int before, boolean limit, boolean have) {
        if (now == -1) return 1;
        if (!limit && have && dp[now][before] != -1) return dp[now][before];

        int ans = 0;
        if (!have) ans = dfs(now - 1, 0, false, false);
        int high = limit ? num[now] : 9;
        for (int i = have ? 0 : 1; i <= high; ++i) {
            if (have && Math.abs(i - before) < 2) continue;
            ans += dfs(now - 1, i, limit && i == high, true);
        }

        if (!limit && have) dp[now][before] = ans;
        return ans;
    }

    // 0 到 n 间的windy个数
    static int solve(int n) {
        if (n == 0) return 1;
        int cnt = 0;
        while (n != 0) {
            num[cnt++] = n % 10;
            n /= 10;
        }
        return dfs(cnt - 1, 0, true, false);
    }

    static void init() {
        int len = (int) (Math.log10(Integer.MAX_VALUE)) + 1;
        num = new int[len];
        dp = new int[len][10];
        for (int i = 0; i < len; ++i) {
            Arrays.fill(dp[i], -1);
        }
    }

    public static void main(String[] args) throws IOException {
        init();
        Scanner sc = new Scanner(System.in);
        int l = sc.nextInt(), r = sc.nextInt();
        System.out.println(solve(r) - solve(l - 1));
    }
}

P2602 数字计数 - 洛谷

题目链接

点击查看代码
import java.io.IOException;
import java.util.Arrays;
import java.util.Scanner;

public class Main {
    // dp[i][j] 从i到最低位,第i位后的有dp[i]][j] 个数码 j,其中dp[i][10]表示方案数
    static long[][] dp;
    static int[] num;

    // 前导位置的0对结果有影响,因此需要have标记
    // 回溯带回来方案数及各个数码个数
    static long[] dfs(int now, boolean limit, boolean have) {
        if (now == -1) {
            long[] ans = new long[11];
            ans[10] = 1;
            return ans;
        }
        if (!limit && have && dp[now][0] != -1) return dp[now];
        long[] ans = new long[11];
        int high = limit ? num[now] : 9;
        for (int i = 0; i <= high; ++i) {
            long[] t = dfs(now - 1, limit && i == high, have || i != 0);
            for (int j = 0; j <= 10; ++j) ans[j] += t[j];
            // 若是前导0,则不计算当前位置
            if (i == 0 && !have) continue;
            ans[i] += t[10];
        }
        if (!limit && have) dp[now] = ans;
        return ans;
    }

    // 1 到 n 间的数码个数及方案数
    static long[] solve(long n) {
        if (n == 0) return new long[11];
        int cnt = 0;
        while (n != 0) {
            num[cnt++] = (int) (n % 10);
            n /= 10;
        }
        return dfs(cnt - 1, true, false);
    }

    static void init() {
        int len = (int) (Math.log10(Long.MAX_VALUE)) + 1;
        num = new int[len];
        dp = new long[len][11];
        for (int i = 0; i < len; ++i) {
            Arrays.fill(dp[i], -1);
        }
    }

    public static void main(String[] args) throws IOException {
        init();
        Scanner sc = new Scanner(System.in);
        long l = sc.nextLong(), r = sc.nextLong();
        long[] ansR = solve(r);
        long[] ansL = solve(l - 1);
        for (int i = 0; i < 10; i++) {
            System.out.print(ansR[i] - ansL[i] + " ");
        }
    }
}

CF1073E. Segment Sum

CF1073E. Segment Sum

题意:求 LR 之间最多不包含 K 个数码的数的和

K10L,R1018

点击查看代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int MOD = 998244353;
i64 POW10[19] = {1};

int k;
// 从0位开始算,如 12 中 1位于1位置,2位于0位置
// dp1[i][j] : 第i位选择了状态为j,当前位置及其后的总方案数
// dp2[i][j] : 第i位选择了状态为j,当前位置及其后的总方案和
vector<vector<i64>> dp1(18, vector<i64>(1 << 10, -1));
vector<vector<i64>> dp2(18, vector<i64>(1 << 10, -1));
// 1. now   : 当前选择第几位
// 2. s     : 上界
// 3. mask  : 当前位置之前,已经用了哪些数码和个数,first记录出现了哪些数码,second记录出现了多少个数码
// 4. limit : 之前数字是否紧贴上界,即当前位置是否有限制
// 5. have  : 前面是否填了数字,即是否为最高位
// 6. 返回值 : 从当前位置到最低位,first方案数,second总方案和
pair<i64, i64> dfs(int now, const string& s, pair<int, int> mask, bool limit, bool have) {
    // 每个位置的数都已经确定
    if (now == -1) return {mask.second <= k, 0};
    // 已经不符合要求了
    if (mask.second > k) return {0, 0};
    // 从当前位置到最低位置,任意选都不会超过k个,则直接返回(499122177是2在模998244353意义下的乘法逆元)
    if (!limit && now + 1 + mask.second <= k) 
        return {(POW10[now + 1] - !have) % MOD, (POW10[now + 1] - 1) % MOD * POW10[now + 1] % MOD * 499122177 % MOD};
    if (!limit && have && dp1[now][mask.first] != -1) return {dp1[now][mask.first], dp2[now][mask.first]};
    i64 cnt = 0, sum = 0;
    // 是最高位,则可以跳过
    if (!have) {
        auto t = dfs(now - 1, s, mask, false, false);
        cnt = (cnt + t.first) % MOD, sum = t.second % MOD;
    }
    int low = have ? 0 : 1;
    int high = limit ? s[now] - '0' : 9;
    for (int i = high; i >= low; --i) {
        int a = mask.first | (1 << i);
        int b = mask.first >> i & 1 ? mask.second : mask.second + 1;
        auto t = dfs(now - 1, s, {a, b}, limit && i == high, true);
        cnt = (cnt + t.first) % MOD;
        sum = (sum + i * POW10[now] % MOD * t.first % MOD + t.second) % MOD;
    }
    if (!limit && have) {
        dp1[now][mask.first] = cnt;
        dp2[now][mask.first] = sum;
    }
    return {cnt, sum};
}
int main() {
    for (int i = 1; i < 19; ++i) POW10[i] = POW10[i - 1] * 10L;
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    string r;
    i64 _l;
    cin >> _l >> r >> k;
    string l = to_string(_l - 1);
    reverse(l.begin(), l.end());
    reverse(r.begin(), r.end());
    i64 ans1 = dfs(l.length() - 1, l, {0, 0}, true, false).second;
    i64 ans2 = dfs(r.length() - 1, r, {0, 0}, true, false).second;
    cout << (ans2 - ans1 + MOD) % MOD << endl;
}

二进制问题

题目链接

点击查看代码
import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static long n;
    static int k, cnt = 0;
    static int[] LIMIT = new int[64];
    static long[][] dp;

    // [i,cnt) 位出现了 j 个 1, 后面有 dp[i][j] 中选择
    static long dfs(int now, int number1, boolean limit) {
        if (number1 > k) return 0;
        if (now == -1) return number1 == k ? 1 : 0;
        if (!limit && dp[now][number1] != -1) return dp[now][number1];
        // 选 0 的情况
        long ans = dfs(now - 1, number1, limit && LIMIT[now] == 0);
        // 选 1 的情况
        if (!limit || LIMIT[now] == 1) ans += dfs(now - 1, number1 + 1, limit && LIMIT[now] == 1);
        if (!limit) dp[now][number1] = ans;
        return ans;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextLong();
        k = sc.nextInt();
        for (long t = n; t != 0; t >>= 1) LIMIT[cnt++] = (int) (t & 1);
        dp = new long[cnt][k + 1];
        for (int i = 0; i < cnt; ++i) Arrays.fill(dp[i], -1);
        // 从最高位开始选
        System.out.println(dfs(cnt - 1, 0, true));
    }
}
posted @   Cattle_Horse  阅读(62)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示