First One
暴力
3分钟写个暴力,验证一下有没有读错题目
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
#include <bits/stdc++.h>
#include <cmath>
using namespace std;
typedef long long ll;
const int maxn = 1e5+6;
ll a[maxn];
int n;
ll range_sum(int l,int r){ return a[r] - a[l-1]; }
void init() {
std::cin >> n;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
a[i] += a[i-1]; //前缀和
}
}
ll log2(int l,int r) {
ll sum = range_sum(l, r);
return log(sum)/ log(2);
}
ll baoli() {
ll ret = 0;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
for(int j = i;j <= n ;++j ) // j: 1->n
{
ret += (log2(i,j)+1) * (i+j);
}
}
return ret;
}
int main (int argc, char *argv[]) {
int T;
std::cin >> T;
while (T--) {
init();
cout << baoli() << endl;
}
return 0;
}
思考
根据公式, 必须要枚举区间
发现: 不是让直接求区间和,而是求
第一步:转换视角(按 Log 值分层)
由于
我们可以遍历这个对数值
这等价于区间和
特殊情况处理:题目规定
这说明当我们枚举区间和为
对应的公式值为 1。此时
, 可以知道,当我们规定 时,我枚举的
所以综上,所以我们修正一下区间的定义:
- 当
时,目标区间和是 ,此时得到 - 当
时,目标区间和是 ,此时得到 - 结合上两条:
,此时得到的 - 当
时,目标区间和是 ,此时得到
我们在把原数组a 转成前缀和数组
第二步:使用双指针固定区间
对于一个固定的
由于数组元素非负,
我们需要维护两个指针(假设为 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
// 发现 ai >=0, 那么可以不用前缀和数组
#include <bits/stdc++.h>
#include <cmath>
using namespace std;
typedef long long ll;
const int maxn = 1e5+6;
ll a[maxn]; // 前缀和
ll P[maxn]; // 前缀和
int n;
void init() {
std::cin >> n;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
P[i] = a[i] +P[i-1];
}
}
ll sum(int l,int r) {
return P[r] - P[l-1];
}
void work() {
ll ans = 0;
for(int k = 0;k<=34;k++) {
// 坑点
ll lower_bound = (1ull << ll(k));
if( k == 0) lower_bound = 0;
ll up_bound = (1ull << ll(k+1) );
ll j1 = 0,j2 = 0;
// 枚举起点
for(ll i = 1 ; i <= n;i++) {
j1 = std::max(j1,i);
// ll sum = P[j1].val - P[i].val;
// 第一个 sum >= lower_bound的位置
while(sum(i,j1) < lower_bound && j1 <=n) j1++;
j2 = std::max(j2,j1);
// 第以个 sum >= up_bound的位置
while(sum(i,j2) < up_bound && j2 <=n ) j2++;
// 没有超过边界
if( j1 <= n) {
// cout << "k = " << k << " " ;
// cout << "i = " << i << ' ';
// cout << "j1 = " << j1 << ' ';
// cout << "j2 = " << j2 << ' ';
ll ij_plus_sum = i * (j2 - j1) + (j2-1+j1) * (j2-j1) / 2;
ll sum_k = (k+1) * ij_plus_sum;
// cout
ans += sum_k;
// cout << "sum_k = " << sum_k << " \n";
}
}
}
std::cout << ans << "\n";
}
int main (int argc, char *argv[]) {
int T;
std::cin >> T;
while (T--) {
init();
work();
}
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 次“定值范围的区间查找”,然后用 双指针 这一武器对每次查找进行