数位 dp
简介
数位 dp 解决的是与数字有关的一类计数问题,在求解过程中常把一个数字的每一位都拆开来看,比如十进制下就是把千位、百位、十位、个位上的数字都拆开来看,其他进制类比十进制。
数位 dp 的问题一般比较显眼,有几个常见形式:
-
要求统计满足一定条件的数的数量(即,最终目的为计数);
-
这些条件经过转化后可以使用「数位」的思想去理解和判断;
-
输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
-
上界很大(比如
),暴力枚举验证会超时。
(from OI Wiki)
在数位 dp 的实现上,我通常采用的是记忆化搜索,这样写不仅容易,而且易于拓展,还可以当板子来背,这已经是 dp 中少见的了。
例题
P2657 [SCOI2009] windy 数
题目大意
求
思路
考虑从最高位开始填数,在记忆化搜索时记录
先把上界的每一位抠出来,那么当搜索放第
然后在枚举第
当然,若是前导零的话还要特别注意,因为这时
在加记忆化时还要注意,若出现了顶上界或前导零的情况是不能记忆化的(当然你也可以多开两维来额外存,不过我觉得没什么必要,这个时候直接暴力搜索就行了,反正也费不了多少时间)。
#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 15;
int l, r;
int f[N][N];
vector<int> num;
int dfs(int pos, int pre_num, bool limit, bool zero) {
if(pos < 0) return 1; //边界,若放完了最后一位就返回 1,因为我们一直是按要求放的,所以此时也是一种情况
if(!limit && pre_num >= 0 && f[pos][pre_num] != -1) return f[pos][pre_num]; //记忆化
int mx = (limit ? num[pos] : 9); //计算上界
int res = 0;
for(int i = 0; i <= mx; i++) {
if(abs(i - pre_num) < 2) continue;
if(!i && zero) //特判前导零的情况,这时 prenum 设为 -2 确保下一位不受任何限制
res += dfs(pos - 1, -2, limit && (i == num[pos]), 1);
else
res += dfs(pos - 1, i, limit && (i == num[pos]), 0);
}
if(!limit && !zero) f[pos][pre_num] = res;
return res;
}
int calc(int x) {
num.clear();
int tmp = x;
//先把上界的每一位抠出来
while(tmp) {
num.push_back(tmp % 10);
tmp /= 10;
}
//初始化
memset(f, -1, sizeof f);
return dfs(num.size() - 1, -2, 1, 1);
}
int main() {
scanf("%d%d", &l, &r);
//数位 dp 通常都有这种类似前缀和的形式
printf("%d\n", calc(r) - calc(l - 1));
return 0;
}
P2602 [ZJOI2010] 数字计数
题目大意
求
思路
对每个数码分开计算,还是拆成
在记忆化搜索时记录一下当前考虑的数码
后面都只放主要代码了,因为真的很板子。
ll dfs(int pos, ll cnt, bool limit, bool zero, int d) {
if(pos < 0) return cnt;
if(!limit && !zero && f[pos][cnt] != -1) return f[pos][cnt];
int mx = (limit ? num[pos] : 9);
ll res = 0;
for(int i = 0; i <= mx; i++)
res += dfs(pos - 1, cnt + ((!zero || i) && (i == d)), limit && (i == num[pos]), zero && (!i), d);
if(!limit && !zero) f[pos][cnt] = res;
return res;
}
Digit Sum
题目大意
求
数据范围:
思路
很明显的数位 dp。
首先把上界
加上记忆化,设
然后在搜索过程中记一下当前数位和
这里再讲一下数位 dp 如何分析时间复杂度。
注意到状态数为
注意!由于最后要
ll dfs(int pos, int r, bool limit, bool zero) {
if(pos < 0) return (r == 0);
if(!limit && !zero && f[pos][r] != -1) return f[pos][r];
int mx = (limit ? num[pos] : 9);
ll res = 0;
for(int i = 0; i <= mx; i++)
res = (res + dfs(pos - 1, (r + i) % d, limit && (i == num[pos]), zero && (!i))) % mod;
if(!limit && !zero) f[pos][r] = res;
return res;
}
P4127 [AHOI2009] 同类分布
题目大意
求出
思路
若是要求整除的数是同一个数,那就和上一题一样,但若除的数都不一样该怎么办?
那我们就换一种思路,直接枚举数位和,然后在搜索时每填一个数就相应地减去,同时记录一下余数,其他的参数照搬即可。
由于最多有
ll dfs(int pos, int sum, int r, bool limit, bool zero) {
if(sum < 0) return 0;
if(pos < 0) return !sum && !r;
if(!limit && !zero && f[pos][sum][r] != -1) return f[pos][sum][r];
int mx = (limit ? num[pos] : 9);
ll res = 0;
for(ll i = 0; i <= mx; i++)
res += dfs(pos - 1, sum - i, (r * 10 + i) % d, limit && (i == num[pos]), zero && (!i));
if(!limit && !zero) f[pos][sum][r] = res;
return res;
}
P10958 启示录
题目大意:
思路:
考虑数位 dp。
一般数位 dp 问题有两种常见形式:
- 询问
内有多少个符合条件的数; - 询问满足条件的第
大(小)的数是什么。
很显然这道题是第二种形式。
首先问题
因为答案具有单调性,于是可以二分判定。
每次二分到一个值
接着考虑问题
int dfs(int pos, int cnt, bool flag, bool limit) {
if(pos < 0) return flag;
if(!limit && f[pos][cnt][flag] != -1) return f[pos][cnt][flag];
int mx = (limit ? num[pos] : 9);
int res = 0;
for(int i = 0; i <= mx; i++) {
int ncnt;
if(i == 6) ncnt = cnt + 1;
else ncnt = 0;
res += dfs(pos - 1, ncnt, flag || (ncnt >= 3), limit && (i == num[pos]));
}
if(!limit) f[pos][cnt][flag] = res;
return res;
}
这里我直接把二分值域拉满了,但是实测发现第
时间复杂度为:
拓展:
数位 dp 一般会与 Lucas 定理一起食用,毕竟 Lucas 定理就是逐位求组合数。
P7976 「Stoi2033」园游会
题目大意:
设函数
思路:
看到奇奇怪怪的组合数求和首先考虑
其中
接着注意到
对于每一个
- 当
时, 取 的时候有贡献,此时这一位的值为 ; - 当
时, 取 的时候有贡献,此时这一位的值为 ; - 当
时, 取 的时候有贡献,此时这一位的值为 。
而乘
然后就会发现统计一下三进制表示下
#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 65, mod = 1732073999;
int T;
ll vmax, n;
vector<int> num;
ll f[N][N];
ll qpow(int a, int b) {
ll ans = 1, base = a;
while(b) {
if(b & 1) ans = ans * base % mod;
base = base * base % mod;
b >>= 1;
}
return ans;
}
ll dfs(int pos, int cnt, bool limit, bool zero) {
if(pos < 0) return qpow(2, cnt);
if(!limit && !zero && ~f[pos][cnt]) return f[pos][cnt];
int mx = (limit ? num[pos] : 2);
ll res = 0;
for(int i = 0; i <= mx; i++)
res = (res + dfs(pos - 1, cnt + (i == 1), limit && (i == num[pos]), zero && (!i))) % mod;
if(!limit && !zero) f[pos][cnt] = res;
return res;
}
ll calc(ll x) {
num.clear();
ll tmp = x;
while(tmp) {
num.push_back(tmp % 3);
tmp /= 3;
}
return dfs(num.size() - 1, 0, 1, 1);
}
int main() {
scanf("%d%lld", &T, &vmax);
memset(f, -1, sizeof f);
while(T--) {
scanf("%lld", &n);
printf("%lld\n", calc(n));
}
return 0;
}
习题:
√P8764 [蓝桥杯 2021 国 BC] 二进制问题
√P6218 [USACO06NOV] Round Numbers S
√P4124 [CQOI2016] 手机号码
√P4317 花神的数论题
√P7976 「Stoi2033」园游会
P3413 SAC#1 - 萌数
P3286 [SCOI2014] 方伯伯的商场之旅
P2481 [SDOI2010] 代码拍卖会
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!