CF2271E Array Emptying详细题解

思路

按数值从大到小贪心,如果能删就将该元素删去,如果不能删就将它分裂。

注意到元素的范围(\(1\le a_i \le n\)),开一个桶记录每个数字出现的次数,省去了排序的时间开销。

设要进行 \(x\) 次分裂操作,那么进行完 \(x\) 次分裂操作后元素数量为 \(n+x\),那么总的操作次数即为 \(n+x+x\)。

不难发现分裂次数 \(x\) 具有单调性,我们考虑二分答案。

cpp 复制代码
inline bool check(int mid){
    int kk=k+2*mid+n-1;
    for(int i=1;i<=n;i++) cntt[i]=cnt[i];
    int shu=n,tot=0;
    while(shu){
        if (cntt[shu]==0){
            shu--;
            continue;
        }
        if (shu>kk){               
            if (tot+1>mid) return false;
            cntt[shu]--;
            tot++;
            cntt[(shu+1)/2]+=2;  
        }else{
            cntt[shu]--;
            kk--;                       
        }
    }
    return true;
}

复杂度:

每组数据 \(O(n\log n)\),空间复杂度 \(O(n)\)。

常见问题及解答

  • \(Q\): \(kk\) 初始值为什么是 \(k+2\times mid+n-1\) ?

  • \(A\): 由于在进行每次操作后 \(k\) 值都会加一,那么最终值为 \(k+2\times mid+n\),最后一次操作加的一不能用,故减一

  • \(Q\): 为什么分裂时 \(kk\) 不减?

  • \(A\): 分裂不删除元素,只是把 \(1\) 个元素拆成 \(2\) 个,不消耗"删除名额",故 \(kk\) 不变。分裂占用的时间步已含在 \(kk\) 初值的 \(2\times mid\) 中 (\(n+mid\) 次删除,\(mid\) 次分裂),循环中再减就重复了。

完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int t,cnt[N],n,k,cntt[N];
inline bool check(int mid){
    int kk=k+2*mid+n-1;
    for(int i=1;i<=n;i++) cntt[i]=cnt[i];
    int shu=n,tot=0;
    while(shu){
        if (cntt[shu]==0){
            shu--;
            continue;
        }
        if (shu>kk){               
            if (tot+1>mid) return false;
            cntt[shu]--;
            tot++;
            cntt[(shu+1)/2]+=2;  
        }else{
            cntt[shu]--;
            kk--;                       
        }
    }
    return true;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	while(t--){
		cin>>n>>k;
		for(int i=1;i<=n;i++){
			cnt[i]=0;
		}
		for(int i=1;i<=n;i++){
			int x;
			cin>>x;
			cnt[x]++;
		}
		int l=0,r=n*3;
		while(l<r){
			int mid=(l+r)>>1;
			if (check(mid)){
				r=mid;
			}else{
				l=mid+1;
			}
		}
		cout<<n+2*r<<'\n';
	}
	return 0;
}

祝2026NOIP RP++