P5568 [SDOI2008]校门外的区间 题解

· · 题解

原题link

题意

对于一个集合 S ,它可以和另外一个集合 T 做以下五种运算:

U TS = S∪T

I TS = S∩T

D TS = S-T

C TS = T-S

S TS = S⊕T

集合的基本运算操作定义如下:

$A∩B$:得到在 $A$ 集合中,且在 $B$ 集合中的元素。 $A−B$:得到在 $A$ 集合中,但不在 $B$ 集合中的元素。 $A⊕B$:$(A-B)∪(B-A)

初始区间 S 为空,输入 M 条计算指令,请你输出计算后的 S (用多个区间表示)。如果集合 S 最终为空,输出 empty set 。

思路

用一个线段树维护每个整数值是否存在,需要记录区间被什么覆盖(记为 tag , -1 表示无覆盖, 0/1 表示被覆盖)和区间是否反转(记为 rev , 1 表示区间被反转) 。 五种操作分别对应如下:

$I$ : $T$ 以外的区间覆盖为 $0$ 。 $D$ : $T$ 对应的区间覆盖为 $0$ 。 $C$ : $T$ 对应的区间反转, $T$ 以外的区间覆盖为 $0$ 。 $S$ : $T$ 对应的区间反转。 pushdown 的时候注意优先级,覆盖的优先级始终高于反转。 对于开区间,可以如下处理: 对于闭区间 $[a,b]$ 替换成 $[2a,2b]$ ,对于开区间 $(a,b)$ 替换成 $[2a+1,2b-1]$ ,如此可以按照纯闭区间处理。 答案输出:先查询每个叶子区间是否取 $1$ ,并记录成一个一维数组,最后扫描一遍整个数组输出答案。 # 代码讲解 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 2*65535+10; const int M = 2*65535; int m,tag[N<<2],ans[N],rev[N<<2]; ``` $tag[u]=-1$ 未被覆盖 $tag[u]=0$ 被 $0$ 覆盖 $tag[u]=1$ 被 $1$ 覆盖 $rev[u]=0$ 未取反 $rev[u]=1$ 被取反 ```cpp inline int lc(int u) { return u<<1; } inline int rc(int u) { return u<<1|1; } ``` 线段树标配,分别表示左右儿子,使用位运算更快。 ```cpp inline void pushdown(int u) { if(tag[u] != -1) { tag[lc(u)] = tag[rc(u)] = tag[u]; rev[lc(u)] = rev[rc(u)] = 0; tag[u] = -1; } if(rev[u]) { if(tag[lc(u)] == -1) { rev[lc(u)] = 1 - rev[lc(u)]; } if(tag[lc(u)] != -1) { tag[lc(u)] = 1 - tag[lc(u)]; } if(tag[rc(u)] == -1) { rev[rc(u)] = 1 - rev[rc(u)]; } if(tag[rc(u)] != -1) { tag[rc(u)] = 1 - tag[rc(u)]; } rev[u] = 0; } } ``` 当节点 $u$ 被覆盖的时候,因为覆盖的优先级大于取反,所以左右儿子继承父亲的 $tag$ ,并设置成没有取反,且把 $tag_u$ 改回 $-1$ 。如果 $u$ 被取反了,则:如果一个儿子未被覆盖,则把其取反情况翻转一下;如果被覆盖了,就把覆盖的值翻转一下。最后把父亲调回未翻转状态。 ```cpp void modify_cover(int u,int l,int r,int x,int y,int k) { if(x <= l && y >= r) { tag[u] = k; rev[u] = 0; } else { int mid = (l+r)>>1; pushdown(u); if(x <= mid) modify_cover(lc(u),l,mid,x,y,k); if(y > mid) modify_cover(rc(u),mid+1,r,x,y,k); } } ``` 覆盖操作时, ```else``` 部分不讲,讲一下 ```if``` 。因为覆盖的优先级大于取反,所以此时把 $tag_u$ 改成应该赋的值( $0$ 或 $1$ ),取反调成 $0$ 。 ```cpp void modify_opposite(int u,int l,int r,int x,int y) { if(x <= l && y >= r) { if(tag[u] != -1) tag[u] = 1 - tag[u]; if(tag[u] == -1) rev[u] = 1 - rev[u]; } else { int mid = (l+r)>>1; pushdown(u); if(x <= mid) modify_opposite(lc(u),l,mid,x,y); if(y > mid) modify_opposite(rc(u),mid+1,r,x,y); } } ``` 只讲 ```if``` 。如果该节点 $u$ 未被覆盖,则把它的取反值翻转一下;如果被覆盖了,就只翻转一下它的覆盖值。 ```cpp void query(int u,int l,int r) { if(l == r) { if(tag[u] != -1) { ans[l] = tag[u]; } if(tag[u] == -1) { ans[l] = rev[u]; } } else { int mid = (l+r)>>1; pushdown(u); query(lc(u),l,mid); query(rc(u),mid+1,r); } } ``` 这一步是遍历线段树的叶子节点。找到时,如果该节点未被覆盖,则取其取反值(未被取反则 $0$ ,被取反则 $1$ );如果被覆盖了,就直接取它的 $tag$ 值,因为此时它的 $rev$ 一定为 $0$ ,在 ```pushdown``` 操作的时候就搞定了。 ```cpp memset(tag,-1,sizeof(tag)); ``` 初始化 $tag$ 。 ```cpp for(int i = 1; i <= m; i++) { char cal,pre,suf,tmp; int lef,rig; cin>>cal>>pre>>lef>>tmp>>rig>>suf; lef <<= 1;rig <<= 1; if(pre == '(') lef += 1; if(suf == ')') rig -= 1; if(cal == 'U') { modify_cover(1,0,M,lef,rig,1); } if(cal == 'I') { if(lef-1 >= 0) modify_cover(1,0,M,1,lef-1,0); if(rig+1 <= M) modify_cover(1,0,M,rig+1,M,0); } if(cal == 'D') { modify_cover(1,0,M,lef,rig,0); } if(cal == 'C') { if(lef-1 >= 0) modify_cover(1,0,M,1,lef-1,0); if(rig+1 <= M) modify_cover(1,0,M,rig+1,M,0); modify_opposite(1,0,M,lef,rig); } if(cal == 'S') { modify_opposite(1,0,M,lef,rig); } } query(1,0,M); ``` 输入时改左右端点遵循“对于闭区间 $[a,b]$ 替换成 $[2a,2b]$ ,对于开区间 $(a,b)$ 替换成 $[2a+1,2b-1]$ ,如此可以按照纯闭区间处理”的原则。 ```cpp bool temp = 0, now = 0; for(int i = 0; i <= N; i++) { if(ans[i]) { temp = 1; if(now == 1) continue; if(!(i&1)) { printf("[%d,",i>>1); now = 1; } if(i&1) { printf("(%d,",i>>1); now = 1; } } if(!ans[i]) { int las = i-1; if(now == 0) continue; if(!(las&1)) { printf("%d] ",las>>1); } if(las&1) { printf("%d) ",(las+1)>>1); } now = 0; } } if(!temp) puts("empty set"); return 0; ``` 扫一遍,实现方法不唯一。 btw ,在我写 ```if(!ans[i])``` 中的 ```las&1``` 的时候,因为是取 $i$ 的开区间,所以 ```las``` 要 ```+1``` 。 $O(m \log n)