Plahte

link

首先如果把矩形抽象成点,那么这些点一定会形成一片森林。理由:矩形之间只重叠不相交,故而对于一个矩形来说一定有至多一个完全覆盖它并且最小的大矩形,也就是说每个点的出度不超过一。又由于每个点连接的点肯定严格比自己大,所以也不会构成环。综上,最后这些点会形成一片森林。而由于连边的方式特征(也可以参照题目那句“那张床单会把球的颜色渗到下面的所有床单上”,可以把“下面所有床单”想象成点到根的路径),我们可以考虑对于每一发子弹找到最小的覆盖到它的矩形,然后统计答案的时候遍历整片森林用 set 启发式合并即可(可以构造出所有子弹打在叶子上的数据,这样如果单纯叶子往根合并会死掉)。

于是问题就变成了如何建树和如何找每发子弹对应结点的问题。

对于第一个我们可以用扫描线的思想,每个矩形拆分成上下两条线(称为上下线),把横坐标离散化之后再按纵坐标排序。找每个矩形在树上的父亲这一步骤放在下线进线段树的时候进行,由于扫描线的原理,假如一个矩形和当前矩形在纵方向上没有交集,那么它要么已经被丢出去了,要么还没有考虑进来,所以此时在线段树中的所有矩形都在纵坐标上包含当前矩形。但是呢我们希望找到的是横坐标也包含的、而且最小的矩形,对于第二个要求我们可以对于每个结点维护一个栈,由于按纵坐标排了序,可以想到越后入线段树的矩形越小,所以直接查询栈顶即可,一定可以保证是符合条件的最小矩形。对于第二个条件,先说结论,直接查询所有在线段树中访问到的结点中栈顶元素的最小值即可,然后插入一条线段的时候直接把它拆到一些区间(就是按着线段树的方式拆),给对应的结点的栈插入一个元素即可。

为什么上面的那种方法是对的呢?先考虑正确性,我们访问到的所有线段树上的结点都是当前线段拆开的结点的祖先,所以假如之前的某条线段覆盖了访问到的点,那么该线段一定可以覆盖这个结点的子孙,这就保证了我们找到的点是正确的。再考虑完备性,也就是是否存在一个未曾覆盖到访问结点的线段会成为答案,而这显然是不会的,毕竟能覆盖某条线段的线段拆开来的所有节点肯定不会比短线段拆开的结点要深,所以我们找到的点一定是最优的。综上,这样找父亲的方法是正确的。

然后就没有什么了。对于一个点,直接找它在线段树中访问到的结点中最大的编号,这个编号对应的矩形显然是最小且包含它的矩形,丢到一个 set 里就可以了(主要是因为有颜色相同的子弹),所以建树和寻找每个子弹对应的结点都是 \(O(N\log N)\)的。统计答案的时候就是树上启发式合并,复杂度 \(O(N\log^2N)\)。实际上手时空都不太紧张。有一些需要注意的:

  • 注意排序函数中纵坐标相同的时候上线段和下线段的相对位置,这关系到是否做到不重不漏的关键。
  • 由于不知名原因我写 stack 会崩,所以写的是指针版的栈,可能让代码繁琐了点。
  • 启发式合并要稍微注意一下写法。
  • 另外就是由于一对上线段和下线段对应的区间是相同的,所以拆开来的结点也是相同的,所以处理上线段时不需要判断直接弹出栈顶元素即可。
  • 代码有注释。码风氢气见谅……
#include<bits/stdc++.h>
//#define feyn
const int N=160010;
const int M=N*3;
using namespace std;
inline void read(int &wh){
    wh=0;int f=1;char w=getchar();
    while(w<'0'||w>'9'){if(w=='-')f=-1;w=getchar();}
    while(w<='9'&&w>='0'){wh=wh*10+w-'0';w=getchar();}
    wh*=f;return;
}
inline int max(int s1,int s2){
	return s1<s2?s2:s1;
}

struct st{int data;st* next;};
inline void insert(st* &top,int data){
	st* now=new st();
	now->data=data,now->next=top;top=now;
}
inline void del(st* &top){top=top->next;}

