P13077 [NOISG 2019] Feast 题解

P13077 [NOISG 2019] Feast 题解

主要思路参考了这篇题解

简化题意

有一个长度为 \(n\) 的数组 \(a\),满足 \(|a_i| \in [1, 10 ^ 9]\)。我们在其中取 \(k\)不重叠的区间,最大化 \(a_i\) 值的总和。

解题思路

下文中:“非负数区间”表示仅包含非负数的区间;“负数区间”表示仅包含负数的区间。

我们先从最简单的样例来看起:

Sample 1

\[n = 5, k = 3, a \in \{1, 2, 4, 3, 1\} \]

不难发现,题意并没有限制怎么分配区间。由于对于所有 \(i\)\(a_i \ge 0\),因此我们可以随便安排区间(因为题目中有条件 \(k \leq n\)),取完就行, 答案就是 \(\sum_{i = 1}^{n} a_i\)

Sample 2

\[n = 5, k = 2, a \in \{-1, 2, -1, 3, 1\} \]

这里有两个 \(-1\)。很明显的一点是,没有收益的情况下,我们肯定不会主动去拿 \(-1\)。仅包含非负数的区间有两个:\(\{2\}\)\(\{3, 1\}\)。由于 \(k = 2\),我们把这两个区间选掉就行了。

Sample 3

\[n = 7, k = 2, a \in \{-1, 3, -1, 3, 1, -1, 5\} \]

这里我们发现有了三个非负数区间:\(\{3\}\)\(\{3, 1\}\)\(\{5\}\)。那可能有人就说了:“把总和最大的两个区间选了不就行了吗?”这个想法比较正常,但是在此处是不对的。因为我们发现:如果把第二个、第三个区间之间的一个 \(-1\) 拿走,相当于把二、三两个区间合并,那么就可以额外再选第一个区间。由于第一个区间的总和是 \(3\),因此我们以 \(-1\) 的代价获得的 \(3\) 的收益。答案就更大了。

这里,我们选的区间变成了 \(\{ 3\}\)\(\{3, 1, -1, 5\}\)


那么上面的那些样例给了我们什么启示呢?有两点:

  • 最佳方案就是可以选走所有的非负数区间(即非负数区间的数量不大于 \(k\))。因为 \(k \leq n\),不用管分配方式。
  • 如果我们无法选走所有非负数所组成的区间,可以考虑以下两种操作:
    • 不选负数。这样,我们会损失一些非负数区间,但没有额外的代价
    • 选负数,连接两个区间。这样,我们能获得选择额外一个非负数区间的机会,但必须付出一些代价

这样,我们就知道了代码的主要内容与思路:

  1. 要找出所有的非负数与负数区间。
  2. 要能够判断扔哪些非负数区间,或者拿那些负数区间(从而连接两个正数区间)。

于是,我们的思路就是反悔贪心

要保证扔掉的非负数区间或者选择的负数区间对答案的影响最小,因此利用优先队列来维护这个序列。首先,我们选走所有的非负数区间。然后,要是非负数区间的数量大于 \(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;
}

posted on 2026-09-06 15:49  Leo_29  阅读(4)  评论(0)    收藏  举报

导航