「一本通 5.3 例 1」Amount of Degrees

GitHub跳转原题关系图返回列表

把「K 个互不相等的 B 的幂之和」等价转化为 B 进制只含 0/1 且恰有 K 个 1 的数,前缀相减后从高位卡上界,每遇到松绑就用组合数一次收账。

OJ: roj

题目 ID: 1585

难度:普及+/提高-

标签:数位 DP组合计数进制python

创建: 2026-09-30 20:14

更新: 2026-10-07 13:50

形式化题目

给定区间 [X,Y][X, Y] 与参数 K,BK, B,统计其中满足下述条件的整数 nn 的个数:

n=Be1+Be2+⋯+BeK,0⩽e1<e2<⋯<eK n = B^{e_1} + B^{e_2} + \dots + B^{e_K}, \qquad 0 \leqslant e_1 < e_2 < \dots < e_K

即 nn 恰好等于 KK 个互不相等的 BB 的非负整数次幂之和。例如 X=15, Y=20, K=2, B=2X=15,\ Y=20,\ K=2,\ B=2 时,17=24+2017 = 2^4+2^0、18=24+2118 = 2^4+2^1、20=24+2220 = 2^4+2^2 满足条件,答案为 33。

正解

思路

1. 等价转化:幂之和就是 BB 进制的 0/1 数

BB 进制表示是唯一的。若 n=Be1+⋯+BeKn = B^{e_1} + \dots + B^{e_K} 且指数互不相同,把这些幂按数位直接摆放不会产生任何进位,得到的 BB 进制表示就是"只有 KK 个 11、其余位为 00";反之,一个 BB 进制只含 0/10/1 的数,把每个 11 还原成对应的幂即得所需拆分。因此:

n 满足条件  ⟺  n 的 B 进制表示只含 0/1 且恰有 K 个 1 n \text{ 满足条件} \iff n \text{ 的 } B \text{ 进制表示只含 } 0/1 \text{ 且恰有 } K \text{ 个 } 1

(指数只取非负整数:样例含 202^0,若允许负指数会得到分数,与"统计整数"矛盾。)

2. 朴素做法与瓶颈

最直接的做法是枚举 [X,Y][X, Y] 中每个数、转成 BB 进制检查一遍——单次判断只要 O(log⁡BY)\mathcal{O}(\log_B Y),但 Y−XY - X 最大可达 231−12^{31}-1,整体不可行。瓶颈在区间跨度而不在单次判断,于是改用前缀和:令 f(n)f(n) 统计 [0,n][0, n] 中的合法数,答案 =f(Y)−f(X−1)= f(Y) - f(X-1),区间长度从复杂度里消失,问题变成单点计数 f(n)f(n)。

3. f(n)f(n):从高位卡上界,一松绑就用组合数收账

f(n)f(n) 是"上界限制 + 逐位决策"结构,即数位 DP。把 nn 写成 BB 进制,从高位到低位扫描,维护:

  • ones:已确定的高位里 11 的个数(还缺 K−onesK - \texttt{ones} 个);
  • rest:当前位之后还剩多少个低位。

一旦某一位严格小于 nn,上界就永久松绑,低位可以自由选 0/10/1,方案数恰是"从剩余位里挑够其余的 11"的组合数,可以立刻入账而不必继续递归:

当前位数字 dd 决策与入账
d⩾2d \geqslant 2 本位放 00 或 11 都严格小于 nn,本位加低位共 rest+1\texttt{rest}+1 位全自由,入账 (rest+1K−ones)\dbinom{\texttt{rest}+1}{K-\texttt{ones}},扫描结束
d=1d = 1 放 00 则低于上界,入账 (restK−ones)\dbinom{\texttt{rest}}{K-\texttt{ones}};放 11 继续卡上界,ones 加 1
d=0d = 0 放 11 会超过 nn,只能放 00 继续卡上界,入账 00

扫完全部位后,nn 本身每一位都是 0/10/1,若 ones=K\texttt{ones} = K 再补上这 11 个方案。所有入账项对应"第一次变小的位置不同"的互斥集合,因此不重不漏。边界上,n=0n=0 时数字列为 [0][0]、全程无入账,故 X=1X=1 对应的 f(0)=0f(0)=0;KK 超过可凑位数时组合数越界取 00,不会多计。

