题目链接: 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;
}