AcWing 1081 度的数量


一、读懂题意

  • \([X,Y]\)之间所有数(闭区间),表示成\(B\)进制,然后每一位上必须是\(1\)\(0\)
    只有每个数位是\(1\)或者是\(0\)的才能表示出题目要求的数字。

  • 题目要求 一段区间 内符合某些条件的数的个数,我们用 前缀和思想 转换为求两个 前缀区间 的问题

\(res[l,r]=res[1,r]?res[1,l?1]\)

  • 题目要求求出在指定的数字\(n\)以内,\(B\)进制每个位置上的数字要么为\(1\),要么为\(0\),而且为\(1\)的个数还是\(K\)的数字个数是多少。

二、数位分离之模板大法

#include 

using namespace std;
const int N = 35; //因为2进制是最小的进制,2^31是极限,所以N=31,为了防止越界,这里开到了35
int l, r;         //左右边界
int k, b;         //k个b进制数
int a[N];         //对数字进行拆分的数位数组,数位分离专用
int dp[N][N];     //dp[pos][sum]表示当前第pos位,已经出现了几次数字1

/**
 * 功能:统计[0~pos]之间答案
 * @param pos 当前枚举到的数位pos(搜索的深度)由高到低
 * @param sum 数字1已经出现了几次
 * @param limit 是不是贴上界
 * @return 数字个数
 */
int dfs(int pos, int sum, bool limit) {
    if (pos == -1) return sum == k;//枚举到最后一位数位,是否恰有k个不同的1(也是递归的终止条件)
    //记忆化搜索,前提是不贴着上界(可以枚举满这一位所有的数字)
    //只有无限制、无前导零才算,不然都是未搜索完的情况
    if (!limit && ~dp[pos][sum]) return dp[pos][sum];
    //up表示能枚举的最高位数
    int res = 0, up = limit ? min(a[pos], 1) : 1;
    for (int i = 0; i <= up; i++) {     //尝试填充每一个可能的数字
        //剪枝
        if (sum + i > k) continue;      //如果超过了k个限制,是不行的(题意要求)
        //下一个位置的深搜
        res += dfs(pos - 1, sum + i, limit && i == a[pos]);
    }
    //如果不受限制,则记录下来提供下次使用;如果受限制,则一把一利索
    return limit ? res : dp[pos][sum] = res;
}

int calc(int x) {
    int pos = 0;
    while (x) a[pos++] = x % b, x /= b;        //把x按照进制分解到数组中
    return dfs(pos - 1, 0, true);
}

int main() {
    cin >> l >> r >> k >> b;
    memset(dp, -1, sizeof dp);
    cout << calc(r) - calc(l - 1) << endl;
    return 0;
}

三、DP解题思路

这是一道数位\(dp\)问题,对于数位\(dp\)问题关键就在于分类讨论。

首先我们把数字\(n\)对于\(B\)进制来进行分解 ,将每一位上的数字存入一个数组中,然后从高位往低位去讨论,

首先 对于第 \(i\) 位数字 \(x\) 有三种情况

\(x = 0\)
\(i\) 位上只能取 \(0\) ,所以产生\(k\)\(1\)的光荣使命只能交给 \(i-1\)去实现啦~

\(x = 1\)

  • \(i\) 位上取 \(0\) 的时候,后面\(i-1\)位都可以随意取值(全都可以取到)。
  • \(i\) 位上取 \(1\) 的时候,后面\(i-1\)位要在小于题目的数\(i-1\)位的前提下取值,并且能取 \(k-last-1\)\(1\)。[这类似于一个递归的子问题]

\(x > 1\)
\(i\) 位上 可以取 \(1 ,0\) ,并且后面 \(i-1\) 位可以随便取。

这里 \(i-1\)位随便取对于\(i-1\) 位讨论的区别,就体现在前一个直接用组合数 \(f[i-1][k-last]\)就可以,后面则需要再进入循环去讨论。

对于最后一位进行单独讨论,如果对于 最后一位时 ,所有的\(k\)\(1\)都已经取好了,也就是\(k==last\)了,才做\(res++\)

组合数公式的递推式
\(C_a^b=C_{a-1}^{b-1}+C_{a-1}^b\)

四、DP代码实现

#include

using namespace std;

const int N = 33; //1≤X≤Y≤2^31?1 2进制时,数位最多,最多不会超过32位,这里给了33位,足够
int K, B;         //允许使用K个,B进制
int f[N][N];

//求组合数
void init() {
    for (int i = 0; i <= N; i++)
        for (int j = 0; j <= i; j++)
            if (!j)f[i][j] = 1;
            else f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
}

/**
 * 功能:计算0~n之间符合题意要求的数字的个数
 * @param n
 * @return
 */
int dp(int n) {
    if (!n) return 0;//如果n==0,返回0
    //转为B进制,存入nums数组
    vector nums;
    while (n) nums.push_back(n % B), n /= B;
    int res = 0;
    int last = 0;//表示已经取了多少个1
    for (int i = nums.size() - 1; i >= 0; i--) {//从最高位对每一位数讨论
        int x = nums[i];
        if (x) {//当x==0时,就请下一位继续讨论吧,表现在代码上就是只讨论x>=1的情况

            //(1)不管是x==1,还是x>1,它们在本位置选择放置0时,套路是一样的:
            //第i位取0,就是对于后面i位(0~i-1,共i位)取k-last个1的数量
            res += f[i][K - last];

            //(2)处理本位取1的情况
            if (x > 1) {
                //如果x>1,就可以直接用组合数表示出来,不用进行讨论,也就是i位取1的时候,后面i位随便取k-last-1个1
                if (K - last - 1 >= 0) res += f[i][K - last - 1];
                break;  //算完了,后面的情况全包圆了,还算个啥?
            } else {
                //如果x==1,那么i位取1的时候,还要进行讨论,后面i位不能随便取,也就不是组合数
                last++;//多占用了一个1
                if (last > K) break;//已选择个数++后,已经大于限定值K,则不用再讨论下去,退出循环,返回当前结果
            }
        }
        //对于最后一位来特殊考虑
        if (!i && last == K) res++;
    }
    //返回方案数
    return res;
}

int main() {
    //预处理组合数
    init();
    //左右边界
    int l, r;
    cin >> l >> r >> K >> B;
    //利用前缀和思路进行计算
    cout << dp(r) - dp(l - 1) << endl;
    return 0;
}