P13077 [NOISG 2019] Feast 题解
P13077 [NOISG 2019] Feast 题解
主要思路参考了这篇题解。
简化题意
有一个长度为 \(n\) 的数组 \(a\),满足 \(|a_i| \in [1, 10 ^ 9]\)。我们在其中取 \(k\) 个不重叠的区间,最大化 \(a_i\) 值的总和。
解题思路
下文中:“非负数区间”表示仅包含非负数的区间;“负数区间”表示仅包含负数的区间。
我们先从最简单的样例来看起:
Sample 1
不难发现,题意并没有限制怎么分配区间。由于对于所有 \(i\), \(a_i \ge 0\),因此我们可以随便安排区间(因为题目中有条件 \(k \leq n\)),取完就行, 答案就是 \(\sum_{i = 1}^{n} a_i\)。
Sample 2
这里有两个 \(-1\)。很明显的一点是,没有收益的情况下,我们肯定不会主动去拿 \(-1\)。仅包含非负数的区间有两个:\(\{2\}\)、\(\{3, 1\}\)。由于 \(k = 2\),我们把这两个区间选掉就行了。
Sample 3
这里我们发现有了三个非负数区间:\(\{3\}\)、\(\{3, 1\}\)、\(\{5\}\)。那可能有人就说了:“把总和最大的两个区间选了不就行了吗?”这个想法比较正常,但是在此处是不对的。因为我们发现:如果把第二个、第三个区间之间的一个 \(-1\) 拿走,相当于把二、三两个区间合并,那么就可以额外再选第一个区间。由于第一个区间的总和是 \(3\),因此我们以 \(-1\) 的代价获得的 \(3\) 的收益。答案就更大了。
这里,我们选的区间变成了 \(\{ 3\}\)、\(\{3, 1, -1, 5\}\)。
那么上面的那些样例给了我们什么启示呢?有两点:
- 最佳方案就是可以选走所有的非负数区间(即非负数区间的数量不大于 \(k\))。因为 \(k \leq n\),不用管分配方式。
- 如果我们无法选走所有非负数所组成的区间,可以考虑以下两种操作:
- 不选负数。这样,我们会损失一些非负数区间,但没有额外的代价。
- 选负数,连接两个区间。这样,我们能获得选择额外一个非负数区间的机会,但必须付出一些代价。
这样,我们就知道了代码的主要内容与思路:
- 要找出所有的非负数与负数区间。
- 要能够判断扔哪些非负数区间,或者拿那些负数区间(从而连接两个正数区间)。
于是,我们的思路就是反悔贪心。
要保证扔掉的非负数区间或者选择的负数区间对答案的影响最小,因此利用优先队列来维护这个序列。首先,我们选走所有的非负数区间。然后,要是非负数区间的数量大于 \(k\),则不断扔掉非负数区间,或者选择一个负数区间,把它和相邻区间合并然后再入队。这样,如果后面我们发现这个区间还能再次被操作,就再把它整个删除并再次合并,以此类推(这样相当于中间那块选过一次的又被选了一次,抵消掉了)。直到我们选择的区间数量不大于 \(k\)。然后就能得到答案。
不过还有一个地方要注意,就是我们选择两边上的负数区间是没有意义的(无法起到连接的意义)。因此,我们需要写一个类似于链表的系统,实时更新一个区间两边区间的序号。
因此,代码也就不难实现了吧。
代码部分
#include <bits/stdc++.h>
using namespace std;
int n, k;
long long a[300010];
long long s = 0, sum = 0, ans = 0;
struct node
{
long long val;//数值
int l, r;//两边区间序号
bool flag;//是否已经被删除
}t[300010];
priority_queue<pair<long long, long long> > q;//优先队列
void del(int x)//删除操作
{
t[x].flag = 1;//标记已删除
//更改两边两个区间的相邻区间下标
t[t[x].l].r = t[x].r;
t[t[x].r].l = t[x].l;
}
int main()
{
cin >> n >> k;
for(int i = 1;i <= n;i ++)
{
cin >> a[i];
if(!a[i])
continue;
if((a[i] < 0 && t[s].val >= 0) || (a[i] > 0 && t[s].val <= 0))//重新开一个区间(区间类型变化)
t[++ s].val = a[i];
else
t[s].val += a[i];
}
for(int i = 1;i <= s;i ++)
{
t[i].l = i - 1, t[i].r = i + 1;//标记相邻区间下标
if(t[i].val > 0)//非负数区间,数量增加
ans += t[i].val, sum ++;
q.push({-abs(t[i].val), i});//放入优先队列
}
while(sum > k)//不断删除或选择区间,直到 sum = k
{
while(t[q.top().second].flag)//已经被删除
q.pop();
long long i = q.top().second;
q.pop();
if((!t[i].l || t[i].r > s) && t[i].val < 0)//负数区间在两边,删除无意义
continue;
sum --;
ans -= abs(t[i].val);//代价为区间的绝对值
t[i].val = t[t[i].l].val + t[t[i].r].val + t[i].val;//合并两个区间
//更改相邻区间下标(把这个区间删了,相邻两个区间就不用删了)
if(t[i].l >= 1)
del(t[i].l);
if(t[i].r <= s)
del(t[i].r);
q.push({-abs(t[i].val), i});//入队
}
cout << ans << endl;
return 0;
}
浙公网安备 33010602011771号