说到数位DP,它主要是用于处理一些与数位有关的问题,主要是计数问题。并且数位DP一直以来是DP家族里比较冷门的一种,但一旦遇上了,如果不会数位DP单纯靠暴力方法很难骗分。
一、什么是数位DP?
数位DP是一种计数用的DP,一般就是要统计一个区间[l,r]内满足一些条件数的个数。所谓数位DP,字面意思就是在数位上进行DP。数位还算是比较好听的名字,数位的含义:一个数有个位、十位、百位、千位......数的每一位就是数位!
之所以要引入数位的概念完全就是为了DP。数位DP的实质就是换一种暴力枚举的方式,使得新的枚举方式满足DP的性质,然后记忆化就可以了。
数位DP往往都是这样的题型,给定一个闭区间[l,r],让你求这个区间中满足某种条件的数的总数。而这个区间可能很大,简单的暴力代码如下:
int ans=0; for(int i=l;i<=r;i++){ if(check(i))ans++; }
我们发现,若区间长度超过1e8,我们暴力枚举就会超时了,而数位DP则可以解决这样的题型。数位DP实际上就是在数位上进行DP。
数位DP解法
数位DP就是换一种暴力枚举的方式,使得新的枚举方式符合DP的性质,然后预处理好即可。
我们来看:我们可以用f(n)表示[0,n]的所有满足条件的个数,那么对于[l,r]我们就可以用[l,r]⟺f(r)−f(l−1),相当于前缀和思想。那么也就是说我们只要求出f(n)即可。那么数位DP关键的思想就是从树的角度来考虑。将数拆分成位,从高位到低位开始枚举。我们可以视N为n位数,那么我们拆分。那么我们就可以开始分解建树,如下。之后我们就可以预处理再求解f(n)了,个人认为求解f(n)是最难的一步。
听完是不是有点绕,我们可以来点题目练习一下,做完就会发现了数位DP的套路了。
二、数位DP经典例题
例题:度的数量
题目:求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于K个互不相等的B的整数次幂之和。例如,设 X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:
输入格式
第一行包含两个整数 X 和 Y,接下来两行包含整数 K 和 B 。
输出格式
只包含一个整数,表示满足条件的数的个数。
数据范围
输入样例:
15 20
2
2
输出样例:
3
解题思路
此题实际上就是将十进制数转化为B进制数,判断位数上的值是否为1。那么我们可以视N为n位数,那么我们拆分。从树的角度考虑:我们设N=76543210,B=10,那么我们从高位往最低位开始枚举如下;枚举时,我们有两种选择:
(1)走右边分支,那么我们填,而题目要求每一位只能填1或者0,而,所以不是合法方案,我们直接剔除。
(2)走左边分支,那么我们可以填0~6,即,那么由于每一位只能填1或者0,所以我们累加这两种选择的方案。
记住,走到了左边分支是可以直接累加的。
所以我们实际上还是要做一个预处理的,我们用f[i][j]表示还剩下i位没有填,且需要填写j个1的方案数。那么在(i,j)这个状态,我们可以选择填1,那么接下来的状态就是f[i−1][j−1],而如果填0,那么接下来的状态就是f[i−1][j],那么状态转移方程就是f[i][j]=f[i-1][j]+f[i][j−1]。而初始状态即是当j=0时,f[i][0]=1。这样我们就可以预处理f数组了。
处理完之后我们就可以直接模拟做了。
代码如下:
/** *@filename:度的数量 *@author: pursuit *@csdn:unique_pursuit *@email: 2825841950@qq.com *@created: 2021-05-12 11:23 **/ #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 100000 + 5; const int mod = 1e9+7; int l,r,k,b; int f[35][35]; //首先我们先预处理f数组。其中f[i][j]表示剩下还有i个没填,需要填写j个1的方案数。 void init(){ for(int i=0;i<35;i++){ for(int j=0;j<=i;j++){ if(!j)f[i][j]=1; else{ f[i][j]=f[i-1][j]+f[i-1][j-1]; } } } } int dp(int n){ //求解f(n)。我们需要避免n为0的情况,这里需要特判。 if(!n)return 0; vector<int> nums;//将n分割,存储位数。 while(n){ nums.push_back(n%b); n/=b; } int ans=0;//答案。 int last=0;//前面的信息,这里代表的是前面分支选取了多少个1. for(int i=nums.size()-1;i>=0;i--){ int x=nums[i]; if(x){ //说明x>0,我们可以选择左边分支填0. ans+=f[i][k-last]; if(x>1){ //当x>1我们才可以枚举左边分支填1. if(k-last-1>=0){ //如果还可以填1的话。 ans+=f[i][k-last-1]; } break;//因为右边分支只能为0或1,所以不符合条件。break。 } else{ //当x=1就可以进入右边的分支继续讨论。 last++; if(last>k)break; } } //考虑到最后一位,如果符合条件那么末位填0也算一种方案。 if(!i&&last==k)ans++; } return ans; } void solve(){ } int main(){ cin>>l>>r>>k>>b; init(); cout<<dp(r)-dp(l-1)<<endl; solve(); return 0; }
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程