「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 大的经典套路吧:

  1. 就是上面说的那种方法,每次确定第 \(j\) 大的区间范围,并且能用 RMQ 来维护。
  2. 第二种可以二分答案,这样显然可以降低难度对吧,然后问题就变化为了求个数问题。
  3. 就是比较常规方法了,用主席树,平衡树,KD-Tree等数据结构维护,这个似乎需要具体问题具体分析。

基本上这种问题都是用堆实现的。

posted @ 2021-03-13 19:50  longdie  阅读(164)  评论(0编辑  收藏  举报