闲聊:多测一定要清空!!!
以及,听说本题有九倍经验。
题目传送门
我的博客-欢迎光临
本题的做法很多,最主要的一个是差分约束。这里我们介绍另一种做法——并查集+树状数组。(虽然这个做法有点……难评,但我认为它最大的优势是快,我现在是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;
}