4. 样例推演

对 f(20)f(20)(K=2, B=2, 20=101002K=2,\ B=2,\ 20 = 10100_2)逐位执行上表:

当前位(权值) dd 操作 本步入账 累计 ones
242^4 11 放 00 账 (42)\binom{4}{2},放 11 继续 66 66 11
232^3 00 只能放 00 00 66 11
222^2 11 放 00 账 (21)\binom{2}{1},放 11 继续 22 88 22
212^1 00 只能放 00 00 88 22
202^0 00 只能放 00 00 88 22
扫完 — ones=2=K\texttt{ones}=2=K,2020 本身入账 11 99 22

其中"放 00 账 (42)=6\binom{4}{2}=6"数出的是 1616 以下恰含两个 11 的数:3,5,6,9,10,123,5,6,9,10,12;"放 00 账 (21)=2\binom{2}{1}=2"数出的是 100102=1810010_2=18、100012=1710001_2=17。同理 f(14)f(14)(14=1110214 = 1110_2)依次入账 (32)+(21)+(10)=6\binom{3}{2} + \binom{2}{1} + \binom{1}{0} = 6,扫完 ones=3≠2\texttt{ones} = 3 \neq 2 不补,故 f(14)=6f(14) = 6。最终答案 f(20)−f(14)=9−6=3f(20) - f(14) = 9 - 6 = 3,对应 17,18,2017, 18, 20,与题面样例一致。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 20:16
# update_at: 2026-09-30 20:16

import sys
from functools import cache


@cache
def comb(n: int, k: int) -> int:
    """C(n, k),越界取 0:数位 DP 展开成组合数时的查表。"""
    if k < 0 or k > n:
        return 0
    if n == 0 or k == 0:
        return 1
    return comb(n - 1, k - 1) + comb(n - 1, k)


def to_base(n: int, b: int) -> list[int]:
    """n 的 b 进制数字,高位在前;n = 0 时返回 [0]。"""
    digits: list[int] = []
    while n:
        digits.append(n % b)
        n //= b
    return digits[::-1] or [0]


def count(n: int, k: int, b: int) -> int:
    """[0, n] 中 b 进制表示只含 0/1 且恰有 k 个 1 的数的个数。

    从高位到低位卡上界:当前位数字 > 1 时,本位放 0/1 都严格小于 n,
    低位可任选 0/1,直接用组合数收尾;当前位是 1 时,放 0 同样低于上界
    (组合数入账),放 1 继续卡;当前位是 0 时只有放 0 才不越界。
    """
    digits = to_base(n, b)
    ans = 0
    ones = 0  # 已确定的高位里 1 的个数
    for i, d in enumerate(digits):
        rest = len(digits) - i - 1  # 当前位之后还剩多少个低位
        if d > 1:
            # 本位放 0 或 1 都小于 n:从「本位 + 低位」共 rest+1 位里再挑 k-ones 个 1
            return ans + comb(rest + 1, k - ones)
        if d == 1:
            ans += comb(rest, k - ones)  # 本位放 0,rest 个低位自由选 0/1
            ones += 1                    # 本位放 1,继续卡上界
        # d == 0:放 1 会超过 n,只能放 0 继续卡
    return ans + (ones == k)  # n 本身每一位都是 0/1,看它是否恰好 k 个 1


def solve() -> None:
    x, y, k, b = map(int, sys.stdin.buffer.read().split())
    print(count(y, k, b) - count(x - 1, k, b))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 10:38
 * update_at: 2026-10-05 10:38
 */
#include <iostream>
using namespace std;

typedef long long ll;

const int MAXN = 45; // B=2 时 2^31-1 只有 31 位,组合数参数留出余量

ll comb_table[MAXN][MAXN]; // comb_table[n][k] = C(n, k)
ll digits[MAXN];           // n 的 B 进制数字,高位在前,下标从 0 开始
int digit_cnt;             // digits 中有效位数的个数

