P11453 [USACO24DEC] Deforestation S
闲聊:多测一定要清空!!!
以及,听说本题有九倍经验,主要针对差分约束。
本题的做法很多,最主要的一个是差分约束。这里我们介绍另一种做法——并查集+树状数组。(虽然这个做法有点……难评,但我认为它最大的优势是快,我现在是896ms,第二面榜)
首先本题值域太大了,我们要先对位置进行离散化(查询的区间和树的位置都离散化)。接着我们可以对原来的 \(n\) 棵树的位置排序(虽然这没必要)。最后我们把原问题转化为最少保留多少棵树。
然后我们就得到了一坨形似 CSP-S 2024 T2 的东西。我们按右端点从小到大排序,然后分别处理每个区间。
这里就体现本题的贪心标签了:对于一个区间而言,如果当前它的限制还未被满足,尽量往右边选择保留的树一定是不劣的,因为它的上一个区间已经选树完成了不用管,但是下一个区间没考虑完,越往右选树,越有可能使这棵树贡献到下面的区间。
或者说,如果你该区间的选树方案里存在一个靠右的树未被选择,显然你把它选上,并去掉最靠左边的树,这样既能满足该区间的约束,下面的区间要选的树也有可能减少,何乐而不为呢?
这样我们的大体思路就有了:离散化+区间排序+枚举区间求贡献。
但是如果从右往左直接扫的话,经过构造有可能会T飞(本人没试过,如有不对还请斧正)。这时我就想起来了一个套路:用并查集维护最靠右的未被选树的位置。具体来说,我们用 \(fa_i\) 表示 \(i\) 左侧最靠近 \(i\) 的没被选树的位置。初始时 \(fa_i=i\)。
这个套路还蛮常见的,但总之当 \(i\) 位置选上树后,我们令 \(fa_i=i-1\),查找某个点前一个未被选树的位置时,直接跑正常的路径压缩即可。
至于树状数组,这个主要是用来判断该区间是否已经满足限制用的。当我们加入一个位置的全部树时,就让当前这个离散化位置在树状数组上加上该位置树的个数,进入下一个区间时直接正常区间查询即可。
由于我们最多会种 \(n\) 棵树,最多有 \(m\) 个约束区间,树状数组的修改和查找是 \(O(\log n)\) 的,所以总时间复杂度 \(O(Tn \log n)\)。
其他问题请看代码。
代码:
P11453
#include<cstdio>
#include<algorithm>
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
while(c<48){
if(c=='-') f=-1;
c=getchar();
}
while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
const int N=1e5+5;
int T,n,m,pos[N],b[N],fa[N],tr[N],num[N];
struct Nahida{
int l,r,num;
}q[N];
inline void INIT(){
for(int i=1;i<=n;i++){
fa[i]=i,tr[i]=0,num[i]=0;
}
}
inline int lowbit(int x){
return x&(-x);
}
inline bool cmp(Nahida x,Nahida y){
return (x.r!=y.r?x.r<y.r:x.l<y.l);
}
inline void add(int x,int k){
while(x<=n){
tr[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x){
int ans=0;
while(x){
ans+=tr[x];
x-=lowbit(x);
}
return ans;
}
inline int FIND(int x){
return (x==fa[x]?x:fa[x]=FIND(fa[x]));
}
signed main(){
T=read();
while(T--){
n=read(),m=read();
INIT();//多测一定要检查你该清空的数组是否全部清空
//多测记得清空!!!
for(int i=1;i<=n;i++){
pos[i]=read();
b[i]=pos[i];
}
for(int i=1;i<=m;i++){
q[i].l=read(),q[i].r=read(),q[i].num=read();
}
//离散化
sort(b+1,b+n+1);sort(pos+1,pos+n+1);
int len=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
pos[i]=lower_bound(b+1,b+len+1,pos[i])-b;
num[pos[i]]++;
//刚才题解里没讲到一个问题,就是一些树的位置可能会重复,所以我们开个桶数组,记录某个位置的树有多少棵
}
for(int i=1;i<=m;i++){
q[i].l=lower_bound(b+1,b+len+1,q[i].l)-b;
q[i].r=upper_bound(b+1,b+len+1,q[i].r)-b-1;
/*
我离散化的时候没有选择把查询端点也一起扔进去离散化,而是在找第一个大于等于左端点的树的位置
和最后一个小于右端点的树的位置,这样我们相当于是把两侧没有树的无用区间忽略掉了
在此同时进行了一个离散化
*/
}
sort(q+1,q+m+1,cmp);//根据右端点从小到大排序
int ans=0;
for(int i=1;i<=m;i++){
int l=q[i].l,r=q[i].r;
int dq=query(r)-query(l-1);
//dq:当前区间已经有了多少棵树
if(dq>=q[i].num){//当前区间已经种够了,就不用种了
continue;
}
ans+=q[i].num-dq;
//否则就要种这么多棵树
while(dq<q[i].num){
//nxt:更具体地说,指的是还没有选树的最靠右的某个位置,这个位置上可以有多棵树
int nxt=FIND(r);
add(nxt,num[nxt]);//注意这里可能有多棵树
fa[nxt]=nxt-1;
dq+=num[nxt];//用刚种的树的数量更新dq
}
}
//ans记录的是最少留多少树,转换成最多砍多少棵树
printf("%d\n",n-ans);
}
return 0;
}

浙公网安备 33010602011771号