CF1526C2 Potions (Hard Version) (贪心 + 线段树)

题目链接: Potions (Hard Version)

大致题意

有n个数, 编号从1~n, 第i个位置的值为a[i].

从编号为1的数字开始选择, 一直到编号为n的数字. 对于第i个数字, 你可以选或者不选. 若选择的话, 总和会加上a[i].

要求: 你需要选择尽可能多的数字, 并且保证选择每一个数字后, 总和不为负.

解题思路

贪心 + 线段树 (我看大家都是 带反悔的贪心做的, 代码太短了, 让我来个长的)

贪心方向: 我们对于所有正数, 直接取即可. 对于负数的情况, 我们也肯定是优先取大的

基于这个原则, 我们考虑对于负数的情况, 首先我们要从小到大去枚举所有位置的负数.
假设我们此时位于index处, 有一个权值为val的负数. (val当正数去看)

那么此时如果这个val能拿, 我们拿了一定是最优的, 因为我们从小到大枚举了. 那么我们怎么判断这个数字是否能拿呢? 很简单, 我们判断[1, index - 1]的区间和sum 是否 大于等于 val 即可. 若可以拿, 我们直接在[1, index - 1]区间减去val即可.

对于index这个位置, 往后的贡献我们是可以正确计算的, 因为在index以后的位置再求前缀和, 无论如何都会减去val. 考虑到对于index之前的位置: 我们发现其实我们希望尽可能在靠后的位置凑出val.

解释: 假设有序列 3 -3 2 1 -2 1, 那么此时树中的序列应为: 3 0 2 1 0 1
对于-2这个值, 我们希望是由 2 和 1凑出, 即修改后序列变为: 3 0 1 0 0 1

这样我们再枚举到-3这个位置时, 我们可以正确计算出答案.

否则, 如果靠左的去凑数, 即: -2这个值, 我用3来满足他, 树中序列变为: 1 0 2 1 0 1.
我们发现当再枚举到-3时, 此时无法计算出正确答案

我们抱着这个思路, 来分析一下线段树能否满足我们的要求.

首先我们需要求[1, index - 1]的前缀和, 这个OK.

对于之后的修改操作, 我们先要找到尽可能靠右, 能凑出val的位置(假设为pos), 这个也OK.
然后我们需要在这个位置减去相应的贡献, 这个也OK.
再然后呢? 我们其实还要做一件事情, 就是把[pos + 1, index - 1]区间赋为权0. 当然这个还是OK的.

额外提一句, 对于找pos位置的方法:

比较好想的是在树外可以做一个二分, 但这样的方式是O(nlognlogn)

我们其实也可以考虑在树上查询, 就是代码要复杂一些了. 这样可以省去一个logn, 具体见代码

到此为止, 我们快乐的发现, 我们可以写线段树了.

复杂度: O(nlogn)

AC代码

#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
#define debug(a) cout << #a << " = " << a << endl;
using namespace std;
typedef long long ll;
const int N = 2E5 + 10, INF = 0x3f3f3f3f;
struct node {
	int l, r;
	ll val;
	bool flag; //清空标记
}t[N << 2];
void pushdown(node& op, bool) { op.val = 0, op.flag = 1; }
void pushdown(int x) {
	if (!t[x].flag) return;
	pushdown(t[x << 1], 1), pushdown(t[x << 1 | 1], 1);
	t[x].flag = 0;
}
void pushup(int x) { t[x].val = t[x << 1].val + t[x << 1 | 1].val; }
void build(int l, int r, int x = 1) {
	t[x] = { l, r, 0, 0 };
	if (l == r) return;
	int mid = l + r >> 1;
	build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
}
void modify(int l, int r, int c, int x = 1) { // c若为INF, 则表示置0操作
	if (l <= t[x].l and r >= t[x].r) {
		if (c != INF) t[x].val += c;
		else pushdown(t[x], 1);
		return;
	}
	pushdown(x);
	int mid = t[x].l + t[x].r >> 1;
	if (l <= mid) modify(l, r, c, x << 1);
	if (r > mid) modify(l, r, c, x << 1 | 1);
	pushup(x);
}
ll ask(int l, int r, int x = 1) { 
	if (l <= t[x].l and r >= t[x].r) return t[x].val;
	pushdown(x);
	int mid = t[x].l + t[x].r >> 1;
	ll res = 0;
	if (l <= mid) res += ask(l, r, x << 1);
	if (r > mid) res += ask(l, r, x << 1 | 1);
	return res;
}

int VAL; //我们要凑的值
void fact(int l, int r, int x = 1) {
	if (!VAL) return;
	if (t[x].l == t[x].r) { //表示[index, x]区间的和满足要求, 推平[index + 1, x];
		int index = t[x].l;
		int can = min(1ll * VAL, t[x].val);
		t[x].val -= can, VAL -= can;
		if (!VAL) modify(index + 1, r, INF);
		return;
	}

	pushdown(x);
	if (l <= t[x].l and r >= t[x].r) { //区间满足要求, 进行树上二分
		ll right = t[x << 1 | 1].val;
		if (right >= VAL) fact(l, r, x << 1 | 1); 
		else VAL -= right, fact(l, r, x << 1);
		pushup(x);
		return;
	}

	int mid = t[x].l + t[x].r >> 1;
	if (r > mid) fact(l, r, x << 1 | 1); //要先去右区间寻找.
	if (l <= mid) fact(l, r, x << 1);
	pushup(x);
}
int main()
{
	int n; cin >> n;
	build(1, n);

	vector<pair<int, int>> v; //存负数的情况
	int res = 0;
	rep(i, n) {
		int x; scanf("%d", &x);
		if (x >= 0) res++, modify(i, i, x); //正数直接拿
		else v.push_back({ -x, i });
	}
	sort(v.begin(), v.end());

	for (auto& [val, id] : v) {
		ll now = ask(1, id); //[1, id] 总贡献有now
		if (now >= val) {
			res++; //表明这个药水一定可以喝

			VAL = val;
			fact(1, id);
		}
	}

	cout << res << endl;
	return 0;
}

END

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥Fau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值