把包含位置且长度受限的区间转成前缀和坐标中的梯形区域,用 ST 表和单调队列线性求每个询问。
OJ: luogu
题目 ID: P14638
难度:省选/NOI-
标签:数据结构ST表单调队列前缀和
日期: 2026-06-22 20:34
题意
给定长度为 n 的序列 a。每次询问给出长度范围 [L,R]。
对每个位置 i,令 k_i 表示所有包含 i、且长度在 [L,R] 内的子区间中,区间和的最大值。
样例输出对应的聚合方式是:
text
(1*k_1) xor (2*k_2) xor ... xor (n*k_n)使用 unsigned 64 位输出。
思路
先看一个可以直接验证想法的朴素解:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n, q;
long long a[MAXN], prefix_sum[MAXN];
long long range_sum(int l, int r) {
return prefix_sum[r] - prefix_sum[l - 1];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
prefix_sum[i] = prefix_sum[i - 1] + a[i];
}
cin >> q;
while (q--) {
int L, R;
cin >> L >> R;
unsigned long long result = 0;
for (int i = 1; i <= n; i++) {
long long best = -(1LL << 60);
for (int l = 1; l <= i; l++) {
for (int r = i; r <= n; r++) {
int len = r - l + 1;
if (L <= len && len <= R) {
best = max(best, range_sum(l, r));
}
}
}
long long product = best * (long long)i;
result ^= (unsigned long long)product;
}
cout << result << '\n';
}
return 0;
}暴力会对每个位置枚举所有包含它的区间,检查长度并取最大区间和。这个做法清楚,但单个询问就可能达到二次复杂度。
设前缀和为:
text
s[0] = 0
s[i] = a[1] + ... + a[i]区间 [l,r] 可以写成前缀坐标:
text
x = l - 1, y = r
sum(l,r) = s[y] - s[x]它包含位置 i,并且长度在 [L,R] 内,等价于:
text
x < i <= y
L <= y - x <= R所以对固定 i,问题是在一个梯形区域里最大化 s[y] - s[x]。
代码把这个梯形拆成几个可以快速处理的块:
- 固定
x后,y的范围是一段区间,用 ST 表查询max s[y],再用单调队列维护当前位置可用的x; - 固定
y的块类似,用 ST 表查询min s[x]; - 中间块中,
x和y的范围可以分开,答案就是max s[y] - min s[x]。
这样每个询问只需要线性扫描若干遍数组。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50005;
const int LOG = 17;
const long long INF = (1LL << 62);
int n, q;
long long prefix_sum[MAXN];
long long max_st[LOG][MAXN], min_st[LOG][MAXN];
int lg2_value[MAXN];
long long best_left[MAXN], best_right[MAXN], best_start[MAXN], answer_pos[MAXN];
int que[MAXN];
void build_st() {
lg2_value[1] = 0;
for (int i = 2; i <= n + 1; i++) {
lg2_value[i] = lg2_value[i >> 1] + 1;
}
for (int i = 0; i <= n; i++) {
max_st[0][i] = prefix_sum[i];
min_st[0][i] = prefix_sum[i];
}
for (int j = 1; j < LOG; j++) {
int len = 1 << j;
for (int i = 0; i + len - 1 <= n; i++) {
max_st[j][i] = max(max_st[j - 1][i], max_st[j - 1][i + (len >> 1)]);
min_st[j][i] = min(min_st[j - 1][i], min_st[j - 1][i + (len >> 1)]);
}
}
}
long long range_max_prefix(int l, int r) {
l = max(l, 0);
r = min(r, n);
if (l > r) {
return -INF;
}
int len = r - l + 1;
int lg = lg2_value[len];
return max(max_st[lg][l], max_st[lg][r - (1 << lg) + 1]);
}
long long range_min_prefix(int l, int r) {
l = max(l, 0);
r = min(r, n);
if (l > r) {
return INF;
}
int len = r - l + 1;
int lg = lg2_value[len];
return min(min_st[lg][l], min_st[lg][r - (1 << lg) + 1]);
}
void push_max_queue(int &head, int &tail, long long value_array[], int pos) {
while (head <= tail && value_array[que[tail]] <= value_array[pos]) {
tail--;
}
que[++tail] = pos;
}
unsigned long long solve_query(int L, int R) {
for (int i = 0; i <= n + 2; i++) {
best_left[i] = -INF;
best_right[i] = -INF;
best_start[i] = -INF;
answer_pos[i] = -INF;
}
// 右侧平行四边形:固定左前缀位置 x,右端点在 [x+L, x+R]。
for (int x = 0; x + L <= n; x++) {
best_start[x] = range_max_prefix(x + L, x + R) - prefix_sum[x];
}
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
while (head <= tail && que[head] < i - L) {
head++;
}
push_max_queue(head, tail, best_start, i - 1);
answer_pos[i] = best_start[que[head]];
}
int half = R - L + 1;
if (half & 1) {
half++;
}
half >>= 1;
// 左侧三角形拆出来的第一块。
for (int x = 0; x + L + half <= n; x++) {
best_left[x] = range_max_prefix(x + L + half, x + R) - prefix_sum[x];
}
head = 1;
tail = 0;
for (int i = L; i <= n; i++) {
int expired = i - L - half;
int add_pos = i - L;
while (head <= tail && que[head] <= expired) {
head++;
}
push_max_queue(head, tail, best_left, add_pos);
if (head <= tail) {
answer_pos[i] = max(answer_pos[i], best_left[que[head]]);
}
}
// 左侧三角形拆出来的第二块。
for (int y = L + half; y <= n; y++) {
best_right[y] = prefix_sum[y] - range_min_prefix(y - R, y - L - half);
}
head = 1;
tail = 0;
for (int i = L + 1; i <= n; i++) {
int add_pos = i + half - 1;
while (head <= tail && que[head] < i) {
head++;
}
if (add_pos <= n) {
push_max_queue(head, tail, best_right, add_pos);
}
if (head <= tail) {
answer_pos[i] = max(answer_pos[i], best_right[que[head]]);
}
}
// 中间正方形:左右端点可以分开取最优前缀最大值和最小值。
for (int i = L; i <= n; i++) {
long long value = range_max_prefix(i, i + half - 1)
- range_min_prefix(i - L - half + 1, i - L);
answer_pos[i] = max(answer_pos[i], value);
}
unsigned long long result = 0;
for (int i = 1; i <= n; i++) {
long long product = answer_pos[i] * (long long)i;
result ^= (unsigned long long)product;
}
return result;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
prefix_sum[i] = prefix_sum[i - 1] + x;
}
build_st();
cin >> q;
while (q--) {
int L, R;
cin >> L >> R;
cout << solve_query(L, R) << '\n';
}
return 0;
}复杂度
预处理前缀和的最大/最小 ST 表需要:
text
O(n log n)每个询问
text
O(n log n + nq)空间复杂度为
总结
本题的关键不是直接枚举区间,而是把区间转成前缀和坐标中的点 (x,y)。
包含位置和长度限制共同形成一个梯形区域。把这个区域拆成几块后,每块都能用 ST 表和单调队列在线性时间内更新所有 k_i。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
