2021杭电多校中超联赛第一二场莫队Trie树题汇总题解

2021杭电多校中超联赛第一二场莫队Trie树题汇总题解


第一场1006 Xor sum

传送门:Xor sum
题意:

  • t (1 ≤ t ≤ 100)组询问,每组询问两行,第一行n (1 ≤ n ≤ 105) 和 k (0 ≤ k ≤ 230),第二行 n 个数字 ai ( 0 ≤ ai ≤ 230)。要求求最短的连续序列,使得aL ^ aL+1 ^ aL+2 ^ … aR-1 ^ aR ≥ k ,每组询问输出满足序列的 L 和 R ,若满足序列有多个则输出 L 最小的。

思路:

  • 首先令数组wi为前i个a数组异或的结果,即前缀异或。由异或符号的性质a^a=0可得
    wL ^ wR = ( a1 ^ a2 ^ … ^ aL ) ^ ( a1 ^ a2 ^ … ^ aR) = aL+1 ^ aL+2 ^ … ^ aR
  • 因此我们需要一种数据结构使得丢进 wR 能返回 w1 ~ wR-1 中与 wR 异或后大于等于 k 的最大 wi ,这样才能使得序列长度最短( 该 wi 即为 wL ) ,因此我们想到建立01Trie树来进行维护。
  • 依次遍历每个前缀异或值,先对 wi 进行询问操作,此时Trie树中为 w1 ~ wi-1 的值,查询以 i 为 R 是否存在满足要求的L,若有则更新答案。
  • 具体询问的方式为使用位运算从高位开始遍历每一位,如果k的当前位为 1 ,那么我们异或的结果只能为 1 才能保证不小于 k ;如果 k 的当前位为 0 ,则异或结果为 0 或 1 都可,当异或结果为 1 时必定满足大于等于k,则用当前节点的 id 数组来更新当前 L ,并向 0 的方向继续寻找是否存在更大的 L 值。如果节点不存在,则表明 w1 ~ wi-1 中不再存在使得大于等于k成立的节点,则退出。
  • 询问操作结束后将 wi 进行插入操作,对于 wi 每一个会经过的节点维护一个 id 数组来记录到达该节点的最大 i 为多少。其实这样操作之后会发现题目要求的 “ 如果多个满足输出 L 最小的 ” 是不需要管的,因为我们每次是固定右端点寻找左端点,如果区间长度与之前寻找的区间长度一样的话左端点必然是大于已经寻找出的最优解的。
  • 时间复杂度 遍历序列O(n),建树O(logn),总计O( nlogn )。具体操作看代码注释。

AC代码(赛后提交405ms)

#include <bits/stdc++.h>
#define x first
#define y second
#define ls u<<1
#define rs u<<1|1
#define debug(x) cout<<"debug"<<' '<<x<<endl;
using namespace std;
const int N = 100010;
const int P = 13331;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int>PII;

int k, t, n, m;
int tr[N * 32][2], idx;
int id[N * 32], w[N];
int l, r, flag;

void insert(int pos, int x) {
	int p = 0;
	for (int i = 29; i >= 0; i--) {
		int t = x >> i & 1;
		if (!tr[p][t]) {
			tr[p][t] = ++idx;
			tr[idx][0] = tr[idx][1] = 0;//用memset会多清空导致tle,但是这样不会
			id[idx] = -1;
		}

		p = tr[p][t];
		id[p] = max(id[p], pos); //记录到达当前节点的最大下标
	}
}

void query(int pos, int x) {
	int p = 0;
	int ans = -1;
	for (int i = 29; i >= 0; i--) {
		int t = (x >> i ) & 1;
		if ((k >> i) & 1) { //k当前位为1,只能朝异或结果为1的方向走
			p = tr[p][1 ^ t];
		} else { //k当前位为0,异或结果0或1都可
			if (tr[p][1 ^ t]) //如果异或结果为1存在
				ans = max(ans, id[tr[p][1 ^ t]]); //更新答案
			p = tr[p][t]; //朝异或结果为0的方向继续寻找是否存在更大值
		}
		if (!p)
			break;//使得>=k的节点不存在则break
	}
	if (p)
		ans = max(ans, id[p]);
	if (ans >= 0 && pos - ans < r - l) {
		flag = 1;
		l = ans, r = pos;
	}

}


