一、数位DP基础知识
数位\(dp\)的题目一般会问,某个区间内,满足某种性质的数的个数。
对应的数位\(dp\)问题有相应的解题技巧:
-
利用前缀和,比如求区间\([x,y]\)中的个数,转化成求\([0,y]\)的个数 -\([0,x-1]\)的个数
-
利用树的结构来考虑(按位分类讨论)
二、解题思路
题目就是在一个区间\([x,y]\)内,找出有多少个符合题意的数,这里的符合题意是指:这个数的\(B\)进制表示中,其中有\(K\)个位置上是\(1\)、其他位上全是\(0\)。
比如样例中\(B=2\),\(K=2\),这个数是\(18\).就是把18化为二进制,\(18\) 化为二进制数为:\(10010\),其中有\(2\)位\(1\),其他位都是\(0\)
再比如 \(B=3\),\(K=2\),这个数是\(12\).就是把\(12\)化为三进制,\(12\)化为三进制为\(110\) ,其中也有\(2\)个\(1\),其余都是\(0\).
该类题目的核心就是根据数位进行分类讨论。
第一步
先将数\(x\)转化成为\(B\)进制,记为\(N\)。
//这是一段将n转化为B进制的代码
//输入:n 和进制B
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,B;
cin >> n >> B;
vector<int> a;
while(n) a.push_back( n % B) , n /= B;
for(int i = a.size()-1; i>= 0; i--) cout<< a[i]<<" ";
}
第二步
数位DP,还是背模板来的快,提高课的数位DP讲解的东西太不好想了,也容易遗漏分枝,感觉\(dfs\)模板大法才是正解:
#include <bits/stdc++.h>
using namespace std;
const int N = 35; //因为2进制是最小的进制,2^31是极限,所以N=31,为了防止越界,这里开到了35
int l, r; //左右边界
int k, b; //k个1
int a[N]; //对数字进行拆分的数位数组,数位分离专用
int dp[N][N]; //dp[pos][sum]表示当前第pos位,已经出现了几次数字1
/**
* 功能:统计[0~pos]之间的答案数量
* @param pos 当前枚举到的数位pos(搜索的深度)由高到低
* @param cnt 数字1已经出现了几次
* @param limit 是不是贴上界
* @return 数字个数
*/
int dfs(int pos, int cnt, bool limit) {
//枚举到最后一位数位,是否恰有k个不同的1(也是递归的终止条件)
if (pos == -1) return cnt == k;
//记忆化搜索
//(1)不贴上界,能取满这一位所有的数字(可以看总结才理解一下取满的意义)
//(2)算过
if (!limit && ~dp[pos][cnt]) return dp[pos][cnt];
//up表示能枚举的最高位数
int res = 0, up = limit ? min(a[pos], 1) : 1;//最后的:1,是因为本题的要求,每一位最高取到1,取2,3,...,不符合题意
//套路枚举当前pos位,能取的每个数字
for (int i = 0; i <= up; i++) {
/*如果超过了k个限制,是不行的(题意要求)
这里是每道题不一样的地方
本题的i,其实取值范围就是两个:0,1,为了和模板的套路一致,也使用了for(int i=0;i<=up;i++)的方式
0:本位没有消耗1,所以已经使用的1的个数还是cnt
1:本位消耗了1,所以已经使用的1的个数是cnt+1
为了把两者放在一起写起来方便,就有了cnt+i
如果cnt+i>k,就没有必要继续研究了,因为此路不通,走到一半就没有子弹了~剪枝
*/
if (cnt + i > k) continue;
//下一个位置的深搜
res += dfs(pos - 1, cnt + i, limit && i == a[pos]);//模板套路
}
//如果不受限制,则记录下来提供下次使用;如果受限制,则一把一利索
return limit ? res : dp[pos][cnt] = res;
}
//计算 [0,x]之间的答案数量,参数是十进制数
int calc(int x) {
memset(dp, -1, sizeof dp);
int pos = 0;
while (x) a[pos++] = x % b, x /= b; //把x按照进制分解到数组中
//比如一共有3位,那么就是0,1,2 ,而此时 pos=3,为了计算最高位,那么就需要pos-1
//pos-1 是因为上面的循环最后一次也++了
//cnt = 0 表示已经使用了0个1
//true : 表示贴上界,不能大于当前的a[pos-1]这一位
return dfs(pos - 1, 0, true);
}
int main() {
cin >> l >> r >> k >> b;
//利用类似于前缀和的思想进行处理
cout << calc(r) - calc(l - 1) << endl;
return 0;
}