数位动态规划问题
数位动态规划问题
今天,我们要给大家介绍的是数位动态规划。数位动态规划一般用来回答类似这样的问题:
请问区间 [l, r] 中有多少个满足某个条件的数。其中 l 和 r 的范围一般很大。
比如说我们有这样一个题:
数位和
请问 0 到 9 这些数字在 \([l, r](1≤l≤r≤10^{16})\) 的数位中分别出现了多少次。
当 l = 112,r = 115 时,每个数里面都有两个 1,所以 1 一共出现了 2 * 4 = 8 次;2、3、4、5 分别出现在其中的一个数里,所以它们都出现了 1 次。
我们来分析下这个问题,借用前缀和的思想,我们可以知道,某个 x (0 ≤ x ≤ 9) 在区间 [l, r] 中出现了多少次,等价于 x 在区间 [1, r] 中出现了多少次 - x 在区间 [1, l - 1] 中出现了多少次。
现在问题变成了对于某个 n,我们要统计 0 到 9 在区间 [1, n] 中分别出现了多少次(在考虑区间 [l, r] 时,我们要考虑区间 [1, r] 和 [1, l - 1])。
为了计算 0 到 9 在区间 [1, n] 中分别出现了多少次,我们把小于 n 的数字 s 按照以下方法进行分类:如果 s 从左到右数第一个小于 n 对应位置的数位是第 i 位,那么 s 就会被分到第 i 类。
当 n = 12345 时,小于 n 的数字会被分成 5 类:
- 第 1 类(第 1 位不同):[1, 9999] (如果 s 的位数比 n 少,那么我们会在前面补 0,在这个例子中,1 会被补全成 00001)
- 第 2 类(第 2 位不同):[10000, 11999]
- 第 3 类(第 3 位不同):[12000, 12299]
- 第 4 类(第 4 位不同):[12300, 12339]
- 第 5 类(第 5 位不同):[12340, 12344]
对于第 i 类数,数字 s 的前 i - 1 位和 n 是一样的,s 的第 i 位要小于 n 的第 i 位,后面那些位置想是几就是几(在上面的例子中,对于第 3 类数,前两位 12 是固定的,第 3 位可以是 0、1、2(比 3 小),第 4 位第 5 位都可以是 0 到 9 中任意的值)。
如果我们能够统计出 0 到 9 在每一类中分别出现了几次,把它们加起来我们就知道了 0 到 9 在区间 [1, n) 中出现了几次。统计 0 到 9 在 n 中出现的次数是简单的,于是我们也就知道了 0 到 9 在区间 [1, n] 中出现了几次啦!
我们继续往后思考,某个 x(0 ≤ x ≤ 9) 在一段区间中出现了多少次怎么算呢?x 是不是有可能出现在个位,也有可能出现在十位,也有可能出现在其他任何一个位置。我们把 x 在每个位置出现的次数加起来,是不是就是 x 出现的总次数呀?x 在某一个位置出现了多少次,是不是等价于这一个位置等于 x 的数字有多少个呀?于是问题变成了计算区间内每个位置等于 x 的数字的个数的和。
我们继续看上面例子中的第 3 类([12000, 12299]),我们来看看每个位置的情况:
- 第 1 位和第 2 位:这两位是固定的,这一类中所有数字(共 300 个)的第 1 位都是 1,第 2 位都是 2,所以 1 和 2 出现的次数都要加上 300;
- 第 3 位:第 3 位要比 12345 的第 3 位小,所以可以是 0、1、2,分别有 100 个数字第 3 位是 0 ([12000, 12099])、1 ([12100, 12199])、2 ([12200, 12299]);
- 第 4 位和第 5 位:对于后面两个位置,每个位置上都可以是 0 到 9 之间任意的数字;对于 x(0 ≤ x ≤ 9),第 4 位上是 x 的数字有 30 个(12zxy,第 3 位 z 可以是 0、1、2,第 5 位 y 可以是 0 到 9 之间任意的数字),同理第 5 位上是 x 的数字也有 30 个。
接下来我们来看更一般的情况。
当 \(n = a_1a_2...a_m\)n = a_1a_2...a_m 时( \(a_i\)a_i 表示 n 从左到右第 i 个数位上的值,其中 m 表示 n 是一个 m 位数),考虑第 i 类数,我们来看看每个位置的情况:
- 对于满足 j < i 的位置 j(i 左边的位置),有 \(a_i * 10^{m-i}\)a_i * 10^{m-i} (\([a_1a_2...a_{i-1} \times 10^{m-i+1},a_1a_2...a_i \times 10^{m-i}-1]\) [a_1a_2...a_{i-1} \times 10^{m-i+1},a_1a_2...a_i \times 10^{m-i}-1] ,第 i 个位置可以是 0 到 \(a_i-1\)a_i-1 ,后面每个位置都可以是 0 到 9,后面一共有 m - i 个位置,所以一共有这么多个数字)个数字第 j 位是 \(a_j\)a_j ;
- 对于位置 i,可以是 0 到 \(a_i-1\)a_i-1,对于 \(x(0\le x\le a_i-1)\)x(0\le x\le a_i-1) ,第 i 位是 x 的数字有 $10{m-i}$10 个( \([a_1a_2...a_{i-1}x \times 10^{m-i},a_1a_2...(x+1) \times 10^{m-i}-1]\) [a_1a_2...a_{i-1}x \times 10^{m-i},a_1a_2...(x+1) \times 10^{m-i}-1] ;
- 对于满足 j > i 的位置 j(i 右边的位置),对于 x(0 ≤ x ≤ 9) ,有 \(a_i * 10^{m-i-1}\)a_i * 10^{m-i-1} 个数字第 j 位是 x(第 i 个位置可以是 0 到 \(a_i-1\)a_i-1,i 右边除了 j 还有 m - i - 1 个位置,每个位置都可以是 0 到 9);满足 j > i 的位置 j 一共有 m - i 个(每个位置是 x 的数字个数都是一样的),所以 x 出现的次数要加上 \(a_i * 10^{m-i-1}*(m-i)\)a_i * 10^{m-i-1}*(m-i)。
于是,我们只要依次枚举每一类,统计下 0 到 9 出现的次数就可以啦!这算法还有没有问题呢?
我们仔细想想,按照刚刚的分类方法,第 1 类(例子中的区间是 [1, 9999])是特殊的,按刚才的算法会把前导 0 算进去。如果一个数字的位数不足 m,刚才的算法会在前面补 0,这些 0 是不能被统计的。这怎么解决呢?
第 1 类中 0 出现的次数需要被单独统计。我们可以枚举第一个非零位在哪里,在第一个非零位后面的 0 就都要被考虑啦!有两种情况:
- 假如第一个非零位是第 1 位,满足 j > 1 的位置 j 是 0 的数字有 \((a_1 - 1) * 10^{m-2}\)(a_1 - 1) * 10^{m-2} 个(第 1 位可以是 1 到 \(a_1 - 1\)a_1 - 1,后面除了 j 还有 m - 2 个位置,每个位置上都可以是 0 到 9);在第 1 位后面共有 m - 1 个位置,所以 0 的出现次数要加上 \((a_1 - 1) * 10^{m-2}*(m-1)\)(a_1 - 1) * 10^{m-2}*(m-1);
- 假如第一个非零位是第 i (i> 1) 位,满足 j > i 的位置 j 是 0 的数字有 $9 * 10^{m-i-1}$9 * 10^{m-i-1} 个(由于第 1 位是 0,无论后面的数字是多少,整个数字都会比 n 小。第 i 位可以是 1 到 9,后面除了 j 还有 m - i - 1 个位置,每个位置上都可以是 0 到 9);在第 i 位后面共有 m - i 个位置,所以 0 的出现次数要加上 $9 * 10^{m-i-1}(m-i)$9 * 10^{m-i-1}(m-i)。
至此,我们就把所有情况都考虑完啦!
#include <bits/stdc++.h>
using namespace std;
int a[21];
long long l, r, ans[10], f[17];
inline void calc(long long n, int xs) {
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
for (int i = 1; i <= m; i++) {
for (int j = 1; j < i; j++)
ans[a[j]] += xs * a[i] * f[m - i];
for (int j = 1; j < a[i]; j++)
ans[j] += xs * f[m - i];
if (i != 1 && a[i])
ans[0] += xs * f[m - i];
if (m != i) {
for (int j = 1; j < 10; j++)
ans[j] += xs * f[m - i - 1] * (m - i) * a[i];
if (i != 1)
ans[0] += xs * f[m - i - 1] * (m - i) * a[i];
}
if (i == 1) {
if (m >= 2)
ans[0] += xs * (a[1] - 1) * (m - 1) * f[m - 2];
for (int j = 2; j < m; j++)
ans[0] += xs * 9 * (m - j) * f[m - j - 1];
}
}
for (int i = 1; i <= m; i++)
ans[a[i]] += xs;
}
int main() {
f[0] = 1;
for (int i = 1; i <= 16; i++)
f[i] = f[i - 1] * 10;
scanf("%lld%lld", &l, &r);
calc(r, 1);
calc(l - 1, -1);
for (int i = 0; i < 10; i++)
printf("%lld ", ans[i]);
printf("\n");
}
让我们来看看代码!
f[0] = 1;
for (int i = 1; i <= 16; i++)
f[i] = f[i - 1] * 10;
f 数组用来记录 10 的幂次等于几,f[i] 等于 10 的 i 次方。
inline void calc(long long n, int xs) {
calc 函数用来计算 0 到 9 在区间 [1, n] 中出现的次数,xs = 1 表示这些出现次数是要加的,xs = -1 表示这些出现次数是要减的,后面的计算中我们只要乘上系数就可以处理加减的情况了。
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
这段代码用来把 n 从左到右的数位一个个放入 a 数组中,由于一开始 n 在 a 数组中是从右往左记的,所以我们要把它前后翻转一下。
for (int i = 1; i <= m; i++) {
从 1 到 m 枚举我们要处理的是哪一类数。
for (int j = 1; j < i; j++)
ans[a[j]] += xs * a[i] * f[m - i];
计算满足 j < i 的位置 j(i 左边的位置)对答案的贡献。
for (int j = 1; j < a[i]; j++)
ans[j] += xs * f[m - i];
if (i != 1 && a[i])
ans[0] += xs * f[m - i];
计算位置 i 对答案的贡献。如果现在考虑的是第 1 类,我们到后面去算 0 出现的次数。
if (m != i) {
for (int j = 1; j < 10; j++)
ans[j] += xs * f[m - i - 1] * (m - i) * a[i];
if (i != 1)
ans[0] += xs * f[m - i - 1] * (m - i) * a[i];
}
计算满足 j > i 的位置 j(i 右边的位置)对答案的贡献。和计算位置 i 对答案的贡献时一样,如果现在考虑的是第 1 类,我们到后面去算 0 出现的次数。
if (i == 1) {
if (m >= 2)
ans[0] += xs * (a[1] - 1) * (m - 1) * f[m - 2];
for (int j = 2; j < m; j++)
ans[0] += xs * 9 * (m - j) * f[m - j - 1];
}
计算第 1 类中 0 出现的次数,我们的做法是枚举第一个非零位在哪里,分第一个非零位是不是第 1 位两种情况讨论。
for (int i = 1; i <= m; i++)
ans[a[i]] += xs;
前面的代码统计了 0 到 9 在区间 [1, n) 中出现的次数,我们再把 n 考虑进去(数组 a 记录了 n 的每个数位),就知道了 0 到 9 在区间 [1, n] 中出现的次数啦!
scanf("%lld%lld", &l, &r);
calc(r, 1);
calc(l - 1, -1);
读入 l,r 后,我们调用 calc 函数,我们拿 0 到 9 在区间 [1, r] 中出现的次数减去在区间 [1, l - 1] 中出现的次数即为在区间 [l, r] 中出现的次数。
for (int i = 0; i < 10; i++)
printf("%lld ", ans[i]);
printf("\n");
最后输出答案就可以啦!
我们再来看一个题!
数数
请问 \([l, r](1≤l≤r≤10^{16})\)l, r 中有多少个数字满足数字中任意相邻两个数位的差的绝对值不超过 2。
比如说,124 满足上述条件,125 却不满足,因为 2 和 5 的差的绝对值等于 3,超过了 2。
解决这个题目的核心思想和 “数位和” 是一样的,首先我们也要做一次前缀和,区间 [l, r] 中有多少满足条件的数字等于区间 [1, r] 中有多少满足条件的数字 - 区间 [1, l - 1] 中有多少满足条件的数字。
对于某个 n,我们想知道区间 [1, n] 中有多少满足条件的数字。
对于区间 [1, n) 中的数字 s, 我们把它们按照以下方法进行分类:如果 s 从左到右数第一个小于 n 对应位置的数位是第 i 位,那么 s 就会被分到第 i 类。
只要我们能求出每一类中有多少满足条件的数字,把它们加起来就是区间 [1, n) 中有多少满足条件的数字。然后再判断下 n 满不满足条件,我们就顺利求出了区间 [1, n] 中有多少满足条件的数字。
现在我们来看如何求每一类中有多少满足条件的数字。当 \(n = a_1a_2...a_m\) 时,考虑第 i 类的数,这些数字的前 i - 1 位和 n 的前 i - 1 位是一样的。紧接着,我们可以枚举第 i 位是几(第 i 位可以是 0 到 \(a_{i-1}\) 中的数字),后面那些位置(满足 j > i 的位置 j)想放什么就可以放什么。
于是我们就可以用动态规划解决这个问题啦!
在具体介绍如何解决这个问题前,我们先讨论下在动态规划中两种不同的转移策略:
- 第一种策略是枚举状态 y,为了计算状态 y 的值 f[y],我们需要枚举从哪些状态 x 可以转移到 y,然后用这些 x 的值更新 f[y]。比如说,如果一个状态的值等于所有能转移到它的状态的值的和,有 \(f(y)=\sum_{x\in pre(y)}f(x)\) ,pre(y) 表示所有能转移到状态 y 的状态的集合。这也是在之前的文章中我们采用的写法;
- 第二种策略是枚举状态 x,这时我们已经算完了状态 x 的值 f[x](能转移到状态 x 的状态都需要在 x 之前被枚举到),我们需要枚举 x 能转移到哪些状态 y,然后用 x 的值更新 y 的值。在上面的例子中,对于所有 x 能转移到的状态 y,执行 f[y] += f[x] (状态 y 的值在原来的基础上加上状态 x 的值)。
大家可以仔细思索一番,这两种策略本质上是一样的,在实际操作过程中,第二种策略往往比第一种策略容易实现,原因是很多时候要想清楚一个状态可以转移到哪些状态是简单的,反过来要想清楚一个状态可以从哪些状态转移过来可能会很难。
现在我们运用第二种策略来解决这个问题。
为了统计有多少满足条件的数字,我们从左到右一位位考虑每个位置上的数字是几,在考虑第 i 位的时候(假设第 i 位后面的位置都不存在),这个位置上可以放几只和第 i - 1 位上是几有关(它们的差的绝对值需要小于等于 2),所以我们只要记当前考虑的最后一个位置上是几就可以啦,前面的位置上是几都不用记!在第 i - 1 位是 0 的时候还有一种特殊情况,有可能前 i - 1 位全是 0,那么第 i 位是几都是可以的(前面的前导 0 其实都不存在)!所以在设计状态的时候,我们还需要把前面的位置是不是全是 0 记下来。
于是,我们用 \(f[i][j][0...1]\) 表示考虑了前 i 个位置,第 i 位上是 j,前 i 位是否全是 0(0 表示全是 0,1 表示不全是)的情况下有多少满足条件的数字(这些数字都只有 i 位,假设第 i 位后面的位置都不存在)。
假设我们考虑的是第 l 类的数,由于前 l - 1 位上的数字是固定的,第 l 位上的数我们也枚举了,所以我们只需要用动态规划处理后面 m - l 位放几的问题。
让我们来看看怎么从考虑了前 i 位的状态转移到考虑了前 i + 1 位的状态,按照上面的分析,分为最后一维是 0 是 1 两种情况。
如果最后一维是 1,也就是 \(f[i][j][1]\) 的情况,意味着前 i 位不全是 0,那么第 i 位的数字是需要被考虑的,我们可以枚举第 i + 1 位的值 k,如果满足 |j - k| ≤ 2,那么 \(f[i][j][1]\) 可以转移到 \(f[i + 1][k][1]\),我们执行 \(f[i + 1][k][1] += f[i][j][1]\)。
如果最后一维是 0,也就是 \(f[i][0][0]\) 的情况,意味着前 i 位全是 0(第 i 位也必须是 0),我们有两种选择:
- 在第 i + 1 位上放 0,现在前 i + 1 位全是 0,所以状态的最后一维仍然是 0,这时 \(f[i][0][0]\) 可以转移到 \(f[i + 1][0][0]\),我们执行 \(f[i + 1][0][0] += f[i][0][0]\);
- 第 i + 1 位不是 0,由于前 i 位都是 0,所以这一位想放几就可以放几,于是我们可以从 1 到 9 枚举 k,这时 \(f[i][0][0]\) 可以转移到 \(f[i + 1][k][1]\),我们执行 \(f[i + 1][k][1] += f[i][0][0]\)。
最后,我们考虑完了所有 m 个位置,当前这类数中满足条件的数字个数为 \(\sum_{x=0}^9f[m][x][1]\) 。由于最后一位可以是 0 到 9 之间的任意数字,所以我们需要枚举最后一位是几。由于我们要统计的是大于等于 1 的满足条件的数字,所以状态的最后一维是 1(如果最后一维是 0,意味着数字的每一位都是 0,于是数字就等于 0 啦)。
#include <bits/stdc++.h>
using namespace std;
int a[21];
long long l, r, f[21][10][2];
long long calc(long long n) {
if (!n)
return 0;
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
long long res = 0;
bool ok = true;
for (int i = 1; i <= m && ok; i++) {
for (int j = 0; j < a[i]; j++) {
if (i != 1 && abs(j - a[i - 1]) > 2)
continue;
memset(f, 0, sizeof(f));
if (i == 1 && !j)
f[i][j][0] = 1;
else
f[i][j][1] = 1;
for (int k = i + 1; k <= m; k++)
for (int l = 0; l < 10; l++)
for (int r = 0; r < 2; r++)
if (f[k - 1][l][r])
for (int x = 0; x < 10; x++) {
if (r && abs(l - x) <= 2)
f[k][x][r] += f[k - 1][l][r];
if (!r)
if (!x)
f[k][0][0] += f[k - 1][0][0];
else
f[k][x][1] += f[k - 1][0][0];
}
for (int j = 0; j < 10; j++)
res += f[m][j][1];
}
if (i != 1 && abs(a[i] - a[i - 1]) > 2)
ok = false;
}
if (ok)
++res;
return res;
}
int main() {
scanf("%lld%lld", &l, &r);
printf("%lld\n", calc(r) - calc(l - 1));
}
我们来看看代码!
long long calc(long long n) {
calc 函数计算的区间 [1, n] 中有多少个满足条件的数字。
if (!n)
return 0;
如果 n = 0,满足条件的数字个数也等于 0。
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
a 数组中记录的是 n 的每一个位置上是几。
long long res = 0;
bool ok = true;
res 用来统计满足条件的数字个数,ok 记录的是考虑到第 i 类数的时候,前 i - 1 个固定的位置是不是满足条件(任意相邻两个数位的差的绝对值不超过 2)。
for (int i = 1; i <= m && ok; i++) {
for (int j = 0; j < a[i]; j++) {
if (i != 1 && abs(j - a[i - 1]) > 2)
continue;
我们枚举每一个类,在处理第 i 类的时候,我们枚举第 i 位上是几,并且也要判断第 i - 1 位上的数字和这个枚举到的数字的差的绝对值是不是小于等于 2。
memset(f, 0, sizeof(f));
if (i == 1 && !j)
f[i][j][0] = 1;
else
f[i][j][1] = 1;
设置动态规划的初始状态,如果现在考虑的是第 1 类并且第 1 个位置上的数字是 0,则状态的最后一维等于 0,否则最后一维等于 1。
for (int k = i + 1; k <= m; k++)
for (int l = 0; l < 10; l++)
for (int r = 0; r < 2; r++)
if (f[k - 1][l][r])
for (int x = 0; x < 10; x++) {
if (r && abs(l - x) <= 2)
f[k][x][r] += f[k - 1][l][r];
if (!r)
if (!x)
f[k][0][0] += f[k - 1][0][0];
else
f[k][x][1] += f[k - 1][0][0];
}
现在我们要用考虑了前 k - 1 个位置的状态推出考虑了前 k 个位置的状态,这时我们要枚举第 k 位上的数是几,对应的进行转移。
for (int j = 0; j < 10; j++)
res += f[m][j][1];
紧接着,我们把第 i 类中满足条件的数字的个数加入 res 中。
if (i != 1 && abs(a[i] - a[i - 1]) > 2)
ok = false;
考虑完第 i 类后,第 i 位上是几就被固定了,我们需要检查第 i - 1 位和第 i 位上的数字是否满足条件,如果不满足的话,后面的类就都不用考虑啦!
if (ok)
++res;
之前统计了区间 [1, n) 中的数,如果 n 满足条件,则 res 还要加 1。当考虑完所有的类之后,ok 其实就记录了 n 满不满足条件。
scanf("%lld%lld", &l, &r);
printf("%lld\n", calc(r) - calc(l - 1));
区间 [l, r] 的答案等于区间 [1, r] 的答案 - 区间 [1, l - 1] 的答案。
在这个做法中,我们需要从 1 到 m 枚举每一类数。对于每一类,我们需要枚举后面的每个位置,它的时间复杂度是 \(O(m^2)\) 的。
这就是区间动态规划啦!
我们总结下区间动态规划的通用做法。
**首先我们要做一次前缀和,区间 [l, r] 的答案等于区间 [1, r] 的答案 - 区间 [1, l - 1] 的答案。对于某个 n,现在我们想知道区间 [1, n] 的答案。然后我们要对区间 [1, n) 分类,对于区间 [1, n) 中的数字 s, 我们把它们按照以下方法进行分类:如果 s 从左到右数第一个小于 n 对应位置的数位是第 i 位,那么 s 就会被分到第 i 类。接着我们要计算每一类的答案是多少,把它们加起来就是区间 [1, n) 的答案。然后再加入 n 对于答案的贡献,我们就顺利求出了区间 [1, n] 的答案。为了算出第 i 类的答案,一般我们会枚举第 i 位上是几,由于前 i - 1 位上的数字是固定的,而 i 后面的位置上想放几就可以放几,对于后面的位置,我们用动态规划处理就行啦
再回到题目,我们有没有更快的做法呢?
答案是肯定的,考虑上面这个做法,在针对每一类进行动态规划的时候,有些计算其实是重复的(如果 n = 12345 ,第 2 类中考虑了前 3 个位置时的 111 和 第 3 类中考虑了前 3 个位置时的 121 对于后面的位置是一样的,因为它们的最后一位都是 1)。
我们该怎么做,才能不进行这些重复计算呢?考虑完前 i 个位置以后,第 i + 1 个位置上可以放几除了和第 i 位是几有关,还有另一件事情能够影响它——当前考虑的数字的前 i 位和 n 的前 i 位是否相等,如果相等的话,第 i + 1 位上只可以放 0 到 \(a_{i+1}\) 中的数(否则整个数就比 n 大了,注意我们想求的是区间 [1, n] 的答案)。如果不相等,也就是说当前考虑的数字的前 i 位比 n 的前 i 位小,第 i + 1 位上想放几就可以放几。于是我们就可以动态规划啦!
现在我们要在动态规划的状态中多加一维。我们用 \(f[i][j][0...1][0...1]\) 表示考虑了前 i 个位置,第 i 位上是 j,前 i 位是否全是 0(0 表示全是 0,1 表示不全是),这个数字的前 i 位和 n 的前 i 位是否相等(0 表示不相等,1 表示相等)的情况下有多少满足条件的数字。
让我们来看看怎么从考虑了前 i 位的状态转移到考虑了前 i + 1 位的状态,状态的前三维如何处理前面已经介绍过了,让我们来看看最后一维的情况:
-
如果当前状态的最后一维是 0,第 i + 1 位想放几就放几;
-
如果当前状态的最后一维是 1,我们有两种选择:
-
第 i + 1 位上放 0 到 \(a_{i+1} - 1\)中的数,最后一维变成了 0;
-
第 i + 1 位上放 \(a_{i+1}\),最后一维仍然是 1;
最后答案等于 \(\sum_{x=0}^9(f[m][x][1][0]+f[m][x][1][1])\)。最后一维等于 0,表示的是考虑的数字比 n 小的情况(考虑的数字的前 m 位比 n 的前 m 位小,由于数字一共只有 m 位,意味着考虑的数字比 n 小);最后一维等于 1,表示的是考虑的数字等于 n 情况。于是,我们就算出了区间 [1, n] 中有多少满足条件的数字。
有了这最后一维,我们从左到右做一遍动态规划就可以搞定啦!现在为什么不用枚举每一类了呢?是因为在之前枚举每一类分别进行动态规划的做法中,其实我们想保证的是在动态规划的过程里,考虑的所有数字都要比 n 小,于是后面每个位置都可以想放几就放几而不用考虑其他的限制。现在通过最后一维,我们也实现了考虑的所有数字都要小于等于 n 的目的!
#include <bits/stdc++.h>
using namespace std;
int a[21];
long long l, r, f[21][10][2][2];
long long calc(long long n) {
if (!n)
return 0;
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
long long res = 0;
memset(f, 0, sizeof(f));
f[0][0][0][1] = 1;
for (int i = 1; i <= m; i++)
for (int j = 0; j < 10; j++)
for (int k = 0; k < 2; k++)
for (int l = 0; l < 2; l++)
if (f[i - 1][j][k][l])
for (int x = 0; x < 10; x++) {
if (l && x > a[i])
continue;
if (l) {
if (x < a[i])
if (!k) {
if (!x)
f[i][0][0][0] += f[i - 1][j][k][l];
else
f[i][x][1][0] += f[i - 1][j][k][l];
} else {
if (abs(j - x) <= 2)
f[i][x][1][0] += f[i - 1][j][k][l];
}
else
if (!k)
f[i][x][1][1] += f[i - 1][j][k][l];
else
if (abs(j - x) <= 2)
f[i][x][1][1] += f[i - 1][j][k][l];
} else {
if (!k) {
if (!x)
f[i][0][0][0] += f[i - 1][j][k][l];
else
f[i][x][1][0] += f[i - 1][j][k][l];
} else
if (abs(j - x) <= 2)
f[i][x][1][0] += f[i - 1][j][k][l];
}
}
for (int i = 0; i < 10; i++)
res += f[m][i][1][0] + f[m][i][1][1];
return res;
}
int main() {
scanf("%lld%lld", &l, &r);
printf("%lld\n", calc(r) - calc(l - 1));
}
代码的核心部分和之前是类似的,大家可以研究下转移中和之前不一样的地方,在此我们就不展开啦!
memset(f, 0, sizeof(f));
f[0][0][0][1] = 1;
需要注意下初始状态,一开始我们一个位置都没有考虑(考虑了前 0 个位置),这其实等价于我们把位置变多(增大 m)以后前面全是前导 0 的情况,所以第 2 维和第 3 维我们都可以设置成 0。由于现在一个位置都没有考虑,所有数字都是一样的,所以最后一维为 1。
现在我们只需要用 i 从左到右枚举每一个位置就可以啦,时间复杂度降到了 O(m)。
基本所有的区间动态规划问题都可以用这个方法优化掉一个 m 的时间复杂度,具体的做法是在状态中加入一维表示考虑了前 i 位以后,这个数字的前 i 位和 n 的前 i 位是否相等,然后我们就知道了第 i + 1 位可以放什么数字。
让我们再来看一个题。
数数 2
请问 \([l, r](1≤l≤r≤10^{16})\) l, r 中有多少个数字满足数字的各个数位的和是质数。
例如,124 是一个满足条件的数字,因为它的各个数位的和等于 7,而 7 是质数。125 不是一个满足条件的数字,因为它的各个数位的和等于 8,而 8 不是质数。
我们用刚刚学到的解决数位动态规划问题的一般方法来解决这个问题。
前缀和以及分类的思路跟之前是一模一样的,由于篇幅原因,在这里就省略了。当 \(n = a_1a_2...a_m\)n = a_1a_2...a_m 时,区间 [1, n) 的数被分为了 m 类。
对于第 l 类数,前 l - 1 个位置的值都是固定的,我们从 0 到 \(a_l-1\)a_l-1 枚举第 l 个位置上的数字是几,l 以后的位置上想放几就可以放几,于是我们可以用动态规划解决后面的位置的情况。
我们用 f[i][j] 表示考虑了前 i 个位置,前 i 位上的数的和是 j 的数字有多少个。考虑 f[i][j] 可以转移到哪些状态,我们只要从 0 到 9 枚举第 i + 1 位上的数字 k,就可以转移到状态 f[i + 1][j + k] 啦!于是我们执行 f[i + 1][j + k] += f[i][j]。
最后统计答案的时候,只要判断下所有数位的和是不是质数就好啦!我们把满足 j 是质数的 f[m][j] 加起来,就是当前考虑的这一类的答案!
#include <bits/stdc++.h>
using namespace std;
long long l, r, f[18][201];
bool b[201];
int a[21];
long long calc(long long n) {
if (!n)
return 0;
int m = 0;
for (; n; n /= 10)
a[++m] = n % 10;
for (int i = 1, j = m; i < j; i++, j--)
swap(a[i], a[j]);
long long res = 0;
int sum = 0;
for (int i = 1; i <= m; i++) {
for (int j = 0; j < a[i]; j++) {
memset(f, 0, sizeof(f));
f[i][sum + j] = 1;
for (int k = i + 1; k <= m; k++)
for (int l = 0; l <= (k - 1) * 9; l++)
if (f[k - 1][l])
for (int x = 0; x < 10; x++)
f[k][x + l] += f[k - 1][l];
for (int j = 2; j <= 9 * m; j++)
if (!b[j])
res += f[m][j];
}
sum += a[i];
}
if (!b[sum])
++res;
return res;
}
int main() {
memset(b, false, sizeof(b));
b[1] = true;
for (int i = 2; i <= 200; i++)
if (!b[i])
for (int j = i + i; j <= 200; j += i)
b[j] = true;
scanf("%lld%lld", &l, &r);
printf("%lld\n", calc(r) - calc(l - 1));
}
让我们来看看代码!
memset(b, false, sizeof(b));
b[1] = true;
for (int i = 2; i <= 200; i++)
if (!b[i])
for (int j = i + i; j <= 200; j += i)
b[j] = true;
b 数组用来记录每个数字是不是质数,b[i] = false 表示 i 是质数,b[i] = true 表示 i 是合数。预处理 b 数组用的是一种时间复杂度为 O(nlogn) 的方法,我们从小到大枚举 i,如果 i 是质数(不存在比它小的除 1 以外的因数),那么我们就把不等于 i 的 i 的倍数都标记为合数。至于这种方法的时间复杂度为什么是 O(nlogn) 的,大家可以学习一下调和级数的概念。这里我们记录 200 以内的数是不是质数就够了,原因是 l, r 的最大值是 10 的 16 次方,这是一个 17 位数,每个位置上最大的数字是 9,所以数位和最大也不会超过 200。
long long calc(long long n) {
calc 函数用来计算区间 [1, n] 里有多少满足条件的数字。
long long res = 0;
int sum = 0;
res 记录的是满足条件的数字个数。在我们处理第 i 类数时,sum 中记录的是前 i - 1 个位置上的数位和。
for (int i = 1; i <= m; i++) {
for (int j = 0; j < a[i]; j++) {
memset(f, 0, sizeof(f));
f[i][sum + j] = 1;
我们从 1 到 m 枚举第 i 类数并且从 0 到 \(a_i - 1\)a_i - 1 枚举第 i 个位置上的数 j。并且设置动态规划的初始状态,这时前 i - 1 位的数位和等于 sum,第 i 个位置上是 j,所以前 i 个位置的数位和等于 sum + j。
for (int k = i + 1; k <= m; k++)
for (int l = 0; l <= (k - 1) * 9; l++)
if (f[k - 1][l])
for (int x = 0; x < 10; x++)
f[k][x + l] += f[k - 1][l];
为了从考虑了前 k - 1 位的状态转移到考虑了前 k 位的状态,我们只要从 0 到 9 枚举第 k 位上的值 x 就好啦!
for (int j = 2; j <= 9 * m; j++)
if (!b[j])
res += f[m][j];
对于所有质数 j,res 要加上 f[m][j]。因为当前考虑的数字一共有 m 个位置,每个位置上最大可以是 9,所以 j 最大可以是 9 * m。
if (!b[sum])
++res;
如果 n 的数位和是质数,res 还要再加 1。
printf("%lld\n", calc(r) - calc(l - 1));
最后答案等于 calc(r) - calc(l - 1)。
对于第 i 类数,状态中的第二维是 O(m) 的,我们要枚举 i 后面的每一位,这部分的时间复杂度也是 O(m) 的,所以处理第 i 类数的时间复杂度为 \(O(m^2)\)O(m^2)。由于我们一共要处理 m 类数,所以这个做法的时间复杂度是 \(O(m^3)\)O(m^3) 的。
有同学可能要问了,前两个题我们花了好大的力气处理前导 0,这题为什么就不用处理了呢?原因是这样的,在这个题目中,前导 0 不会对数位和的值产生影响,所以也就不用处理了。
我们也可以用 " 数数 “中优化复杂度的方法优化掉一个 m,有兴趣的读者可以尝试尝试。
最后还有一个题留给大家思考。
数数 3
请问 \([l, r](1≤l≤r≤10^{16})\)l, r 中有多少个数字 a 满足数字中存在 3 个连续的数位 \(a_i, a_{i+1}, a_{i+2}\)a_i, a_{i+1}, a_{i+2} 使得 \(a_i < a_{i+1} < a_{i+2}\)a_i <a_{i+1} < a_{i+2} ,其中 \(a_i\)a_i 表示 a 从左到右数第 i 位上的值。
数位动态规划问题,完结撒花!

浙公网安备 33010602011771号