int main() {
	cin >> t;
	while (t--) {
		scanf("%d%d", &n, &k);
		//初始化
		l = -1;
		r = n + 1;
		flag = 0;//确定是否存在合法序列满足要求
		tr[0][0] = tr[0][1] = 0;
		idx = 0;

		for (int i = 1; i <= n; i++) {
			int a;
			scanf("%d", &a);
			w[i] = w[i - 1] ^ a;
			query(i, w[i]);
			insert(i, w[i]);
		}
		if (flag) {
			printf("%d %d\n", l + 1, r);
		} else
			puts("-1");

	}
}

第一场1010 zoto

传送门:zoto
题意:

  • t (1 ≤ t ≤ 5) 组数据,每组数据2 + m 行,第一行n (1 ≤ n ≤ 105) 和 m (0 ≤ m ≤ 105),第二行n个数字ai ( 0 ≤ ai ≤ 105) ,代表 f [ i ] = ai。之后m行询问每行 x0 y0, x1 ,y1 ( 1 ≤ x0 ≤ x1 ≤ n, 0 ≤ y0 ≤ y1 ≤ 105 ),问 区间 [ x0 , x1 ] 中在 [ y0 , y1 ]之间的函数值有多少

思路:

  • dalao们的经典二维数点题,蒟蒻补前置知识都补了好久orz。
  • x轴将所有询问存下用莫队进行离线操作,y轴使用分块来进行计数。学了分块和基础莫队之后再看就都是板子。
  • 时间复杂度 O(n * sqrt(n)+m * sqrt(n) )具体操作看代码注释。

AC代码(赛后提交1669ms)

#include <bits/stdc++.h>
#define x first
#define y second
#define ls u<<1
#define rs u<<1|1
#define debug(x) cout<<"debug"<<' '<<x<<endl;
using namespace std;
const int N = 1000010;
const int P = 13331;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int>PII;

int k, t, n, m;
int w[N], ans[N], cnt[N], tot[N];
int len, leny;

struct query {
	int l, r, a, b, id;
} q[N];

int get(int x) { //返回x轴分块
	return x / len;
}

int gety(int x) { //返回y轴分块
	return x / leny;
}

bool cmp(query x, query y) {
	if (get(x.l) == get(y.l)) {
		if (get(x.l) & 1)
			return x.r < y.r;
		else
			return x.r > y.r;
	}
	return get(x.l) < get(y.l);
}

void add(int x) {
	if (!cnt[x])
		tot[gety(x)]++;//tot记录这个块中拥有的不同数值种类数量
	cnt[x]++;//cnt记录该数值出现总次数
}

void del(int x) {
	cnt[x]--;
	if (!cnt[x])
		tot[gety(x)]--;
}

void get_ans(int id, int l, int r) {
	//分块板子
	int res = 0;
	if (gety(l) == gety(r)) {
		for (int i = l; i <= r; i++)
			if (cnt[i])
				res++;
	} else {
		int i = l, j = r;
		while (gety(i) == gety(l)) {
			if (cnt[i])
				res++;
			i++;
		}
		while (gety(j) == gety(r)) {
			if (cnt[j])
				res++;
			j--;
		}
		for (int k = gety(i); k <= gety(j); k++)
			res += tot[k];
	}
	ans[id] = res;
}

int main() {
	cin >> t;
	while (t--) {
		cin >> n >> m;
		int maxn = 0; //记录y轴最大值用来y轴分块
		memset(cnt, 0, sizeof cnt);
		memset(tot, 0, sizeof tot);
		for (int i = 1; i <= n; i++) {
			scanf("%d", &w[i]);
			maxn = max(maxn, w[i]);
		}

		len = 1.0 * n / sqrt(m) + 1; //x轴一个块的长度
		leny = sqrt(maxn); //y轴一个块的长度

		for (int i = 1; i <= m; i++) {
			int l, r, a, b;
			scanf("%d%d%d%d", &l, &a, &r, &b);
			q[i] = {l, r, a, b, i};
		}

		sort(q + 1, q + 1 + m, cmp);
		//基础莫队板子
		for (int k = 1, j = 1, i = 0; k <= m; k++) {
			int id = q[k].id, l = q[k].l, r = q[k].r;
			int a = q[k].a, b = q[k].b;
			while (i < r)
				add(w[++i]);
			while (i > r)
				del(w[i--]);
			while (j > l)
				add(w[--j]);
			while (j < l)
				del(w[j++]);
			//x轴到达[l ,r]区间后对y轴[a ,b]区间用分块进行查询操作
			get_ans(id, a, b);
		}

		for (int i = 1; i <= m; i++)
			printf("%d\n", ans[i]);
	}
}

