P5568 题解

题意简述

对一个空集 \(S\) 进行 \(M(M\le 7\times10^4)\) 次操作,每次给出一个集合 \(T\)(以自然数区间形式给出),对 \(S\) 进行以下五种操作之一:

  1. \(S=S\cup T\)
  2. \(S=S\cap T\)
  3. \(S=S-T\)
  4. \(S=T-S\)
  5. \(S=(S-T)\cup (T-S)\)

求最终的 \(S\)

题目分析

很直观的线段树题,分建树、修改、查询分别展开说明。

建树

由于涉及到的区间都是自然数区间,因此需要维护的区间个数有限,用线段树的结点维护就可以了。

为了方便维护,我们可以将所有自然数自身组成的闭区间 \([k,k]\)(这种区间按理说是不合法的,但是本题都这么写了就将就用吧) 编号为 \(2k\),将所有两个相邻自然数之间的开区间 \((k,k+1)\) 编号为 \(2k+1\)。我们姑且称这两种区间为“单位区间”,则所有的自然数区间都可以表示为“单位区间”的并。例如 \((a,b]\) 可以表示为编号 \(2a+1\)\(2b\) 的“单位区间”之并,\([b,a)\) 可以表示为编号 \(2a\)\(2b-1\) 的“单位区间”之并。

我们考虑线段树结点维护的信息。最基本地,每个结点维护其对应区间的两端“单位区间”的编号 \(l\)\(r\)。而考虑到题目中的操作实质上就是改变了一些“单位区间”是否在答案区间里的状态,因此再让结点维护其对应区间中在 \(S\) 里的“单位区间”的个数 \(cnt\)。最后就是懒标记 \(tag\),我们设计其可能值为 \(-1,0,1,2\),分别代表无标记、将区间内全部“单位区间”都从 \(S\) 中删除、将区间内全部“单位区间”都加入 \(S\)、将全部“单位区间”是否在 \(S\) 里的状态取反(可能有点奇怪,可先看修改部分再理解)。标记累加的具体细节详见代码。

修改

分析一下本题的五种操作:

对于操作 \(1\),把 \(T\) 中的所有“单位区间”加入 \(S\) 里,即 \(T\) 对应的的结点 \(cnt\) 赋值为 \(r-l+1\)

对于操作 \(2\),把所有不在 \(T\) 中的“单位区间”从 S 中去除,即除 \(T\) 之外的区间对应的结点 \(cnt\) 赋值为 \(0\)

对于操作 \(3\),把 \(T\) 中的所有“单位区间”从 \(S\) 中去除,即 \(T\) 对应的结点 \(cnt\) 赋值为 \(0\)

对于操作 \(4\),把不属于 \(T\) 中的所有“单位区间”从 \(S\) 中去除,并把属于 \(T\) 中的所有“单位区间”状态取反即可。即除 \(T\) 之外的区间对应的结点 \(cnt\) 赋值为 \(0\)\(T\) 对应的结点 \(cnt\) 赋值为 \(r-l+1-cnt\)

对于操作 \(5\),把属于 \(T\) 中的所有“单位区间”状态取反,即 \(T\) 对应的结点 \(cnt\) 赋值为 \(r-l+1-cnt\)

查询

题目要求输出在 \(S\) 中的区间,并不一定全都是结点对应的。但是注意到这些区间对应的所有结点一定满足 \(cnt=r-l+1\),所以我们考虑递归找到所有满足该条件的结点,再将其对应的区间合并即可。

我们从根结点开始查询。当前结点满足 \(cnt=r-l+1\),就把该结点对应的区间直接加到答案里,不再递归。否则,若左子结点的 \(cnt≠0\),递归到左节点;若右子结点的 \(cnt≠0\),则再递归到右节点。当然,如果最开始根结点 \(cnt=0\),就输出 empty set

答案区间的合并是简单的,不再赘述。至于输出,根据区间的端点奇偶性判断区间开闭并计算原编号即可。

代码实现

