【题解】[NOI2010] 超级钢琴
碾压我智商
注意我们怎么用堆:
对于一个方案数极大的情况,我们要选前 K 大
显然我们不能一开始就把所有方案塞到堆里
我们可以考虑确定一种子结构的形式,对所有方案,某个较优解可以通过这个结构“引出”对应的次优解,于是弹出、插入地做下去
一般地,这个结构可以(最好)是一个树(森林),比如对于区间的递归二划分;每个节点的出度最好是个常数
同时,我们只需要保证这个结构形式包含了所有情况,且具有答案上的次序性
然后我们只需要一开始往堆里塞若干“根节点”,就可以开始跑了
回到这题,求连续子段的前 K 大
考虑前缀和 sum ,设 \(m = poi(p, l, r)\) 表示 \(p\) 为左端点,右端点区间为 \([l, r]\) ,取到最大区间和时的位置
那么这个答案引出的次优解是 \(poi(p, l, m-1)\) 和 \(poi(p, m+1, r)\)
至于取到最大区间和,就是 \([l, r]\) 最大 \(sum\) 的位置,最大区间和即 \(sum[m]-sum[p-1]\)
然后一开始只需要塞入取值为 1~N 的 \(p\) 和对应满区间的“根节点”就行了
和 ST 表结合很是优美
#include <queue>
#include <cstdio>
using namespace std;
typedef long long ll;
const int MAXN = 500005;
int N, K, L, R, A[MAXN], ST[MAXN][21], poi[MAXN][21], lg[MAXN];
struct node {
int p, l, r, v;
bool operator < (const node &o) const {
return v < o.v;
}
};
priority_queue<node> Q;
int min(int x, int y) { return x < y ? x : y; }
int max(int x, int y) { return x > y ? x : y; }
int mx(int l, int r)
{
if (ST[l][lg[r-l+1]]>=ST[r-(1<<lg[r-l+1])+1][lg[r-l+1]])
return poi[l][lg[r-l+1]];
else return poi[r-(1<<lg[r-l+1])+1][lg[r-l+1]];
}
int main()
{
scanf("%d%d%d%d", &N, &K, &L, &R);
for (int i=1; i<=N; i++) { scanf("%d", &A[i]); A[i] += A[i-1]; }
for (int i=1; i<=N; i++) lg[i] = lg[i-1] + ((1<<(lg[i-1]+1))==i);
for (int i=1; i<=N; i++) ST[i][0] = A[i], poi[i][0] = i;
for (int o=1; o<=lg[N]; o++) {
int d = 1<<o, dd = 1<<(o-1);
for (int i=1; i+d-1<=N; i++) {
if (ST[i][o-1]>=ST[i+dd][o-1])
ST[i][o] = ST[i][o-1], poi[i][o] = poi[i][o-1];
else ST[i][o] = ST[i+dd][o-1], poi[i][o] = poi[i+dd][o-1];
}
}
for (int i=1; i<=N; i++) {
if (i+L-1> N) break;
int l = i+L-1, r = min(N, i+R-1);
Q.push((node) { i, l, r, A[mx(l, r)]-A[i-1] });
}
ll ans = 0;
for (; K; K--) {
node x = Q.top(); Q.pop(), ans += x.v;
int m = mx(x.l, x.r);
if (m-1>=x.l) Q.push((node) {x.p, x.l, m-1, A[mx(x.l, m-1)]-A[x.p-1] });
if (m+1<=x.r) Q.push((node) {x.p, m+1, x.r, A[mx(m+1, x.r)]-A[x.p-1] });
}
printf("%lld", ans);
}