换皮题:作业


第二场1004 I love counting

传送门: I love counting
题意:

  • 一组数据,第一行 n ( n ≤ 105 ) 代表序列长度,第二行n个数字 ci ( 0 ≤ ci ≤ n ),第三行Q( Q ≤ 105 )代表询问个数,接下来Q行 L , R , a , b ( 1 ≤ L ≤ R ≤ n , a ≤ n + 1 , b ≤ n + 1 )问 序列 L ~ R 中 满足 ci ^ a ≤ b 的数值种类

思路:

  • 前两道题的综合题,位运算从高位向低位遍历,对于每一位都计算该位满足的区间中的数值种类个数,则转换为了区间计数问题即1010题。详细讲解: 移步
  • 具体操作看代码注释。

AC代码(赛后提交1268ms)

#include <bits/stdc++.h>
#define x first
#define y second
#define ls u<<1
#define rs u<<1|1
#define debug(x) cout<<"debug"<<' '<<x<<endl;
using namespace std;
const int N = 1000010;
const int P = 13331;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int>PII;

int k, t, n, m;
int w[N], ans[N], cnt[N], tot[N];
int len, leny;

struct query {
	int l, r, a, b, id;
} q[N];

int get(int x) { //返回x轴分块
	return x / len;
}

int gety(int x) { //返回y轴分块
	return x / sqrt(N);
}

bool cmp(query x, query y) {
	if (get(x.l) == get(y.l)) {
		if (get(x.l) & 1)
			return x.r < y.r;
		else
			return x.r > y.r;
	}
	return get(x.l) < get(y.l);
}

void add(int x) {
	if (!cnt[x])
		tot[gety(x)]++;//tot记录这个块中拥有的不同数值种类数量
	cnt[x]++;//cnt记录该数值出现总次数
}

void del(int x) {
	cnt[x]--;
	if (!cnt[x])
		tot[gety(x)]--;
}

void get_ans(int id, int l, int r) {
	//分块板子
	int res = 0;
	if (gety(l) == gety(r)) {
		for (int i = l; i <= r; i++)
			if (cnt[i])
				res++;
	} else {
		int i = l, j = r;
		while (gety(i) == gety(l)) {
			if (cnt[i])
				res++;
			i++;
		}
		while (gety(j) == gety(r)) {
			if (cnt[j])
				res++;
			j--;
		}
		for (int k = gety(i); k <= gety(j); k++)
			res += tot[k];
	}

	ans[id] += res;
}

int main() {
	cin >> n;
	for (int i = 1; i <= n; i++)
		scanf("%d", &w[i]);
		
//  求大佬解释一下为啥
//  先赋值给leny调用get函数的时候会t但是gety函数里直接写除sqrt(N)不会t,太玄学了
 	len =sqrt(n); //x轴一个块的长度
//	leny = sqrt(N); //y轴一个块的长度

	cin >> m;
	for (int i = 1; i <= m; i++) {
		int l, r, a, b;
		scanf("%d%d%d%d", &l, &r, &a, &b);
		q[i] = {l, r, a, b, i};
	}

	sort(q + 1, q + 1 + m, cmp);
	//基础莫队板子
	for (int k = 1, j = 1, i = 0; k <= m; k++) {
		int id = q[k].id, l = q[k].l, r = q[k].r;
		int a = q[k].a, b = q[k].b;
		while (i < r)
			add(w[++i]);
		while (i > r)
			del(w[i--]);
		while (j > l)
			add(w[--j]);
		while (j < l)
			del(w[j++]);


		//进行类似Trie树的操作
		int c = 0;
		for (int pp = 17; pp >= 0; pp--) { //数据范围1e5从17开始够了
			int t = a >> pp & 1;
			if (b >> pp & 1) {
				if (t) { //如果a,b该位都为1,那么c当前也为1的话c^a必然≤b,低位的数值将不影响结果
					get_ans(id, c + (1 << pp), c + (1 << (pp + 1)) - 1);
					//c+=0<<pp;继续往0方向走即保持高位和b相同的情况寻找合法区间
				} else { //如果a该位为0 b该位为1,c当前为0的话低位对结果无影响
					get_ans(id, c, c + (1 << pp) - 1);

					c += 1 << pp;//往1方向走即保持高位和b相同的情况寻找合法区间
				}
			} else
				c += t << pp;
		}
		get_ans(id, a ^ b, a ^ b);

	}

	for (int i = 1; i <= m; i++)
		printf("%d\n", ans[i]);

}
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值