[NOI2010] 超级钢琴
前缀和 + ST 表区间最值 + 堆分裂区间,贪心取前 k 大子数组和。
OJ: luogu
题目 ID: P2048
难度:NOI/NOI+/CTSC
标签:前缀和ST表堆贪心多路归并
日期: 2026-08-05 09:50
题意
超级钢琴弹奏出
超级和弦 = 编号连续的若干个音符,长度不少于
选
, - 保证存在满足要求的乐曲
思路
朴素做法
最直接的做法是枚举所有合法子数组,计算它们的和,排序后取前
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-05
* update_at: 2026-08-05
*/
// brute.cpp:小数据暴力解,枚举所有合法超级和弦,排序取前 k 个。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k, L, R;
cin >> n >> k >> L >> R;
vector<long long> a(n + 1), S(n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> a[i];
S[i] = S[i - 1] + a[i];
}
// 枚举所有合法超级和弦的和
vector<long long> sums;
for (int i = 1; i <= n; i++) {
int left = i + L - 1;
int right = min(i + R - 1, n);
for (int j = left; j <= right; j++)
sums.push_back(S[j] - S[i - 1]);
}
// 降序排序,取前 k 个
sort(sums.rbegin(), sums.rend());
long long ans = 0;
for (int i = 0; i < k; i++)
ans += sums[i];
cout << ans << "\n";
return 0;
}这个暴力能帮助我们确认目标:本题本质上是在所有长度属于
代数转化:固定左端点
连续音符序列
如果同时让
固定
解空间分裂:从最大到次大
只求每个左端点的最大值还不够,因为题目要全局前
堆中每个状态可以理解为:
:固定的左端点 :当前右端点 的可行搜索区间 :区间 中使 最大的下标
状态的贡献就是
弹出
如果某个子区间非空,就再用 RMQ 找出这个子区间内最大的
底层组件
需要两个核心组件:
- ST 表:维护前缀和数组
的区间最大值下标。注意这里要存“下标”,不是只存最大值,因为弹出后要用这个下标 j分裂区间。 - 大根堆:维护状态
Node{sum, i, j, l, r}。sum是当前状态的最大子数组和,j是当前区间中最优右端点,[l,r]是还没有被消费的右端点区间。
执行流程:
- 计算前缀和数组
。 - 建 ST 表,支持
查询区间内 最大的位置。 - 对每个合法左端点
,令初始右端点区间为 ,查询最优 后入堆。 - 重复
次:弹出堆顶并累加答案,再以弹出的 为断点分裂左右区间,查询后重新入堆。
样例推演
以官方样例
| 步骤 | 堆顶状态 | 弹出和 | 区间分裂 |
|---|---|---|---|
| 1 | |||
| 2 | 无分裂 | ||
| 3 | 无分裂 |
弹出
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-05
* update_at: 2026-08-05
*/
// 前缀和 + ST 表 RMQ + 堆:每次弹出当前最大子段和,分裂区间后补入次大。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
const int LOG = 20;
int n, k, L, R;
long long S[MAXN]; // 前缀和
int st[MAXN][LOG]; // ST 表存下标
int lg[MAXN]; // 预处理 log
void build_st() {
for (int i = 1; i <= n; i++) st[i][0] = i;
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
int a = st[i][j - 1];
int b = st[i + (1 << (j - 1))][j - 1];
st[i][j] = (S[a] >= S[b]) ? a : b;
}
}
// 返回 [l, r] 中 S 值最大的下标
int query(int l, int r) {
int j = lg[r - l + 1];
int a = st[l][j], b = st[r - (1 << j) + 1][j];
return (S[a] >= S[b]) ? a : b;
}
struct Node {
long long sum; // 该区间的最优和
int i; // 起始位置
int j; // 最优结束位置
int l, r; // 当前区间 [l, r]
bool operator<(const Node& o) const { return sum < o.sum; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k >> L >> R;
S[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> S[i];
S[i] += S[i - 1];
}
// 预处理 log
lg[1] = 0;
for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
build_st();
// 大根堆:每个起始位置维护一个最优区间
priority_queue<Node> pq;
for (int i = 1; i <= n; i++) {
int l = i + L - 1;
int r = min(i + R - 1, n);
if (l > r) continue;
int j = query(l, r);
pq.push({S[j] - S[i - 1], i, j, l, r});
}
long long ans = 0;
for (int t = 0; t < k; t++) {
Node node = pq.top();
pq.pop();
ans += node.sum;
// 分裂:左半 [l, j-1]
if (node.l < node.j) {
int jj = query(node.l, node.j - 1);
pq.push({S[jj] - S[node.i - 1], node.i, jj, node.l, node.j - 1});
}
// 分裂:右半 [j+1, r]
if (node.j < node.r) {
int jj = query(node.j + 1, node.r);
pq.push({S[jj] - S[node.i - 1], node.i, jj, node.j + 1, node.r});
}
}
cout << ans << "\n";
return 0;
}复杂度
ST 表建表
ST 表占用
总结
本题的核心不是 ST 表本身,而是“前缀和建模 + 固定左端点 + 区间最优值弹出后分裂”的组合模型。
看到“连续子段和 / 路径异或和”“区间长度限制
《序列合并》是最基础的有序流归并;《超级钢琴》把“下一项”升级为“分裂后的子区间最优项”;《异或粽子》则把 RMQ 换成可持久化 01-Trie 查询第
图示解析
下面这张图展示样例中每个起点的初始右端点区间,以及堆弹出后的分裂路线:
前缀和 S: 0 3 5 -1 7
下标: 0 1 2 3 4
起始 i=1: j∈[2,3] S最大=5 (j=2) → 弹出后分裂 [3,3]
起始 i=2: j∈[4,4] S最大=7 (j=4) → 只有一个候选
起始 i=3: j∈[4,4] S最大=7 (j=4) → 只有一个候选
堆弹出: 5(行1) → 4(行2) → 2(行3) = 11每个起始位置对应一组右端点候选,堆维护每组当前能给出的最大子数组和。弹出一个候选后,原来的右端点区间被断点分裂,左右两边继续贡献新的候选。这样就能在不枚举所有子数组的情况下,按从大到小的顺序取出前
