把「K 个互不相等的 B 的幂之和」等价转化为 B 进制只含 0/1 且恰有 K 个 1 的数,前缀相减后从高位卡上界,每遇到松绑就用组合数一次收账。
OJ: roj
题目 ID: 1585
难度:普及+/提高-
标签:数位 DP组合计数进制python
创建: 2026-09-30 20:14
更新: 2026-10-07 13:50
形式化题目
给定区间
即
正解
思路
1. 等价转化:幂之和就是 进制的 0/1 数
(指数只取非负整数:样例含
2. 朴素做法与瓶颈
最直接的做法是枚举
3. :从高位卡上界,一松绑就用组合数收账
ones:已确定的高位里的个数(还缺 个); rest:当前位之后还剩多少个低位。
一旦某一位严格小于
| 当前位数字 |
决策与入账 |
|---|---|
| 本位放 |
|
放 ones 加 1 |
|
| 放 |
扫完全部位后,
4. 样例推演
对
| 当前位(权值) | 操作 | 本步入账 | 累计 | ones |
|
|---|---|---|---|---|---|
| 放 |
|||||
| 只能放 |
|||||
| 放 |
|||||
| 只能放 |
|||||
| 只能放 |
|||||
| 扫完 | — |
其中"放
代码
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++ 版(同一算法):
/**
* 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;
}复杂度
- 时间复杂度:
次组合数查询。 的 进制表示最多 位( 且 ),每位至多入账一次;组合数用 functools.cache记忆化的帕斯卡递归,comb(n, k)的参数范围是、 ,缓存的子问题总量不超过 ,均摊为常数。 - 空间复杂度:
。除数字数组与组合数缓存外只有常数个变量,实测峰值约 (主要是 Python 解释器本身),相对 限制可忽略。
总结
本题的钥匙是一次等价转化:"
区间计数先用前缀相减消掉跨度,再把单点计数 f[pos][ones][tight] 的递归数位 DP,也可以照本解法写成逐位累加的迭代形式,复杂度相同。