一、数位DP基础知识

数位\(dp\)的题目一般会问,某个区间内,满足某种性质的数的个数

对应的数位\(dp\)问题有相应的解题技巧

  • 利用前缀和,比如求区间\([x,y]\)中的个数,转化成求\([0,y]\)个数 -\([0,x-1]\)个数

  • 利用的结构来考虑(按位分类讨论

二、解题思路

AcWing 1081 度的数量

题目就是在一个区间\([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;
}

相关文章:

  • 2021-12-13
  • 2021-12-13
  • 2021-08-17
  • 2022-01-26
  • 2022-12-23
  • 2021-10-30
  • 2021-08-28
  • 2021-07-17
猜你喜欢
  • 2022-01-25
  • 2021-08-15
  • 2021-12-13
  • 2022-12-23
  • 2021-11-02
  • 2022-01-25
  • 2021-11-19
相关资源
相似解决方案