NOI Online 2021 #1 T3 岛屿探险(CDQ分治+Trie)

NOI Online 2021 #1 T3 岛屿探险

题目大意

  • n n n座岛屿编号为 1 − n 1-n 1n,每个岛屿有两个值 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) acmin(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,q1051a,b,c,d2241

题解

  • 部分分给了很清晰的提示。
  • 情况一:始终满足 d ≤ b d\le b db,则每个岛的 b b b并没有用,把 a a a插入 0 / 1 0/1 0/1的Trie树中,再把询问 c , d c,d c,d带进去查询。这里需要把询问拆成 l − 1 l-1 l1 r r r,然后和编号一起排序离线查询。查询具体的做法是,若 d d d当前位为 0 0 0,则进入异或后为 0 0 0的子树;否则加上异或后为 0 0 0的子树,再进入异或后为 1 1 1的子树。
  • 情况二:始终满足 b ≤ d b\le d bd,则每个询问的 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树来实现,都是比较常见的套路,但可能由于最近缺乏做题,意识不足,并未在这方面有任何想法。
posted @ 2021-03-27 22:31  AnAn_119  阅读(62)  评论(0编辑  收藏  举报