First One
这道题是一道非常经典的**“枚举贡献 + 双指针”**问题。
核心难点在于公式中含有
1. 核心思路
第一步:转换视角(按 Log 值分层)
由于
我们可以遍历这个对数值
这等价于区间和
特殊情况处理:题目规定
- 当
时,目标区间和范围是 。 - 当
时,目标区间和范围是 。
第二步:使用双指针固定区间
对于一个固定的
由于数组元素非负,
我们需要维护两个指针(假设为 idx1 和 idx2):
idx1: 满足的最小下标。 idx2: 满足的最小下标。
那么对于当前的
第三步:数学求和
对于范围
2. 复杂度分析
- 外层循环枚举
:大约 35 次。 - 内层双指针扫描整个数组:
。 - 总时间复杂度:
,对于 的数据量,运算次数约 ,完全可以在 1 秒内通过。
3. C++ 代码实现
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
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
ll a[MAXN];
ll P[MAXN]; // 前缀和
void solve() {
int n;
if (!(cin >> n)) return;
P[0] = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
P[i] = P[i-1] + a[i];
}
ll total_ans = 0;
// 枚举 k,即 log2(S(i,j)) 的值
// S 的最大值是 10^10,log2(10^10) ≈ 33.2,枚举到 36 足够
for (int k = 0; k <= 36; ++k) {
// 计算当前 k 对应的区间和范围 [lower, upper)
ll lower, upper;
if (k == 0) {
lower = 0; // 特殊处理 log2(0)=0 的情况
upper = 2;
} else {
lower = (1LL << k);
upper = (1LL << (k + 1));
}
// 双指针寻找合法的 j 区间 [l, r-1]
// l: 第一个满足 sum >= lower 的位置
// r: 第一个满足 sum >= upper 的位置
// 合法区间即为 sum 在 [lower, upper) 之间
int l = 1, r = 1;
for (int i = 1; i <= n; ++i) {
// 确保 l 和 r 不小于当前起点 i
if (l < i) l = i;
if (r < i) r = i;
// 移动 l 指针,直到 P[l] - P[i-1] >= lower
while (l <= n && P[l] - P[i-1] < lower) {
l++;
}
// 移动 r 指针,直到 P[r] - P[i-1] >= upper
while (r <= n && P[r] - P[i-1] < upper) {
r++;
}
// 此时合法的 j 的范围是 [l, r - 1]
// 如果 l >= r,说明不存在合法的 j
if (l < r) {
// 计算区间 [l, r-1] 内有多少个 j
ll count = (r - 1) - l + 1;
// 计算区间内所有 (i + j) 的和
// sum = i * count + (l + (r-1)) * count / 2
ll sum_ij = i * count + (ll)(l + (r - 1)) * count / 2;
// 累加到总答案:(log值 + 1) * sum_ij
// 这里的 log值 就是 k (或者 k=0 时对应公式里的值也是1,即 k+1)
total_ans += (ll)(k + 1) * sum_ij;
}
}
}
cout << total_ans << endl;
}
int main() {
// 优化 I/O
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
if (cin >> t) {
while (t--) {
solve();
}
}
return 0;
}
代码细节解释:
-
区间范围 (
lower,upper):- 题目公式是
floor(log2(sum)) + 1。 - 如果
sum在之间, floor(log2)是,公式值是 。 - 特别地,如果
sum=0,题目说视为log2(0)=0,公式值是。如果 sum=1,log2(1)=0,公式值也是。所以 时我们将范围设为 ,权重为 ,逻辑是统一的。
- 题目公式是
-
双指针逻辑:
- 我们使用两个
while循环分别寻找区间的左边界l和右边界r。 l是第一个达标的(大于等于下界)。r是第一个超标的(大于等于上界)。- 所以有效的
是从 l到r-1。
- 我们使用两个
-
等差数列求和:
- 我们需要求
。 - 拆开来看就是
。 就是 。 是从 加到 ,直接用首项加末项乘以项数除以2。
- 我们需要求
理解
利用双指针在
这就是解题的“灵魂”所在。
这里有三个核心要素支撑着这个算法的正确性和高效性:
1. 核心变换:化“变量”为“常量”
原题的难点在于
在数组中寻找所有满足
的子区间。
2. 核心性质:单调性(非负整数)
双指针能跑
- 当我们固定左端点
,向右移动寻找 时,区间和是单调递增的。 - 当我们把左端点
向右移一格( ),区间和变小了。为了让和重新回到 这个目标范围,右端的指针 只能向右移,绝不需要回头。
正是因为 “右指针不回头” 这一特性,保证了对于每一个
3. 核心技巧:批量处理
如果不使用双指针,固定
总结
你的直觉很准:
这道题就是把 “求解 Log 值的和” 拆解成了 35 次“定值范围的区间查找”,然后用 双指针 这一武器对每次查找进行