NOI Online 2021 #1 T3 岛屿探险(CDQ分治+Trie)
NOI Online 2021 #1 T3 岛屿探险
题目大意
- n n n座岛屿编号为 1 − n 1-n 1−n,每个岛屿有两个值 a i , b i a_i,b_i ai,bi。 q q q次询问,给出 l , r , c , d l,r,c,d l,r,c,d,求编号在 [ l , r ] [l,r] [l,r]中的岛屿满足 a ⨁ c ≤ min ( b , d ) a\bigoplus c\le \min(b,d) a⨁c≤min(b,d)的数量。
- n , q ≤ 1 0 5 , 1 ≤ a , b , c , d ≤ 2 24 − 1 n,q\le10^5,1\le a,b,c,d\le2^{24}-1 n,q≤105,1≤a,b,c,d≤224−1
题解
- 部分分给了很清晰的提示。
- 情况一:始终满足 d ≤ b d\le b d≤b,则每个岛的 b b b并没有用,把 a a a插入 0 / 1 0/1 0/1的Trie树中,再把询问 c , d c,d c,d带进去查询。这里需要把询问拆成 l − 1 l-1 l−1和 r r r,然后和编号一起排序离线查询。查询具体的做法是,若 d d d当前位为 0 0 0,则进入异或后为 0 0 0的子树;否则加上异或后为 0 0 0的子树,再进入异或后为 1 1 1的子树。
- 情况二:始终满足 b ≤ d b\le d b≤d,则每个询问的 d d d没有用,把 a a a和 b b b一起插入Trie树中,插入的方法和情况一查询的方法类似,把满足条件的子树打上标记。然后再把 c c c带进去查询。同样需要把询问拆成两部分。
- 满分的做法,可以用CDQ分治,先把询问和插入按 d d d和 b b b一起排序,然后分成上面两种情况做,每个区间都按编号再排一次序,两个指针边扫边插入和查询即可。
- 要注意的是查询到Trie树的叶子节点也要加入答案,插入到Trie树的叶子节点也要打上标记。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100010
struct node {
int o, x, k, c, r;
}a[N * 3], p[N * 3], q[N * 3], st[N * 3];
struct {
int p[2], s;
}f[N * 48];
int ans[N], sum = 1;
int cmp(node x, node y) {
return x.x < y.x;
}
void insert(int k, int c) {
int x = 1;
for(int i = 23; i >= 0; i--) {
int o = 0;
if(k & (1 << i)) o = 1;
if(!f[x].p[o]) f[x].p[o] = ++sum;
x = f[x].p[o];
f[x].s += c;
}
}
int find(int k, int mi) {
int s = 0, x = 1;
for(int i = 23; i >= 0; i--) {
int o = 0;
if(k & (1 << i)) o = 1;
if(mi & (1 << i)) {
if(f[x].p[o]) s += f[f[x].p[o]].s;
if(f[x].p[!o]) x = f[x].p[!o]; else break;
}
else {
if(f[x].p[o]) x = f[x].p[o]; else break;
}
if(i == 0) s += f[x].s;
}
return s;
}
void change(int k, int mi, int c) {
int x = 1;
for(int i = 23; i >= 0; i--) {
int o = 0;
if(k & (1 << i)) o = 1;
if(mi & (1 << i)) {
if(!f[x].p[o]) f[x].p[o] = ++sum;
f[f[x].p[o]].s += c;
if(!f[x].p[!o]) f[x].p[!o] = ++sum;
x = f[x].p[!o];
}
else {
if(!f[x].p[o]) f[x].p[o] = ++sum;
x = f[x].p[o];
}
}
f[x].s += c;
}
int get(int k) {
int x = 1, s = 0;
for(int i = 23; i >= 0; i--) {
int o = 0;
if(k & (1 << i)) o = 1;
if(f[x].p[o]) x = f[x].p[o]; else break;
s += f[x].s;
}
return s;
}
void solve(int l, int r) {
if(l == r) return;
int mid = (l + r) / 2;
solve(l, mid);
solve(mid + 1, r);
int ps = 0, qs = 0, i;
for(i = l; i <= mid; i++) if(a[i].o) p[++ps] = a[i];
for(i = mid + 1; i <= r; i++) if(!a[i].o) q[++qs] = a[i];
int x = 1;
for(i = 1; i <= ps; i++) {
while(x <= qs && q[x].r <= p[i].r) insert(q[x].k, 1), x++;
ans[p[i].o] += find(p[i].k, p[i].x) * p[i].c;
}
for(i = 1; i <= qs && q[i].r <= p[ps].r; i++) insert(q[i].k, -1);
ps = qs = 0;
for(i = l; i <= mid; i++) if(!a[i].o) p[++ps] = a[i];
for(i = mid + 1; i <= r; i++) if(a[i].o) q[++qs] = a[i];
x = 1;
for(i = 1; i <= qs; i++) {
while(x <= ps && p[x].r <= q[i].r) change(p[x].k, p[x].x, 1), x++;
ans[q[i].o] += get(q[i].k) * q[i].c;
}
for(i = 1; i <= ps && p[i].r <= q[qs].r; i++) change(p[i].k, p[i].x, -1);
int j = l, k = mid + 1, t0 = l - 1;
while(j <= mid || k <= r) {
if(j <= mid && (k > r || a[j].r <= a[k].r)) st[++t0] = a[j], j++; else st[++t0] = a[k], k++;
}
for(i = l; i <= r; i++) a[i] = st[i];
}
int main() {
int n, Q, i, j, tot = 0;
scanf("%d%d", &n, &Q);
for(i = 1; i <= n; i++) {
scanf("%d%d", &a[i].k, &a[i].x);
a[++tot].o = 0, a[tot].r = i;
}
for(i = 1; i <= Q; i++) {
int L, R, C, D;
scanf("%d%d%d%d", &L, &R, &C, &D);
a[++tot].o = i, a[tot].r = R, a[tot].k = C, a[tot].x = D, a[tot].c = 1;
a[++tot].o = i, a[tot].r = L - 1, a[tot].k = C, a[tot].x = D, a[tot].c = -1;
}
sort(a + 1, a + tot + 1, cmp);
solve(1, tot);
for(i = 1; i <= Q; i++) printf("%d\n", ans[i]);
return 0;
}
自我小结
- 询问区间拆成两半,以及位运算计数用Trie树来实现,都是比较常见的套路,但可能由于最近缺乏做题,意识不足,并未在这方面有任何想法。
哈哈哈哈哈哈哈哈哈哈