【GESP】C++五级真题(前缀和思想考点) luogu-P14074 [GESP202509 五级] 有趣的数字和
GESP C++ 2025年9月五级真题,前缀和考点,题目难度⭐⭐⭐☆☆,五级来说难度适中。洛谷难度等级普及/提高−
luogu-P14074 [GESP202509 五级] 有趣的数字和
题目要求
题目背景
为保证只有时间复杂度合理的算法通过本题,本题时限下调。
题目描述
如果一个正整数的二进制表示包含奇数个 $1$,那么小 A 就会认为这个正整数是有趣的。
例如,$7$ 的二进制表示为 $(111)_2$,包含 $1$ 的个数为 $3$ 个,所以 $7$ 是有趣的。但是 $9=(1001)_2$ 包含 $2$ 个 $1$,所以 $9$ 不是有趣的。
给定正整数 $l,r$,请你统计满足 $l\le n\le r$ 的有趣的整数 $n$ 之和。
输入格式
一行,两个正整数 $l,r$,表示给定的正整数。
输出格式
一行,一个正整数,表示 $l,r$ 之间有趣的整数之和。
输入输出样例 #1
输入 #1
1
3 8
输出 #1
1
19
输入输出样例 #2
输入 #2
1
65 36248
输出 #2
1
328505490
说明/提示
【数据范围】
对于 $40\%$ 的测试点,保证 $1\le l\le r\le 10^4$。
对于另外 $30\%$ 的测试点,保证 $l=1$ 并且 $r=2^k-1$,其中 $k$ 是大于 $1$ 的正整数。
对于所有测试点,保证 $1 \le l\le r\le 10^9$。
【提示】
由于本题的数据范围较大,整数类型请使用 long long。
题目分析
这是一道典型的数位 DP(Digit DP)题目。
1. 问题转化
题目要求计算区间 $[l, r]$ 内所有二进制表示中含有奇数个 $1$ 的整数之和。 根据前缀和的思想,区间 $[l, r]$ 的答案可以转化为: \(\text{Answer} = \text{solve}(r) - \text{solve}(l-1)\) 其中 $\text{solve}(n)$ 表示在区间 $[1, n]$ 中满足条件的整数之和。
2. 暴力法分析
最直接的方法是遍历 $l$ 到 $r$ 之间的每一个数,统计其二进制中 $1$ 的个数。如果 $1$ 的个数是奇数,则累加该数。 对于每个数,统计 $1$ 的个数需要 $O(\log n)$ 的时间。总时间复杂度为 $O((r - l) \log r)$。
题目给出的数据范围是 $l, r \le 10^9$。如果 $r - l$ 很大(例如接近 $10^9$),总运算量将达到 $10^{10}$ 级别,远远超出一般评测机 1 秒约 $10^8$ 次运算的限制,因此暴力法只能通过部分小数据测试点,必定会超时。
3. 数位 DP 解法:手把手教你“试填法”
数位 DP 听起来很高大上,其实核心逻辑就是一位一位地试填数字。我们可以把它想象成在一个二叉树上走迷宫。
一、 核心思想:二叉树上的抉择
假设我们要找 $[0, n]$ 之间符合条件的数。把 $n$ 转为二进制,比如 $n = 6$ (二进制 110)。我们从最高位(第 2 位)开始,每一位都有两个选择:填 0 或者填 1。这就像走迷宫,每次遇到岔路口(0 或 1),我们都要做一个决定,直到走到最底层(第 0 位)。
二、 关键逻辑详解:Limit(上界限制)是什么?
这是数位 DP 中最难理解也最精妙的地方。我们用 $n=6$ (二进制 110) 来举例演示。
我们要构造一个数 $x$,要求 $x \le n$。
第 2 位(最高位):$n$ 的第 2 位是 1。
- 选择填 0:构成的数是
0??。不管后面问号填什么(最大是011即 3),一定比110(6) 小。- -> 限制解除!从下一位开始,想填 0 就填 0,想填 1 就填 1,完全自由。
- 选择填 1:构成的数是
1??。这就“顶格”了,贴着 $n$ 走了。- -> 限制继续!下一位不能随便填,最大只能填 $n$ 的下一位。
第 1 位:
- 如果在上一位我们填了 0(限制已解除),这一位随便填 0 或 1。
- 如果在上一位我们填了 1(限制仍在),$n$ 的第 1 位是 1。
- 我们填 0 -> 变成
10?,比110小,限制解除。 - 我们填 1 -> 变成
11?,又顶格了,限制继续。
- 我们填 0 -> 变成
通俗口诀:
“只要这一位填的小于 $n$ 对应的位,后面就自由了;如果这一位填的和 $n$ 一样,后面还得受限制”
三、 递归函数设计 (DFS)
为了让程序自动完成这个过程,我们设计一个递归函数 dfs。它需要三个关键参数:
(1) pos (我在哪?)
- 告诉函数当前正在处理第几位。比如
pos=2表示在处理最高位。 - 当
pos < 0时,说明所有位都填完了,我们可以根据结果判断这个数是不是“有趣的”。
(2) parity (现在的状态是啥?)
- 题目只关心 1 的个数是奇数。
- 我们带个记号
parity往下走。 - 遇到
0,记号不变;遇到1,记号翻转(0变1,1变0)。 - 走到尽头时,看一眼
parity是 1 还是 0,就知道这个数合不合格。
(3) limit (还能随便填吗?)
- 就是上面解释的“上界限制”。
true代表受限,只能填到 $n$ 当前位的数字;false代表自由,可以填 0 或 1。
四、 进阶:不仅要数数,还要算和
普通的数位 DP 只需要返回“有多少个满足条件的数”。但这道题要求“所有满足条件的数的和”。这需要一点数学推导。
场景模拟:
假设当前在第 pos 位,我们决定填数字 d(d 是 0 或 1)。然后递归调用下一层,下一层告诉我们:“在你的这个决定下,后面有 cnt 个合法的数,它们后面的部分加起来和是 sum。”
那么,当前这一位填的 d 对总和的贡献是多少呢?
- 当前位的价值:当前位是第
pos位,填数字d代表的数值是 $d \times 2^{pos}$。 - 出现了多少次:这个前缀会拼上
cnt种不同的后缀,所以这个数值出现了cnt次。 - 公式:
\(\text{当前位贡献} = (d \times 2^{pos}) \times cnt\) \(\text{当前分支总和} = \text{当前位贡献} + \text{后缀累加和(sum)}\)
五、 总结
- 拆分:把 $n$ 拆成二进制数组。
- 搜索:从最高位开始
dfs,维护limit和parity。 - 记忆:如果当前状态没有限制 (
!limit),就把结果记下来,下次直接用。
时间复杂度
- 每个位上只有 0 和 1 两种选择,所以状态数量是 $O(2 \times \log n) = O(\log n)$。
- 每个状态的计算量是 $O(1)$,因为只需要简单的位运算和乘法。
所以,整体时间复杂度是 $O(\log n)$。二进制位最多约 30-60 位,状态数量极少,计算非常高效。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
* P14074 [GESP202509 五级] 有趣的数字和
* 题目要求:计算 [l, r] 区间内二进制表示中包含奇数个 1 的整数之和。
* 算法:数位 DP
*/
#include <iostream>
typedef long long ll;
// Result 结构体用于存储 DP 的结果
struct Result {
ll cnt; // 满足条件的数的个数
ll sum; // 满足条件的数的和
};
// digits 存储数字 n 的二进制每一位,digits[0] 是最低位
int digits[40];
// memo 用于记忆化搜索,memo[pos][parity] 表示处理到第 pos 位,当前 1
// 的个数奇偶性为 parity 时的结果 parity = 0 表示偶数个 1,parity = 1 表示奇数个
// 1
Result memo[40][2];
// dfs 函数进行数位 DP
// pos: 当前处理的二进制位(从高位到低位)
// parity: 当前已经确定的高位中 1 的个数的奇偶性(0 为偶,1 为奇)
// limit: 是否受到上界限制。true 表示当前位只能取 0 到 digits[pos],false
// 表示可以取 0 或 1
Result dfs(int pos, int parity, bool limit) {
// 递归边界:处理完所有位(pos < 0)
if (pos == -1) {
// 如果最终 1 的个数是奇数(parity == 1),则这是一个“有趣的数”
if (parity == 1) {
return {1, 0}; // 返回个数 1,和 0(因为具体的数值在回溯时计算)
} else {
return {0, 0}; // 不是有趣的数
}
}
// 记忆化搜索:如果不再受限且已经计算过,直接返回结果
if (!limit && memo[pos][parity].cnt != -1) {
return memo[pos][parity];
}
// 确定当前位的枚举上限
int up = limit ? digits[pos] : 1;
Result res = {0, 0};
// 枚举当前位可能的取值 i (0 或 1)
for (int i = 0; i <= up; i++) {
// 递归处理下一位
// pos - 1: 下一位
// parity ^ i: 更新奇偶性。如果 i=0,奇偶性不变;如果 i=1,奇偶性翻转
// limit && (i == up):
// 更新限制状态。只有当前受限且选了上限,下一位才继续受限
Result sub = dfs(pos - 1, parity ^ i, limit && (i == up));
// 累加符合条件的数的个数
res.cnt += sub.cnt;
// 计算当前位对和的贡献
// 当前位的值是 i * 2^pos
ll cur_val = (ll)i << pos;
// 当前位贡献的总和 = 当前位的值 * 后续满足条件的数的个数 +
// 后续满足条件的数的和
res.sum += sub.sum + cur_val * sub.cnt;
}
// 如果不受限,记录结果到 memo
if (!limit) {
memo[pos][parity] = res;
}
return res;
}
// solve 函数计算 [0, n] 范围内有趣的数的和
ll solve(ll n) {
int len = 0;
// 将 n 转换为二进制存入 digits 数组
while (n) {
digits[len] = n & 1;
n >>= 1;
len++;
}
// 初始化 memo 数组为 -1,表示未计算
for (int i = 0; i < 40; i++) {
for (int j = 0; j < 2; j++) {
memo[i][j].cnt = -1;
}
}
// 从最高位开始 DFS
// 初始 limit 为 true,因为最高位受 n 的限制
// 初始 parity 为 0,因为还没选任何 1
return dfs(len - 1, 0, true).sum;
}
int main() {
// 定义左右边界 l 和 r,使用 long long 防止溢出
ll l, r;
std::cin >> l >> r;
// 利用前缀和思想:[l, r] 的和 = solve(r) - solve(l - 1)
std::cout << solve(r) - solve(l - 1) << std::endl;
return 0;
}
同样,本题之所以调低时间限制,就是因为很显然存在暴力模拟算法,可以直接算出答案,显然违反了出题者本意。示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
typedef long long ll;
int main() {
// 定义左右边界 l 和 r,使用 long long 防止溢出
ll l, r;
std::cin >> l >> r;
// ans 用于存储区间 [l, r] 内所有“有趣的整数”之和
ll ans = 0;
// 遍历区间 [l, r] 中的每一个整数 i
// 时间复杂度分析:
// 外层循环执行 (r - l + 1) 次。
// 内层 while 循环每次处理一个整数,执行次数为该整数的二进制位数,约为
// log2(r)。 对于 r <= 10^9,log2(r) 约为 30。 总时间复杂度为 O((r - l) *
// log r)。 当 r - l 很大(如 10^9)时,运算量约为 3 * 10^10,会超出通常 1
// 秒(约 10^8 次运算)的时间限制,导致超时 (TLE)。
// 该算法只能通过小数据范围的测试点 (40% 数据)。
for (ll i = l; i <= r; i++) {
int tmp = 0; // tmp 用于记录当前整数 i 的二进制表示中 1 的个数
ll num = i; // 复制 i 到 num,以免修改循环变量 i
// 计算 num 的二进制表示中 1 的个数
while (num > 0) {
// 如果 num 的最低位是 1 (即 num 是奇数)
if (num & 1) {
tmp++;
}
// 将 num 右移一位,相当于除以 2
num >>= 1;
}
// 如果 1 的个数是奇数,则认为该整数是“有趣的”
if (tmp % 2 == 1) {
ans += i; // 将该整数累加到总和 ans 中
}
}
// 输出最终结果
std::cout << ans << std::endl;
return 0;
}
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP 学习专题站:GESP WIKI
“luogu-”系列题目可在洛谷题库进行在线评测。
“bcqm-”系列题目可在编程启蒙题库进行在线评测。
欢迎加入:Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答
欢迎加入:C++ GESP/CSP认证学习QQ频道,考试资源总结汇总
欢迎加入:C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助
