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;
}
posted @ 2025-10-29 19:06  qwqSW  阅读(8)  评论(0)    收藏  举报