题目描述
难度分:1500
输入T(≤104)表示T组数据。所有数据的n之和≤2×105。
每组数据输入n(1≤n≤2×105)和长为n的数组a(1≤a[i]≤109)。
一开始cnt=0。有两种操作,每种都可以执行任意次。
- 第一种:把一个大于0的a[i]减少1,然后cnt自增。
- 第二种:把一个大于等于cnt的a[i]减少cnt,然后cnt=0。
把所有a[i]都变成0,最少要操作多少次?
输入样例
4
4
1 3 1 1
4
1 2 1 1
6
3 2 1 5 2 4
2
1 6
输出样例
4
4
11
5
算法
贪心
一开始读了个假题,以为操作2能把所有≥cnt的数都减少cnt,卡好久过不了样例但是又不知道错哪。一定要仔细读题!
其实没想太明白这样贪为什么一定对,但是直觉上觉得应该在一个有序结构上操作更方便,每次把小的数先利用操作1减1,减到满足“cnt=最大值”了就执行一次操作2把这个最大值干掉。
按照这种思路,我们可以先把所有元素都放入到一个平衡树中,初始化cnt=0。每次检查cnt+最小值与最大值的关系。分为以下三种情况:
- cnt+minimum<maximum,直接让cnt增加minimum,操作数也增加minimum,将minimum从平衡树中删除。
- cnt+minimum=maximum,让cnt=0,操作数先增加minimum(操作1),再增加1(操作2干掉maximum),将minimum和maximum都从平衡树中删除。
- cnt+minimum>maximum,让cnt=0,操作数先增加maximum−cnt(操作1),再增加1(操作2干掉maximum),将minimum和maximum都从平衡树中删除。但此时最小值并没有完全被减成0,而是minimum−(maximum−cnt),因此还要将剩下的部分放入到平衡树中。
当平衡树中只有一个数的时候退出这个过程,根据剩余的数last与cnt的关系来决定最后一步。分为以下两种情况:
- cnt>last,此时只能用操作1,让操作数增加last。
- 否则让cnt增加x,这个x是满足cnt+x≤last−x的最大x,这样就能保证在使用操作2时能够尽量让last接近0。先执行x次操作1,然后执行一次操作2使last变为last−x−(cnt+x)。最后last还剩多少就要执行多少次操作1。
复杂度分析
时间复杂度
在贪心的过程中每个数都要从平衡树中被删除,最多两次就能把一个数删掉(多数情况下一次就删掉了),因此一共删了O(n)次。而平衡树的插入和删除操作时间复杂度为O(log2n),所以整个算法的时间复杂度为O(nlog2n)。
空间复杂度
额外空间就是平衡树所消耗的空间,为O(n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010;
int t, n, a[N];
int main() {
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
multiset<int> st;
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
st.insert(a[i]);
}
LL cnt = 0, ans = 0;
while(st.size() > 1) {
int cur = *st.begin(), mx = *st.rbegin();
if(cnt + cur < mx) {
cnt += cur, ans += cur;
st.erase(st.find(cur));
}else {
st.erase(st.find(cur));
st.erase(st.find(mx));
ans++;
if(cnt + cur > mx) {
ans += mx - cnt;
st.insert(cnt + cur - mx);
}else {
ans += cur;
}
cnt = 0;
}
}
if(st.size()) {
int last = *st.begin();
if(cnt <= last) {
int x = last - cnt >> 1;
last -= x, cnt += x, ans += x;
if(cnt > 0) {
last -= cnt;
ans++;
}
ans += last;
}else {
ans += last;
}
}
printf("%lld\n", ans);
}
return 0;
}