把二进制位数约束按 kn 与 w 的关系切成三档:全合法档用组合数直接求和,全超长档截断,临界档只卡首位 d1 < 2^{w-k(n-1)}。
OJ: roj
题目 ID: 1649
难度:普及+/提高-
标签:数学组合计数组合数进制
创建: 2026-10-01 00:48
更新: 2026-10-07 13:50
形式化题目
给定正整数
至少是 位数,且最高位非 ; - 除最后一位外,每一位严格小于其右边相邻的那一位(即数位从左到右严格递增);
- 把
看作二进制数后总位数不超过 ,即 。
样例:
正解
思路
1. 朴素做法与转化
最朴素的想法是枚举每个可能的
2. 条件 2:严格递增数位就是组合数
3. 条件 3:把 与 的关系切成三档
- 全超长档
: ,一个都不合法; 且 随 单调增,更大的位数同样全超长 → 枚举到此为止; - 全合法档
: ,该档全部 个都合法; - 临界档
:同档内有合法有不合法,需要按数值细算。 它至多出现在一个位数上:若 和 都是临界档,则 与 矛盾, 所以临界档就是最长的那一档。
4. 临界档:只需卡首位
写
- 必要:
; - 充分:
时 。
所以条件 3 在临界档恰等价于
下表按三档拆解样例
| 位数 |
与 |
档位 | 计数方式 | 数量 |
|---|---|---|---|---|
| 全合法 | ||||
| 临界 | ||||
| 全超长 | 枚举截断 | |||
| 合计 |
观察重点:
最后按位数求和即得答案:
其中
代码
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-10-01 00:48
# update_at: 2026-10-01 00:48
import sys
from math import comb
def count_r(k: int, w: int) -> int:
"""按位数 n 分类计数:所有满足条件的 2^k 进制数 r 的个数。"""
m = 1 << k # 数位集合 {0, 1, ..., 2^k - 1},共 m 个数位
total = 0
for n in range(2, m): # 至少 2 位;首位非 0,n 个递增数位至多从 m-1 个里选
if k * (n - 1) >= w: # r ≥ 2^{k(n-1)} ≥ 2^w,位数再增必然超长
break
if k * n <= w: # r < 2^{kn} ≤ 2^w,位数不超长则每一位都合法
total += comb(m - 1, n) # 从 1..m-1 里选 n 个数位,严格递增唯一对应一个 r
else:
# 只剩首位 d1 能"救"超长:d1·2^{k(n-1)} ≤ r,故需 d1 < 2^{w-k(n-1)};
# 反之 d1 满足该上界时 r ≤ 2^w - 1 必然不超长,条件恰为 d1 < top。
top = 1 << (w - k * (n - 1))
total += sum(comb(m - 1 - d1, n - 1) for d1 in range(1, top)) # 余下 n-1 位从 d1+1..m-1 选
return total
def solve() -> None:
k, w = map(int, sys.stdin.read().split())
print(count_r(k, w))
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-06 00:26
* update_at: 2026-10-06 00:26
*/
// main.cpp:「一本通 6.6 例 2」2^k 进制数
// 思路:按位数 n 分类,kn <= w 的档整档用组合数 C(m-1, n) 求和;
// 临界档(k(n-1) < w < kn)只卡首位 d1 < 2^t(t = w - k(n-1)),
// 贡献为 sum C(m-1-d1, n-1);k(n-1) >= w 的档全超长,直接截断。
// 结果可达 200 位十进制,用 10^9 进制的大整数数组实现。
#include <cstdio>
#include <algorithm>
typedef long long ll;
using namespace std;
const ll BASE = 1000000000LL; // 大整数用 10^9 进制压位
const int MAXL = 30; // 结果不超过 200 位十进制,30 段足够
// 全局大整数:ans 存最终答案,tmp 存每次算出的组合数(低位在前)
ll ans[MAXL];
ll tmp[MAXL];
int anslen;
int tmplen;
// 清零大整数
void big_zero(ll a[], int &len) {
len = 1;
a[0] = 0;
}
// ans += tmp,逐段相加处理进位
void big_add() {
int l = max(anslen, tmplen);
ll carry = 0;
for (int i = 0; i < l; i++) {
ll s = carry;
if (i < anslen) s += ans[i];
if (i < tmplen) s += tmp[i];
ans[i] = s % BASE;
carry = s / BASE;
}
anslen = l;
while (carry > 0) {
ans[anslen++] = carry % BASE;
carry /= BASE;
}
}
// tmp *= x(x 为小整数),逐段乘加进位
void big_mul(ll x) {
ll carry = 0;
for (int i = 0; i < tmplen; i++) {
ll s = tmp[i] * x + carry;
tmp[i] = s % BASE;
carry = s / BASE;
}
while (carry > 0) {
tmp[tmplen++] = carry % BASE;
carry /= BASE;
}
}
// tmp /= x(x 为小整数),高位到低位做除法
void big_div(ll x) {
ll rem = 0;
for (int i = tmplen - 1; i >= 0; i--) {
ll cur = rem * BASE + tmp[i];
tmp[i] = cur / x;
rem = cur % x;
}
while (tmplen > 1 && tmp[tmplen - 1] == 0) tmplen--;
}
// 把组合数 C(N, K) 算进 tmp;利用前缀积 C(N,i) 都是整数,边乘边除不会失精度
void big_comb(ll N, ll K) {
big_zero(tmp, tmplen);
tmp[0] = 1;
for (ll i = 1; i <= K; i++) {
big_mul(N - K + i);
big_div(i);
}
}
int main() {
ll k, w;
scanf("%lld %lld", &k, &w);
ll m = 1LL << k; // 2^k 进制,数位集合 {0,1,...,m-1}
big_zero(ans, anslen);
for (ll n = 2; n <= m - 1; n++) { // 位数 n:至少 2 位,至多 m-1 位
if (k * (n - 1) >= w) break; // r >= 2^{k(n-1)} >= 2^w,再长全超长,截断
if (k * n <= w) {
// 全合法档:r < 2^{kn} <= 2^w,选 n 个递增数位即唯一确定一个 r
big_comb(m - 1, n);
big_add();
} else {
// 临界档:条件 3 恰等价于首位 d1 < 2^t,t = w - k(n-1)
ll t = w - k * (n - 1);
ll top = 1LL << t;
for (ll d1 = 1; d1 <= top - 1; d1++) { // 首位非 0 且小于 2^t
ll N = m - 1 - d1; // 余下 n-1 位从 d1+1..m-1 里选
if (N < n - 1) continue; // 候选数位不够,组合数为 0
big_comb(N, n - 1);
big_add();
}
}
}
// 输出:最高段不带前导 0,其余段补足 9 位
printf("%lld", ans[anslen - 1]);
for (int i = anslen - 2; i >= 0; i--) printf("%09lld", ans[i]);
printf("\n");
return 0;
}复杂度
- 时间复杂度:
次组合数运算(外层至多 档位数,临界档内层至多 个首位),每次 math.comb为,整体远小于 1000ms 限制。 - 空间复杂度:
工作变量;结果不超过 200 位十进制(约 664 bit),Python 原生大整数直接承载。
总结
- 「数位严格递增、首位非
」的计数标准手法是组合数:选集合即定序列,答案为 。 - 二进制位数约束要翻译成数值约束
,再按 与 的关系分「全超长 / 全合法 / 临界」三档; 全超长档给出枚举的天然上界,全合法档可以整档打包求和。 - 临界档用「夹逼」证明:
的上下界说明 超长与否只取决于首位 是否小于 ,把一个数值判断化回了组合计数。