// 预处理帕斯卡三角,供数位扫描时查组合数。
void build_comb() {
    for (int n = 0; n < MAXN; n++) {
        comb_table[n][0] = 1;
        for (int k = 1; k <= n; k++) {
            comb_table[n][k] = comb_table[n - 1][k - 1] + comb_table[n - 1][k];
        }
    }
}

// 返回 C(n, k);k < 0 或 k > n 时按 0 处理(凑不够 1 的方案不存在)。
ll comb(ll n, ll k) {
    if (k < 0 || k > n) {
        return 0;
    }
    return comb_table[n][k];
}

// 把 n 写成 b 进制,高位在前存入 digits[];n = 0 时存入单个 0。
void to_base(ll n, ll b) {
    digit_cnt = 0;
    if (n == 0) {
        digits[digit_cnt] = 0;
        digit_cnt++;
        return;
    }
    ll low[MAXN];
    int low_cnt = 0;
    while (n > 0) {
        low[low_cnt] = n % b;
        low_cnt++;
        n /= b;
    }
    for (int i = low_cnt - 1; i >= 0; i--) {
        digits[digit_cnt] = low[i];
        digit_cnt++;
    }
}

// 统计 [0, n] 中 B 进制表示只含 0/1 且恰有 k 个 1 的数的个数。
// 从高位到低位卡上界:某一位一旦严格小于 n,剩余位可自由选 0/1,用组合数一次收账。
ll count_prefix(ll n, ll k, ll b) {
    to_base(n, b);
    ll ans = 0;
    ll ones = 0; // 已确定的高位中 1 的个数,还差 k - ones 个
    for (int i = 0; i < digit_cnt; i++) {
        ll d = digits[i];
        ll rest = digit_cnt - i - 1; // 当前位之后还剩多少个低位
        if (d > 1) {
            // 本位放 0 或 1 都严格小于 n,本位加低位共 rest+1 位自由挑 k-ones 个 1
            return ans + comb(rest + 1, k - ones);
        }
        if (d == 1) {
            ans += comb(rest, k - ones); // 本位放 0,rest 个低位自由选
            ones++;                      // 本位放 1,继续卡上界
        }
        // d == 0:放 1 会超过 n,只能放 0 继续卡上界,不入账
    }
    if (ones == k) {
        ans++; // n 本身每位都是 0/1,看它是否恰好 k 个 1
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    ll x, y, k, b;
    cin >> x >> y;
    cin >> k >> b;

    build_comb();
    cout << count_prefix(y, k, b) - count_prefix(x - 1, k, b) << "\n";

    return 0;
}

复杂度

  • 时间复杂度:O(log⁡BY)\mathcal{O}(\log_B Y) 次组合数查询。nn 的 BB 进制表示最多 3131 位(B=2B=2 且 Y=231−1Y = 2^{31}-1),每位至多入账一次;组合数用 functools.cache 记忆化的帕斯卡递归,comb(n, k) 的参数范围是 n⩽31n \leqslant 31、k⩽20k \leqslant 20,缓存的子问题总量不超过 32×2132 \times 21,均摊为常数。
  • 空间复杂度:O(log⁡BY)\mathcal{O}(\log_B Y)。除数字数组与组合数缓存外只有常数个变量,实测峰值约 14.7 MB14.7\text{ MB}(主要是 Python 解释器本身),相对 128 MB128\text{ MB} 限制可忽略。

总结

本题的钥匙是一次等价转化:" KK 个互不相等的 BB 的幂之和"就是"BB 进制只含 0/10/1 且恰有 KK 个 11 的数",唯一性由 BB 进制表示唯一保证。

区间计数先用前缀相减消掉跨度,再把单点计数 f(n)f(n) 写成数位 DP 的组合数展开:从高位到低位卡上界,每位只有三种决策,一旦严格小于上界就用 (restK−ones)\binom{\texttt{rest}}{K-\texttt{ones}} 一次性收账。这样状态量被压成线性扫描,O(log⁡BY)\mathcal{O}(\log_B Y) 完成整个计算;同一算法在 C++ 里既可以写成 f[pos][ones][tight] 的递归数位 DP,也可以照本解法写成逐位累加的迭代形式,复杂度相同。