数位DP经典例题
例题1:度的数量
题意
求给定区间\([X,Y]\)中满足下列条件的整数个数:这个数恰好等于\(K\)个互不相等的\(B\)的整数次幂之和。
例如,设\(X=15, Y=20, K=2, B=2\),则有且仅有下列三个数满足题意:
- \(17 = 2^{4} + 2^{0}\)
- \(18 = 2^{4} + 2^{1}\)
- \(20 = 2^{4} + 2^{2}\)
题目链接:https://www.acwing.com/problem/content/1083/
数据范围
\(1 \leq X \leq Y \leq 2^{31} − 1\)
\(1 \leq K \leq 20\)
\(2 \leq B \leq 10\)
思路
首先,按照数位DP惯用套路,将问题转化为\(g(Y) - g(X-1)\),其中\(g(x)\)表示前\(x\)个数中满足要求的数字个数。
然后,我们可以将\(x\)转化为\(B\)进制,这样通过统计\(1\)的个数就能快速判断是否满足要求。
令\(f(i,j)\)表示如果正在枚举第\(i\)位且已经有\(j\)个\(1\),答案有多少个(有limit的单算)。则\(f(i,j) = \sum\limits_{t=0}^{up} f(i - 1,j+t)\)
由于记忆化搜索版数位DP时间复杂度较高,因此考虑剪枝:在枚举的过程中,如果数量超过了\(K\),则不继续搜索。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 35;
int l, r, k, b;
int f[N][N];
//f(i, j)表示如果正在枚举第i位且已经有j个b的整数次幂,答案有多少个
//最终答案为f(p - 1, 0)
int a[N];
int dfs(int p, int num, bool limit) //limit为1表示当前位上有限制,为0表示无限制
{
if(p < 0) return num == k; //枚举完最后一位,判断是否为答案
if(!limit && f[p][num] != -1) return f[p][num];
int up = limit ? min(a[p], 1) : 1; //当前位的选择上限
int ans = 0;
for(int i = 0; i <= up; i ++) {
if(num + i > k) continue;
ans += dfs(p - 1, num + i, limit && i == a[p]);
}
if(!limit) f[p][num] = ans;
return ans;
}
int solve(int x)
{
int p = 0;
while(x) {
a[p ++] = x % b;
x /= b;
}
memset(f, -1, sizeof f);
return dfs(p - 1, 0, 1);
}
int main()
{
cin >> l >> r >> k >> b;
printf("%d\n", solve(r) - solve(l - 1));
return 0;
}
例题2:数字游戏
题意
某人命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如\(123, 446\)。
现在大家决定玩一个游戏,指定一个整数闭区间\([a,b]\),问这个区间内有多少个不降数。
题目链接:https://www.acwing.com/problem/content/1084/
数据范围
\(1 \leq a \leq b \leq 2^{31}−1\)
思路
在搜索的时候记录一下上一位是多少即可。
令\(f(i,j)\)表示如果正在枚举第\(i\)位且上一位是\(j\),答案有多少个(有limit的单算)。则\(f(i,j) = \sum\limits_{t=j+1}^{up} f(i - 1,t)\)
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int l, r;
int f[N][N];
int a[N];
int dfs(int p, int pre, bool limit)
{
if(p < 0) return 1;
if(!limit && f[p][pre] != -1) return f[p][pre];
int up = limit ? a[p] : 9;
int ans = 0;
for(int i = pre; i <= up; i ++) {
ans += dfs(p - 1, i, limit && i == a[p]);
}
if(!limit) f[p][pre] = ans;
return ans;
}
int solve(int x)
{
int p = 0;
while(x) {
a[p ++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(p - 1, 0, 1);
}
int main()
{
while(~scanf("%d%d", &l, &r)) printf("%d\n", solve(r) - solve(l - 1));
return 0;
}
例题3:Windy数
题意
Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为\(2\)的正整数被称为 Windy 数。
Windy 想知道,在\(A\)和\(B\)之间,包括\(A\)和\(B\),总共有多少个 Windy 数?
题目链接:https://www.acwing.com/problem/content/1085/
数据范围
\(1 \leq A \leq B \leq 2 \times 10^9\)
思路
这道题需要考虑前导零。考虑前导零的意思就是,前导零部分不需要满足题目中的限制条件。
与limit处理方法类似,是否是前导零用一个标记lead表示,然后单独去讨论前导零的情况即可。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 15;
char L[N], R[N];
int a[N];
ll f[N][N];
ll dfs(int p, int pre, bool lead, bool limit)
{
if(p < 0) return 1;
if(!limit && !lead && f[p][pre] != -1) return f[p][pre];
int up = limit ? a[p] : 9;
ll ans = 0;
for(int i = 0; i <= up; i ++) {
if(lead) { //如果为前导零
if(!i) ans += dfs(p - 1, 0, 1, limit && i == a[p]); //如果当前填0
else ans += dfs(p - 1, i, 0, limit && i == a[p]); //如果当前不填0
}
else { //不是前导零
if(abs(i - pre) >= 2) ans += dfs(p - 1, i, 0, limit && i == a[p]);
}
}
if(!limit && !lead) f[p][pre] = ans;
return ans;
}
ll solve(string s)
{
int p = 0;
int len = s.size();
for(int i = len - 1; i >= 0; i --) a[p ++] = s[i] - '0';
memset(f, -1, sizeof f);
return dfs(p - 1, 0, 1, 1);
}
bool check(string s)
{
int len = s.size();
for(int i = 1; i < len; i ++) {
int a = s[i] - '0', b = s[i - 1] - '0';
if(abs(a - b) < 2) return false;
}
return true;
}
int main()
{
cin >> L >> R;
printf("%lld", solve(R) - solve(L) + check(L));
return 0;
}
例题4:数字游戏II
题意
某人又命名了一种取模数,这种数字必须满足各位数字之和\(\mod N\)为\(0\)。
现在大家又要玩游戏了,指定一个整数闭区间\([a.b]\),问这个区间内有多少个取模数。
题目链接:https://www.acwing.com/problem/content/1086/
数据范围
\(1 \leq a,b \leq 2^{31}−1\)
\(1 \leq N < 100\)
思路
这道题就是就是经典的计数DP拓展到了数位DP。
令\(f(i,j)\)表示当前枚举到第\(i\)位且模数为\(j\)的情况下,方案数为多少。\(f(i, j) = \sum\limits_{t=0}^{up} f(i-1, (j+t)\% N)\)
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15, M = 110;
int l, r, n;
int a[N];
int f[N][M];
int dfs(int p, int num, bool limit)
{
if(p < 0) return !num;
if(!limit && f[p][num] != -1) return f[p][num];
int up = limit ? a[p] : 9;
int ans = 0;
for(int i = 0; i <= up; i ++) {
ans += dfs(p - 1, (num + i) % n, limit && i == a[p]);
}
if(!limit) f[p][num] = ans;
return ans;
}
int solve(int x)
{
int p = 0;
while(x) {
a[p ++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(p - 1, 0, 1);
}
int main()
{
while(cin >> l >> r >> n) printf("%d\n", solve(r) - solve(l - 1));
return 0;
}
例题5:不要62
题意
问\([L, R]\)之间有多少数字里面没有\(4\)和\(62\)。
题目链接:https://www.acwing.com/problem/content/1087/
数据范围
\(1 \leq L \leq R \leq 10^9\)
思路
首先看没有\(4\)的情况,我们只需要在枚举当前位时,把\(4\)去掉即可。
对于没有\(62\)的情况,我们记录一个pre,如果pre为\(6\)且当前位为\(4\),则continue
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int l, r;
int a[N];
int f[N][N];
int dfs(int p, int pre, bool limit)
{
if(p < 0) return 1;
if(!limit && f[p][pre] != -1) return f[p][pre];
int up = limit ? a[p] : 9;
int ans = 0;
for(int i = 0; i <= up; i ++) {
if(i == 4) continue;
if(pre == 6 && i == 2) continue;
ans += dfs(p - 1, i, limit && i == a[p]);
}
if(!limit) f[p][pre] = ans;
return ans;
}
int solve(int x)
{
int p = 0;
while(x) {
a[p ++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(p - 1, 0, 1);
}
int main()
{
while(cin >> l >> r) {
if(!l && !r) break;
cout << solve(r) - solve(l - 1) << endl;
}
return 0;
}
例题6:计数问题
题意
给定两个整数\(a\)和\(b\),求\(a\)和\(b\)之间的所有数字中\(0 \sim 9\)的出现次数。
题目链接:https://www.acwing.com/problem/content/340/
数据范围
\(0 < a, b < 10^8\)
思路
这道题需要统计\(10\)个量,因此我们可以跑\(10\)次数位DP,分别统计每个数字的出现个数。
由于这道题前导零是不算在内的,因此需要考虑前导零的情况。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int l, r;
int a[N];
int f[N][N];
int dfs(int p, int num, int x, bool lead, bool limit)
{
if(p < 0) return num;
if(!limit && !lead && f[p][num] != -1) return f[p][num];
int up = limit ? a[p] : 9;
int ans = 0;
for(int i = 0; i <= up; i ++) {
if(lead) {
if(!i) ans += dfs(p - 1, num, x, lead, limit && i == a[p]);
else ans += dfs(p - 1, num + (x == i), x, 0, limit && i == a[p]);
}
else ans += dfs(p - 1, num + (x == i), x, lead, limit && i == a[p]);
}
if(!limit && !lead) f[p][num] = ans;
return ans;
}
int solve(int x, int t)
{
int p = 0;
while(x) {
a[p ++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(p - 1, 0, t, 1, 1);
}
int main()
{
while(cin >> l >> r) {
if(!l && !r) continue;
if(l > r) swap(l, r);
for(int i = 0; i < 10; i ++) {
printf("%d ", solve(r, i) - solve(l - 1, i));
}
printf("\n");
}
return 0;
}