数位dp常用于求某个区间 [L,R] 内符合某个要求的数,
利用前缀和的思想,我们求出[0,N]区间内符合要求的数,然后分别求出[0,R]和[0,L-1]符合区间的数,二者相减即可。
对数 an-1an-2……a0 的划分常采用树的形式:
Case1 : 枚举0到an-1-1,然后,后n-1位随便取,
Case2:最高位取an-1固定,次高位枚举0到an-2-1,后n-2位随便取,然后第n-2位取an-2,一直枚举到最后一位只能取a0,这个数就是an-1an-2……a0。
![](https://i-blog.csdnimg.cn/blog_migrate/a34015d70bdf35af82ac3e9db4b8cc87.png)
左分支和最后一个结点a0用于计数,除此之外的右分支用于分类讨论。
1081. 度的数量
题目描述
求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。
输入格式
第一行包含两个整数 X 和 Y,接下来两行包含整数 K 和 B。
输出格式
只包含一个整数,表示满足条件的数的个数。
数据范围
1≤X≤Y≤231−1,
1≤K≤20,
2≤B≤10
输入样例:
15 20
2
2
输出样例:
3
Hint:
X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:
17=24+20 ;18=24+21 ;20=24+22
分析:
给定一个十进制数n,求0到n之间符合要求的数的个数:
将该十进制数转换成B进制数的形式, an-1an-2……a0 ,ai的取值在0~B-1之间,但是由于Bai 互不相等,所以每个只能出现一次,因此符合要求的B进制数每一位上只能取0或1,最终是K个1,n-K个0。
因此符合要求的B进制数最大值是111……1(n个1),又由于K>=1,所以符合要求的数在000……01到111……11之间。
本来根据数位dp的思想,是要枚举每一个 左分支从0到ai-1的每个数 和 右分支 ai 的情况的,但是就题论题,这个题不需要,因为每一位的值一旦大于1就可以提前结束了,只用枚举0和1就行了。
简单想法: 当前最高位取一个数后,后面的数任意取是否在n的范围之内。
1.如果这个数n的B进制数的最高位an-1大于1,那么说明这个数从符合要求的 0到111……1 之间可以任意取,因此只要在n位中取够K个1就行了,即
C(n,k)
。计数就已经结束了。
2.如果最高位an-1等于1,那么只有当最高位取0的时候,后n-1位可以任意取,当取1的时候,要受限于第an-2的范围,因此,当最高位取0的时候,计数后n-1位取K个1的情况C(n-1,k)
,当最高位取1的时候,此时不能计数,固定该位为1,考虑把an-2当作"最高位"处理,开始下一位处理。
3.如果最高位an-1小于1,即为0,说明它只能取0,且后n-1位还不能任意选择,因此固定该位为0,开始处理下一位。
算法实现
核心:
我们把最终的枚举情况分为两部分,最高位是否为0?
如果最高位为0,则该位固定为0,直接走右分支。
如果最高位大于0,那么它取0的情况是必然要有的,最高位取0后,后n-1位就取K个1,即C(n-1,k)
然后判断该位是否大于1,
如果大于1的话,就是上面的情况1,再考虑最高位取1,那么后n-1位取K-1个1,即C(n-1,k-1)
,注意到刚好有C(n,k)=C(n-1,k)+C(n-1,k-1)
,递推公式,此时就结束了。
如果该位等于1,就是上面的情况2,固定该位为1,因为后面的不能任意取,然后走右分支,继续把an-2当作最高位再次划分。
即上面所说的枚举,根据最高位固定后,后面的是否可以任意取,把每一位ai,划分成0到ai-1和ai两个集合。
预处理组合数:
递推公式:C(a,b)=C(a-1,b-1)+C(a-1,b)
初始化情况: C(0,0)=1; C(0,k)=0;(k>0)
C(k,0)=1;
#include <iostream>
#include <vector>
using namespace std;
const int N=35; //以二进制拆分,拆分的位数最多,最多拆分成31位,(2^31)-1
int K,B,f[N][N];
void init() //预处理组合数 dp[i][j],即C(i,j)
{//c(a,b)=c(a-1,b)+c(a-1,b-1)
for (int i=0;i<=N;i++) {//特地从0开始,只为了初始化一下C(0,0)=1,再往下走j从1开始,大于0就不会循环了,所以C(0,j)=0
f[i][0]=1;
for (int j=1;j<=i;j++) //C(k,0)=1,在上一行已经初始化过了,
f[i][j]=f[i-1][j-1]+f[i-1][j];
}
}
int dp(int n)//求0~n之间满足条件的数
{
if (n==0) return 0; //K>=1,所以该数必大于等于1,n为0时,0~0之间不满足这样的数
//将十进制数拆成B进制数
vector<int> nums;//存储B进制数
while (n) {
nums.push_back(n%B);
n=n/B;
}
int res=0,t=K; //K不能被破坏,要多次调用该函数,所以定义一个t
for (int i=nums.size()-1;i>=0;i--) {//n表示成B进制数一共有i+1位,第i+1位的下标是i,i+1位里要有K个1。
int x=nums[i];
if (x) { //当x大于0时,说明该位可以取0,剩下的随便选,才有走左分支的机会,否则x==0时就直接跳过,走右分支,开始下一位
res+=f[i][t]; //x取0时的情况,剩下i位要取K个1
if (x>1) {//如果x>1的话,说明该位取1也可以,剩下的位也可以随便取,因为n>111……1,不再走右分支判断了,直接左分支全部计数完成
res+=f[i][t-1]; //i+1位取1的话,剩下i位取K-1个1
break; //已经全部枚举完了,可以退出了。
} //以上是左分支的情况
else { //x==1的话,走右分支,固定第i+1位,开始讨论第i位
t--; //如果x为1的话,这位必须固定选1,走右分支,后i-1位只剩K-1个1,但是也不能随便选。
if (!t) {res++; break;} //一路下来都是右分支的情况,可能取0,也可能取1,如果取够K个1就停止,可能是中途某个右分支,或者到最后一个右分支,后面位数都取0,这是最后一个符合要求的数,且该数最大。
}
}
}
return res;
}
int main()
{
init();
int l,r;
cin>>l>>r>>K>>B;
cout<<dp(r)-dp(l-1);
return 0;
}
对于枚举0到n之间符合要求数,都是左分支的数小于右分支,一路右分支直到右分支也停下来,此时这个数是符合要求的数里面最大的数了。
1082 数字游戏
题目描述
陆亿行命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如 123,446。
现在大家决定玩一个游戏,指定一个整数闭区间 [a,b],问这个区间内有多少个不降数。
输入格式
输入包含多组测试数据。
每组数据占一行,包含两个整数 a 和 b。
输出格式
每行给出一组测试数据的答案,即 [a,b] 之间有多少不降数。
数据范围
1≤a≤b≤231−1
输入样例:
1 9
1 19
输出样例:
9
18
分析:
先求整体,即不受约束条件的时候,求以数字j为最高位,且一共有i位的情况下的非降数的个数。
最高位j可以取0,这样求i位数时,可以描述到1位到i-1位数之间的数,
假设n=233,
1.i=3时,_ _ _
j=0时,描述000到099之间的非降数;
j=1时,描述100到199之间的非降数;100到110之间肯定不是非降数。
然后i=2时,后面不能随便取,只能到233,所以固定第一位,
2.i=2时,2 _ _
j=0时,200到209之间的非降数;
j=1时,210到219之间的非降数;
……依次类推。
这可以用递推的方式实现。(见后面叙述)
我们求0到n之间的非降数,给定一个n
假设给定一个十进制数字n,每一位表示成 an-1an-2an-3……a1 ,
第一次,最高位是an-1,它前面没有数字限制,可以分成两个分支,0到an-1-1和an-1。 取左分支:0到an-1时,后面n-1位可以任意取,分别根据前面的递推得出这些情况下的非降数个数,进行累加。取右分支时:固定该位为an-1,然后走下一位,这是下一位由于an-2,由于非降数的限制,他就不能从0开始枚举了,要从an-1开始枚举,因此现在应该分成两个分支,an-1到an-2-1和an-2,然后依次按上面的情况进行递推。由于是非降数,一定要确保an-2 >= an-1,最次取等号的时候,就是不走左分支,直接走右分支,固定该位,但是如果有an-2 < an-1,那就到头了,因为它违反了非降数的定义,就可以停止了,就不存在非降数了。
枚举开头是从0开始还是从ai开始,可以维护一个变量,用它来记录,由于刚开始时没有限制,从0开始,所以该变量初始化为0。
![](https://i-blog.csdnimg.cn/blog_migrate/20f1f6083ec2d860791076d12cedbab3.png)
左分支的数都是要小于右分支的数的,左分支枚举的数从0开始,越往右走,数越大,随时可能中断,一路选择右分支到底就是这个数本身,如果能走到最后一个右分支,说明这个数n就是一个非降数。
左分支负责计数,右分支分类讨论,但是最后一个右分支是存在的最后一种情况。
预处理: 计数类dp
状态表示:f[i,j]
表示i位数字,最高位是j的不降数的个数。
状态计算: 以次高位k作为划分,对于满足条件k>=j
的,有
f
[
i
,
j
]
=
∑
f
[
i
−
1
,
k
]
f[i,j]=∑f[i−1,k]
f[i,j]=∑f[i−1,k]
初始化: 预先初始化位数为1的不降数,注意到f[1,i]显然是只有一位的时候的做法,全都要初始化为1。
先循环位数,求第i位时需要用到第i-1位,需要先处理好。
![](https://i-blog.csdnimg.cn/blog_migrate/4e6f0d8e33d919007cf7e8028ddb4390.png)
算法实现
数据范围x y,最大到231 -1,近似109,因此最低只用求10位数的情况下的非降数个数就可以了。
预处理10位的情况。
#include <iostream>
#include <queue>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=11;
int f[N][N]; //f[i][j]表示一共有i位,且最高位填j的数个数
//预处理,递推公式:当k>=j时,f[i][j]+=f[i-1][k]
void init()
{
//初始化,从长度i=1开始。
for (int j=0;j<=9;j++) f[1][j]=1;
//递推
for (int i=2;i<=N;i++) //先循环位数,求第i位时需要用到第i-1位,需要先处理好
for (int j=0;j<=9;j++) //i位时,最高位以j结尾,寻找j
for (int k=j;k<=9;k++) f[i][j]+=f[i-1][k];
//寻找符合条件的次高位k
}
int dp(int n)
{
if (n==0) return 1; //0也是一个数,1种情况
//存储十进制数的每个位上的数
vector<int> nums;
while (n) nums.push_back(n%10),n/=10;
int i,res=0,last=0; //last记录上一位的前驱是多少,初始化为0,因此第一次枚举最高位的时候可以从0枚举
for (i=nums.size()-1;i>=0;i--) {//下标是i,实际上有i+1位,因为是从0开始存储的
int x=nums[i];
if (x<last) break; //如果当前位的最大值小于上一位的值,后面就不存在非降数了
//走左分支,最高位的值为a^n=x,从枚举0到x-1,由于小于x,所以后面可以任意取,直接求出结果
for (int j=last;j<x;j++) res+=f[i+1][j];
//本来应该是枚举从0到x-1的,但是由于是非降数,所以从该位承接上一位数(last维护),开始枚举
//走右分支,前面枚举到到x-1了,这次该位置取x,维护last的值
last=x;
}
//顺利一路右分支走到底,这是右分支可以统计的最后一种情况,即n这个数本身也是非降数。
if (i==-1) res++;
return res;
}
int main()
{
init();
int l,r;
while(cin>>l>>r) cout<<dp(r)-dp(l-1)<<endl;
return 0;
}
求0到n之间的非降数时:
n==0的话显然直接输出1;
否则
将n每一位拆开,从最高位开始对每一位依次考虑,
1) 若上一位最大值(last)已经高于本位最大值,则说明已经求完所有方案数,直接break
2) 否则:
(1)若不选当前位置最大值(0~an-1),进入左分支,则当前位置取从上一位最大值到这一位最大值之间的数j,以此作为最高位,假设此时算上最高位共有i位,记录所有的f[i][j]
,将这些方案数全部累计到答案中。
(2)若选当前位最大值an,进入右分支,不计数,只更新last。
当右分支走到底时,是最后一种情况,n本身,说明它也是非降数,否则就中途退出了。