攻克蓝桥杯2025省赛真题:01串统计问题(附完整代码+思路拆解)
蓝桥杯省赛的算法题往往兼具"思维性"和"实战性",2025年第十六届省赛的「01串」问题就是典型------看似简单的统计问题,暗藏对大数处理和数学规律的考察。本文会从题意拆解、核心思路、代码解析到优化细节,带你彻底吃透这道题。
一、题目解读
题目描述
我们有一个无限长的01串,它由 0, 1, 2, 3, ... 这些自然数的二进制表示依次拼接 而成,前若干位形如:011011100101110111 · · ·。
给定一个正整数 x,请计算这个串的前 x 位中包含多少个数字 1。
输入输出与样例
- 输入:一个正整数
x(1 ≤ x ≤ 10^18) - 输出:前
x位中1的个数 - 样例:
输入7→ 前7位是0 1 1 0 1 1 1→ 输出5
核心痛点
如果直接暴力拼接二进制数、逐位统计,当 x 达到 10^18 时,时间和空间都会直接爆炸。必须找到二进制数的分布规律,用"分段统计"替代暴力遍历。
二、解题思路拆解
第一步:简化问题------跳过无意义的bin(0)
自然数0的二进制是 "0",仅占1位且无数字1,因此我们可以先把 x 减1(跳过这一位),从自然数1的二进制开始统计,最终结果不受影响。
第二步:按二进制位数「分段」
观察自然数的二进制长度规律:
| 二进制位数 t | 数字范围 | 数字个数 | 总占用位数 |
|---|---|---|---|
| 1位 | [1](2⁰ ~ 2¹-1) | 2⁰ = 1 | 1×1 = 1 |
| 2位 | [2,3](2¹ ~ 2²-1) | 2¹ = 2 | 2×2 = 4 |
| 3位 | [4,7](2² ~ 2³-1) | 2² = 4 | 3×4 = 12 |
| ... | ... | 2^(t-1) | t×2^(t-1) |
总结规律:
t位二进制数的范围是[2^(t-1), 2^t - 1];- 共有
2^(t-1)个这样的数; - 总共占用
t × 2^(t-1)位。
第三步:整段统计1的数量(核心公式)
对于每一段 t 位的二进制数,我们可以用数学公式直接算出其中1的总数,无需逐个统计:
- 最高位的1 :所有
t位二进制数的最高位都是1,因此这部分有2^(t-1)个1; - 剩余t-1位的1 :剩下的
t-1位中,每一位的0和1是均匀分布的(比如2位二进制数的低位:2是10、3是11,低位0和1各出现1次)。因此每一位有2^(t-2)个1,t-1位总共有(t-1) × 2^(t-2)个1; - 整段总数 :
2^(t-1) + (t-1) × 2^(t-2)。
第四步:分阶段处理
- 整段处理 :如果当前剩余需要统计的位数
x≥ 该段总位数(t×2^(t-1)),则直接把该段的1的数量加到答案中,同时x减去该段总位数,t加1继续处理下一段; - 剩余部分处理 :当剩余
x不足一个完整段时,从该段的第一个数(2^(t-1))开始,逐个拼接二进制位、统计1的个数,直到x被消耗完。
三、完整代码与逐行解析
先贴出可直接运行的代码(优化了pow精度问题,原代码的pow可能因double精度丢失出错):
cpp
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
// 数据类型必须用long long,否则1e18会溢出
typedef long long LL;
LL x, res = 0, t = 1;
// 将数字i转二进制(高位到低位),统计前x位中的1(x会逐步消耗)
void check(LL i) {
vector<int> bin;
// 先把i转成二进制(低位在前)
while (i) {
bin.push_back(i % 2);
i /= 2;
}
// 从高位到低位遍历(逆序),消耗x并统计1
for (int j = bin.size() - 1; j >= 0 && x > 0; j--, x--) {
if (bin[j] == 1) res++;
}
}
// 快速幂计算2^k(替代pow,避免double精度问题)
LL pow2(LL k) {
LL ans = 1;
while (k--) ans <<= 1;
return ans;
}
int main() {
cin >> x;
// 跳过bin(0)(仅1位0,无1)
if (x >= 1) x--;
// 整段处理:按二进制位数t分段统计
while (x >= t * pow2(t - 1)) {
// 计算当前t位段中1的总数
LL cnt1 = pow2(t - 1); // 最高位的1的数量
LL cnt2 = (t - 1) * pow2(t - 2); // 剩余t-1位的1的数量
res += cnt1 + cnt2;
// 消耗当前段的总位数
x -= t * pow2(t - 1);
// 处理下一个长度的二进制数
t++;
}
// 处理剩余不足整段的部分:逐个数字拼接统计
for (LL i = pow2(t - 1); x > 0; i++) {
check(i);
}
cout << res << endl;
return 0;
}
代码关键解析
- pow2函数:替代系统pow(返回double),用位运算实现2的幂次,避免大数精度丢失(比如t=60时,2^59用double会丢失精度);
- check函数:核心是"逐位消耗x"------把数字转二进制后,从高位到低位遍历,每遍历一位就把x减1,若该位是1则答案加1,直到x为0;
- 整段处理的while循环:核心逻辑,用数学公式批量统计整段的1,时间复杂度极低(t最多到60,因为2^60已超过1e18);
- 剩余部分的for循环:不足整段时,暴力拼接二进制位,由于剩余x最多是t×2^(t-1)(t≤60),这部分循环次数极少,不会超时。
四、样例验证(输入7)
我们用样例输入7走一遍流程,验证结果:
- 初始x=7 → 跳过bin(0),x=6;
- 整段处理:
- t=1:t×2^(t-1)=1×1=1 ≤ 6 → 统计1位段的1(仅数字1,二进制
1,1个1)→ res=1,x=6-1=5,t=2; - t=2:t×2^(t-1)=2×2=4 ≤5 → 统计2位段的1(数字2
10、311,总共有1+2=3个1)→ res=1+3=4,x=5-4=1,t=3; - t=3:t×2^(t-1)=3×4=12 >1 → 退出整段循环;
- t=1:t×2^(t-1)=1×1=1 ≤ 6 → 统计1位段的1(仅数字1,二进制
- 剩余部分处理:
- t=3,起始数是4(2^(3-1)=4),调用check(4):4的二进制是
100; - 遍历高位到低位:第一位是1 → res=4+1=5,x=1-1=0 → 结束;
- t=3,起始数是4(2^(3-1)=4),调用check(4):4的二进制是
- 最终输出res=5,与样例一致。
五、注意事项与优化
- 数据类型 :全程必须用
long long,int最多存到2e9,无法处理1e18; - pow精度问题 :原代码用
pow(2, t-1)存在隐患,比如t=60时,2^59的double表示会丢失低位,必须用位运算/快速幂替代; - 边界条件 :
- x=1:仅bin(0),输出0;
- x=2:bin(0)+bin(1) →
01,输出1; - x=0:题目规定x是正整数,无需处理;
- 时间复杂度:整段处理的循环次数≤60(2^60>1e18),剩余部分循环次数≤60,整体时间复杂度O(1),完全满足2秒时限。
六、总结
这道题的核心是**"避暴力、找规律、分段算"**:
- 利用二进制数的长度分布规律,把无限长的01串拆成若干段;
- 对每一段用数学公式批量统计1的数量,避免逐位遍历;
- 仅对最后不足整段的部分暴力补全,保证效率。
这种"分段统计+数学公式"的思路,是蓝桥杯大数问题的高频解法,掌握后能解决一类"无限序列统计"问题。建议大家动手跑一遍代码,修改不同的x值(比如x=10、x=1e5),加深对思路的理解。