洛谷P2048 [NOI2010]超级钢琴 题解
2019/11/14 更新日志:
近期发现这篇题解有点烂,更新一下,删繁就简,详细重点。代码多加了注释。就酱紫啦!
正解步骤
-
我们需要先算美妙度的前缀和,并初始化RMQ。
-
循环 \(i\) 从 \(1\) 到 \(n\) ,因为以i为起点的 和弦 终点必定是 \(i + L - 1\) 到 \(i + R - 1\) 之间,所以只要在区间内用RMQ取 超级和弦 ,并加入以美妙度从小排到大的优先队列中。
-
取出堆顶元素,将美妙度加入 \(ans\) ,并将元素切为从 (当前元素的左边界 到 当前元素终点 - 1) 和 (\(当前元素终点 + 1 到 当前元素右边界\)) 两个部分,并再次加入优先队列,依次进行 \(k\) 次。
-
输出答案即可
为什么要使用前缀和 and RMQ?
数据范围是500000,很明显,为了优化需要前缀和 and RMQ。
前缀和最明显的用处,是可以优化一个用来循环累加和的 \(n\) 。而 \(RMQ\) ,显然区间最值符合题目要求。
这两个算法询问答案都是O(1)。前缀和后面减去前面,RMQ只需要初始化一下,然后O(1)询问即可。
为什么第三步要切开元素并放入优先队列? 直接累加前 \(k\) 个元素不行么?
首先,我们肯定可以确定:优先队列中第一大的和弦一定是 \(全局\) 最大的和弦。 不要问我怎么证明
那么优先队列中第二大的和弦一定是 \(全局\) 次大的和弦么?这就不一定了。
所以我们需要切开元素并放入优先队列,保证每次取出来的元素一定是全局大小排名的元素
自己拿出纸和笔,结合题解自己思考,在草稿纸上演算一下,就懂了
那我就把解题思路放上吧233
实在还有问题,私信本人233
感谢老师 @apple365 的思路指引。
AC代码
#include<bits/stdc++.h>
#include<cctype>
#pragma GCC optimize(2)
#define in(a) a = read()
#define out(a) write(a)
#define outn(a) out(a),putchar('\n')
#define ll long long
#define Min(a,b) a < b ? a : b
#define Max(a,b) a > b ? a : b
#define rg register
#define New ll
using namespace std;
namespace IO_Optimization{
inline New read()
{
New X = 0,w = 0;
char ch = 0;
while(!isdigit(ch))
{
w |= ch == '-';
ch=getchar();
}
while(isdigit(ch))
{
X = (X << 3) + (X << 1) + (ch ^ 48);
ch = getchar();
}
return w ? -X : X;
}
inline void write(New x)
{
if(x < 0) putchar('-'),x = -x;
if(x > 9) write(x/10);
putchar(x % 10 + '0');
}
#undef New
}
using namespace IO_Optimization;//上面一坨优化的东西不用在意
const int MAXN = 500000 + 2;//定义常亮
int n,k,L,R;
int sum[MAXN],lg[MAXN],dp[MAXN][20],pos[MAXN][20];
// 前缀和 lg2值
//dp[i][j]表示i的2^j次方祖先 pos数组来记录最佳位置
ll ans;
struct Node
{
int start, left, right, t, val;
//超级和弦的起点 左、右边界 最值位置 最值
bool operator < (const Node &next) const
{
return val < next.val; //最值从大到小排序
}
};
inline void RMQ_init() //预处理
{
for(rg int j = 1;j <= 20; ++j)
for(rg int i = 1;i + (1 << j) - 1 <= n; ++i)
{
if(dp[i][j - 1] > dp[i + (1 << (j - 1))][j - 1])
{
dp[i][j] = dp[i][j - 1];
pos[i][j] = pos[i][j - 1];//更新最值位置
}
else
{
dp[i][j] = dp[i + (1 << (j - 1))][j - 1];
pos[i][j] = pos[i + (1 << (j - 1))][j - 1];
}
}
return;
}
inline int RMQ_query(int l, int r) //返回最值的位置
{
int t, tmp = lg[r - l + 1];
if(dp[l][tmp] > dp[r - (1 << tmp) + 1][tmp])
t = pos[l][tmp];
else t = pos[r - (1 << tmp) + 1][tmp];
return t;
}
int main()
{
in(n),in(k),in(L),in(R);
lg[0] = -1;//lg2(0) = -1,方便后面预处理lg2值
for(rg int i = 1;i <= n; ++i)
{
int a = read();//读入音符
sum[i] = sum[i - 1] + a; //前缀和
lg[i] = lg[i >> 1] + 1; //预处理lg2值
dp[i][0] = sum[i];
pos[i][0] = i; //初始化最大值的位置
}
RMQ_init();//初始化
priority_queue<Node> pq; //定义优先队列
for(rg int i = 1;i + L - 1 <= n; ++i) //计算每个位置最大的超级和弦
{
int t = RMQ_query(i + L - 1, Min(n, i + R - 1));
Node cur;
cur.val = sum[t] - sum[i - 1]; //由前缀和取最大值
cur.t = t;
cur.start = i; //当前超级和弦的起始位置
cur.left = i + L - 1; //当前的左边界
cur.right = Min(n,i + R - 1); //当前的右边界
pq.push(cur); //入堆
}
for(rg int i = 1;i <= k; ++i) //取k次堆顶的值
{
Node cur = pq.top();
pq.pop();
ans = ans + cur.val; //累加结果
Node next;
if(cur.t > cur.left) //当前取最值的位置 大于 当前和弦的 左边界
{
next.start = cur.start;
next.left = cur.left;
next.right = cur.t - 1; //新的右边界
next.t = RMQ_query(next.left, next.right);
next.val = sum[next.t] - sum[next.start - 1];
pq.push(next);
}
if(cur.t < cur.right) //当前取最值的位置 小于 当前和弦的 右边界
{
next.start = cur.start;
next.left = cur.t + 1; //新的左边界
next.right = cur.right;
next.t = RMQ_query(next.left, next.right);
next.val = sum[next.t] - sum[next.start - 1];
pq.push(next);
}
}
outn(ans);
return 0;
}