数位DP

数位DP

TODO

引入

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

什么是数位

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

用来解决什么问题

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

例题

233. 数字 1 的个数 - 力扣

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

数据范围:\(0\le n\le 10^{9}\)

循环

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

点击查看代码
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(\sum_{i=0}^nlen(i))=O(\sum_{i=0}^{n}log_{10}i)=O(nlog\ n)\)

根据计算机 \(1\) 秒大约处理 \(10^7\) 次运算,会超时

数位搜索

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

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

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

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

我们需要进行判断处理:

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

\(r=789\) 为上界为例:

最高位可以枚举 \(0\sim 7\) 之间的数

当枚举到 \(7\) 时, 第二位就只能枚举 \(0\sim 8\) 了,因为选择 \(9\) 会达到 \(79?\) 越界了

有的同学可能注意到了,上文说的最高位可以枚举 \(0\sim 7\),这不对。

确实不对,所以我们需要对 \(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)\)

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

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

  • 当计算 \(1??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(2??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(3??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(4??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(5??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(6??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(7??\) 时,后面两位的选择为 \(00\sim 99\)
  • 当计算 \(8??\) 时,后面两位的选择为 \(00\sim 99\)

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

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

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

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

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

实际上所选择的方案数是 \(10^i\),如上述 \(1??\)\(00~\sim 99\) 就是 \(10^2\)

点击查看代码
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\)

时间复杂度 \(=\) 状态个数 \(\times\) 转移个数

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

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

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

总结

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

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

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

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

    最后调用一个方法,进行去重(一般是减法去重,如 \(ans_{[l,r]}=ans_{[0,r]}-ans_{[0,l-1]}\)

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

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

  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\)

十进制下,每 \(10^k\) 个数间没有关系

应该枚举二进制

学到了 😪

错解
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 的数字组合 - 力扣

题目链接

枚举的数发生变化,变为枚举给出的数字,而不是原来的 \(0\sim 9\)

不用记忆化是因为每次没有限制的时候是在给出的 \(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

题意:求 \(L\sim R\) 之间最多不包含 \(K\) 个数码的数的和

\(K\le10\)\(L,R\le10^{18}\)

点击查看代码
#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 @ 2023-02-02 17:38  Cattle_Horse  阅读(60)  评论(0编辑  收藏  举报