//线段树部分 
#define lc (wh<<1)
#define rc (wh<<1|1)
#define mid (t[wh].l+t[wh].r>>1)
struct node{
	int l,r;
	st* top;//每个结点的栈 
}t[M<<2];
inline void build(int wh,int l,int r){
	t[wh].l=l,t[wh].r=r;t[wh].top=NULL;
	if(l==r)return;
	build(lc,l,mid);
	build(rc,mid+1,r);
}
inline int change(int wh,int wl,int wr,int data){
	int an=0;
	if(t[wh].top!=NULL)an=t[wh].top->data;//一路寻找所有访问到的点的栈顶的最大值 
	if(wl<=t[wh].l&&t[wh].r<=wr){//找到了拆出来的结点 
		if(data)insert(t[wh].top,data);//如果是下线段要加入 
		else del(t[wh].top);//否咋弹出来 
		return an;
	}
	if(wl<=mid)an=max(an,change(lc,wl,wr,data));
	if(wr>mid)an=max(an,change(rc,wl,wr,data));
	return an;
}
inline int ask(int wh,int pl){
	int an=0;if(t[wh].top!=NULL)an=t[wh].top->data;
	if(t[wh].l==t[wh].r)return an;
	return max(an,ask(pl<=mid?lc:rc,pl));//如题,找一个点的答案 
}
#undef lc
#undef rc
#undef mid

int m,n,b[M];
struct sq{int l,r,h,data,id;}a[N<<1];//线段,l和r是线段的端点,h是高度,data是上下 
inline bool cmp_sq(sq s1,sq s2){return s1.h==s2.h?s1.data<s2.data:s1.h<s2.h;}
struct po{int x,y,co;}p[N];//子弹对应的点,co是颜色 
inline bool cmp_po(po s1,po s2){return s1.y<s2.y;}

vector<int>son[N<<1];set<int>data[N<<1];int ff[N<<1];
inline void add_sq(int wh){
	if(a[wh].data==1){
		int fa=change(1,a[wh].l,a[wh].r,wh);
		if(fa)son[fa].push_back(wh);ff[wh]=fa;//记录父亲儿子关系,应该有更简洁的写法 
	}
	else change(1,a[wh].l,a[wh].r,0);
}//加入一条线段 

int ans[N],set_[N<<1];
inline void solve(int wh){
	set_[wh]=wh;
	for(vector<int>::iterator it=son[wh].begin();it!=son[wh].end();it++){
		int th=*it;solve(th);
		if(data[set_[th]].size()>data[set_[wh]].size())swap(set_[wh],set_[th]);
		//保证小集合向大集合合并 
		for(set<int>::iterator ii=data[set_[th]].begin();ii!=data[set_[th]].end();ii++){
			data[set_[wh]].insert(*ii);
		}
	}
	ans[a[wh].id]=data[set_[wh]].size();
	return;
}

signed main(){
	
	#ifdef feyn
	freopen("in.txt","r",stdin);
	#endif
	
	read(m);read(n);
	int x1,x2,y1,y2,co;
	int cnt_a=0,cnt_b=0;
	for(int i=1;i<=m;i++){
		read(x1);read(y1);read(x2);read(y2);
		a[++cnt_a]=(sq){x1,x2,y1,1,i};
		a[++cnt_a]=(sq){x1,x2,y2+1,-1,i};
		b[++cnt_b]=x1;b[++cnt_b]=x2;
	}
	for(int i=1;i<=n;i++){
		read(x1);read(y1);read(co);
		b[++cnt_b]=x1;p[i]=(po){x1,y1,co};
	}
	sort(b+1,b+cnt_b+1);
	int num=unique(b+1,b+cnt_b+1)-b-1;
	build(1,1,num);//对横坐标离散化 
	for(int i=1;i<=cnt_a;i+=2){
		a[i].l=a[i+1].l=lower_bound(b+1,b+num+1,a[i].l)-b;
		a[i].r=a[i+1].r=lower_bound(b+1,b+num+1,a[i].r)-b;
	}
	for(int i=1;i<=n;i++)p[i].x=lower_bound(b+1,b+num+1,p[i].x)-b;
	//离散化 
	sort(a+1,a+cnt_a+1,cmp_sq);
	sort(p+1,p+n+1,cmp_po);
	//按纵坐标排序 
	for(int i=1,j=1;i<=n;i++){
		while(j<=cnt_a&&a[j].h<=p[i].y)add_sq(j++);
		int wh=ask(1,p[i].x);
		data[wh].insert(p[i].co);
	}
	for(int i=1;i<=cnt_a;i++){
		if(a[i].data<0||ff[i])continue;
		solve(i);//如果一个点是树根那么遍历这棵树并统计答案 
	}
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	
	return 0;
}
posted @ 2022-08-11 17:19  Feyn618  阅读(30)  评论(0编辑  收藏  举报