「NOI2010」 超级钢琴(K大)
opening
经典题了,套路题,没做过就挺吃亏的,今天做了一下,确实跟今天 T1 的其中一种方法完全一样啊。
ouline
给定一个序列 \(a(-1000 <= a_i <= 1000)\) ,定义一个区间 \((l, r)\) 的价值为区间和,求最大的 K 个区间的 sum。
说白了就是求区间 K 大。
solution
第 K 大问题挺经典了吧,方法也不少,这里先讲关于这道题的吧。
首先区间最大和一定是以 \(i\) 为左端点的 最大值 中 最大的那一个。
如果我们能求出以 \(i\) 为左端点的 第 \(j\) 大值,那么这道题就可以做了。
我们定义以 \(i\) 为左端点的合法右端点区间为 \(L, R\),那么我们可以用 RMQ 求出最大值的位置 \(d\),然后我们把区间分成 \(L, d-1\) 和 \(d+1, R\) 两部分,易知次大值u一定是这两个区间的最大值之一,所以我们把这两个区间的次大值扔进去,后面反复重复该过程,知道求够 K 个为止。
显然这样做是对的吧,而且复杂度也是非常对劲的 \(O(nlogn)\),所以这道题就解决了。
std
// by longdie
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5 + 5;
ll ans;
int n, K, L, R, a[N], sum[N], f[N][20], id[N][20], lg[N];
struct node {
int val, x, l, r, p;
node() {}
node(int a, int b, int c, int d, int e) { val = a, x = b, l = c, r = d, p = e; }
friend bool operator < (const node &a, const node &b) { return a.val < b.val; }
} ; priority_queue<node> q;
inline int get(int l, int r, int &x) {
int k = lg[r - l + 1];
if(f[l][k] >= f[r-(1<<k)+1][k]) { x = id[l][k]; return f[l][k]; }
else { x = id[r-(1<<k)+1][k]; return f[r-(1<<k)+1][k]; }
}
signed main() {
scanf("%d%d%d%d", &n, &K, &L, &R);
lg[1] = 0, lg[2] = 1;
for(register int i = 3; i <= n; ++i) lg[i] = lg[i/2] + 1;
for(register int i = 1; i <= n; ++i) scanf("%d", &a[i]), sum[i] = sum[i-1] + a[i];
for(register int i = 1; i <= n; ++i) f[i][0] = sum[i], id[i][0] = i;
for(register int j = 1; j <= 18; ++j)
for(register int i = 1; (i+(1<<j)-1) <= n; ++i) {
if(f[i][j-1] >= f[i+(1<<j-1)][j-1]) id[i][j] = id[i][j-1], f[i][j] = f[i][j-1];
else id[i][j] = id[i+(1<<j-1)][j-1], f[i][j] = f[i+(1<<j-1)][j-1];
}
for(register int i = 1, d; i <= n; ++i) {
if(i + L - 1 > n) break;
int tmp = 0;
tmp = get(i + L - 1, min(n, i + R - 1), d) - sum[i-1];
q.push(node(tmp, i, i+L-1, min(n, i+R-1), d));
//cout << tmp << '\n';
}
for(register int i = 1, d; i <= K; ++i) {
node t = q.top(); q.pop();
ans += t.val;
int s = t.x, l = t.l, r = t.r, p = t.p;
if(p > l) {
int tmp = get(l, p-1, d) - sum[s-1];
q.push(node(tmp, s, l, p-1, d));
}
if(p < r) {
int tmp = get(p+1, r, d) - sum[s-1];
q.push(node(tmp, s, p+1, r, d));
}
}
cout << ans << '\n';
return 0;
}
extend
小编大概总结了一下 K 大的经典套路吧:
- 就是上面说的那种方法,每次确定第 \(j\) 大的区间范围,并且能用 RMQ 来维护。
- 第二种可以二分答案,这样显然可以降低难度对吧,然后问题就变化为了求个数问题。
- 就是比较常规方法了,用主席树,平衡树,KD-Tree等数据结构维护,这个似乎需要具体问题具体分析。
基本上这种问题都是用堆实现的。