#include<bits/stdc++.h>
using namespace std;
struct node
{
	int l,r,cnt,tag;	
}tr[524300];
char op[2],str[50];
int l[70010],r[70010],i,q,OP[70010],mx=INT_MIN,mn=INT_MAX,ans1[140010],ans2[140010],cnt,L;
bool pt(int l,int r)
{
	printf("%c%d,%d%c ",(l&1?'(':'['),l>>1,r+1>>1,(r&1?')':']'));
}
void pushup(int p)
{
	tr[p].cnt=tr[p<<1].cnt+tr[p<<1|1].cnt;
}
void addtag(int p,int val)
{
	if(!val)
	{
		tr[p].tag=0;
		tr[p].cnt=0;
	}//如果是全改成 0,直接设置即可。 
	else if(val==1)
	{
		tr[p].tag=1;
		tr[p].cnt=tr[p].r-tr[p].l+1;
	}//全改成 1 也类似。 
	else//取反情况较复杂。 
	{
		tr[p].cnt=tr[p].r-tr[p].l+1-tr[p].cnt;
		if(tr[p].tag==2)//如果原来就要取反,再取反就不变了。 
			tr[p].tag=-1;
		else if(tr[p].tag>-1)//如果原来是要全改成什么,取反后就是改成相反的数了 。 
			tr[p].tag=(!tr[p].tag);
		else
			tr[p].tag=2;//如果原来不改,就记上标记。 
	}
}//给结点 p 进行操作 val 
void pushdown(int p)
{
	if(tr[p].tag!=-1)
	{
		addtag(p<<1,tr[p].tag);//传给左子结点 
		addtag(p<<1|1,tr[p].tag);//传给右子结点 
		tr[p].tag=-1;//清空标记 
	}
}
void build(int p,int l,int r)
{
	tr[p].l=l,tr[p].r=r;
	tr[p].tag=-1;//初始没有标记。 
	if(l==r)
	{
		tr[p].cnt=0;//初始 S 为空 
		return;
	}
	int mid=l+r>>1;
	build(p<<1,l,mid);//建左子节点
	build(p<<1|1,mid+1,r);//建右子节点 
	pushup(p);
}
void change(int p,int l,int r,int val)
{
	if(tr[p].l>=l&&tr[p].r<=r)
	{
		addtag(p,val);//当前结点全在修改区间里,直接改。 
		return;
	}
	pushdown(p);//先传标记
	int mid=tr[p].l+tr[p].r>>1;
	if(l<=mid)//左子结点要改 
		change(p<<1,l,r,val);
	if(r>mid)//右子结点要改
		change(p<<1|1,l,r,val);
	pushup(p);
}
void query(int p)
{
	pushdown(p);//先传标记。 
	if(tr[p].cnt==tr[p].r-tr[p].l+1)
	{
		ans1[++cnt]=tr[p].l;
		ans2[cnt]=tr[p].r;//把结点的左右编号放到答案里 
		return;
	}
	if(tr[p<<1].cnt)
		query(p<<1);//左子结点有在 S 里的就查询
	if(tr[p<<1|1].cnt)
		query(p<<1|1);//右子结点有在 S 里的就查询
}
int main()
{
	while(scanf("%s %s",op+1,str+1)==2)	
	{
		q++;
		l[q]=r[q]=0;
		for(i=2;str[i]<='9'&&str[i]>='0';i++)
			l[q]=l[q]*10+str[i]-'0';
		for(i++;i<strlen(str+1);i++)
			r[q]=r[q]*10+str[i]-'0';
		if(str[1]=='[')
			l[q]=2*l[q];
		else
			l[q]=2*l[q]+1;
		if(str[strlen(str+1)]==']')
			r[q]=2*r[q];
		else
			r[q]=2*r[q]-1;
		if(op[1]=='U')
			OP[q]=1;
		else if(op[1]=='I')
			OP[q]=2;
		else if(op[1]=='D')
			OP[q]=3;
		else if(op[1]=='C')
			OP[q]=4;
		else
			OP[q]=5;
		mx=max(mx,r[q]);
		mn=min(mn,l[q]); 	//省空间,只在修改涉及到的最小值到最大值之间建树即可
	}
	mn--;//注意修改时会涉及到 mn-1 和 mx+1 
	mx++;
	build(1,mn,mx); 
	for(int i=1;i<=q;i++)
	{
		if(OP[i]==1)
			change(1,l[i],r[i],1);
		else if(OP[i]==2)
			change(1,mn,l[i]-1,0),change(1,r[i]+1,mx,0);
		else if(OP[i]==3)
			change(1,l[i],r[i],0);
		else if(OP[i]==4)
			change(1,mn,l[i]-1,0),change(1,l[i],r[i],2),change(1,r[i]+1,mx,0);
		else
			change(1,l[i],r[i],2);
	}
	if(!tr[1].cnt)//S 为空 
	{
		printf("empty set");
		return 0;
	}
	query(1);//查询 
	L=ans1[1];
	for(int i=2;i<=cnt;i++)
		if(ans1[i]-ans2[i-1]!=1)
		{
			pt(L,ans2[i-1]);
			L=ans1[i];
		}
	pt(L,ans2[cnt]);//输出 
	return 0;
}
posted @ 2023-07-09 17:39  Hadtsti  阅读(93)  评论(0编辑  收藏  举报