数位DP
数位DP
TODO
引入
视频讲解:数位dp_哔哩哔哩
什么是数位
数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 \(0\sim9\),其他进制可类比十进制。
用来解决什么问题
数位 \(DP\) 常用来解决找出一定区间内 满足一定条件 的数,一般是用来计数(不排除求和、求积的可能)
例题
求 \([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\) 的基本步骤:
-
将给出的区间转化为两部分,如区间 \([l,r]\) 可转化为 \([0,l-1]\) 和 \([0,r]\) 或者 \([1,l-1]\) 和 \([1,r]\)
最后调用一个方法,进行去重(一般是减法去重,如 \(ans_{[l,r]}=ans_{[0,r]}-ans_{[0,l-1]}\))
-
根据数位从高位向低位枚举(前导 \(0\) 是否对结果有影响,若无影响则不需要 \(have\) 标记)
-
思考 \(10^i\) 个数间的关系,是否有重复部分
-
对重复部分进行记忆化
注意:
- 若要记忆化,后续的值一定要回溯带回来,不然记忆化部分会缺少
- 记忆化只在没有限制的普遍情况才记忆(也就是
!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\) 位重复的数字个数
写这题的时候发现,状态与前面位置不重复数字的个数有关
\(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
题意:求 \(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));
}
}