HDU I love counting(莫队+字典树)

目录

字典树

01字典树

莫队

I Love Counting 题解


题目链接:Problem - 6964 (hdu.edu.cn)

题意:n个数的序列,每个位置都有一个权值c,进行Q次询问,每次询问给定一个区间(l,r)和两个数字a,b,问这个序列有多少种权值c满足c异或a<=b?

知识点:

字典树

参考:(4条消息) 字典树与01字典树详解_菜鸡成长史-CSDN博客_01字典树

插入

设数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。

1.字典树的边表示字母

2.字典树的结点用于存放一个特殊字符,记录从根节点到这个节点为止这样一个单词。

3.有相同前缀的单词共用前缀结点,所以我们可以快速跑出最长公共前缀、最多单词的公共前缀等等。

4.第二种编号方式是由单词字母所在的位置即深度和字母的字典序所决定的,每个节点的子节点都应该从0编到25,会造成比较大的空间浪费。而第一种编号方式显然比较适用于大部分情况,节约空间,用到哪个分哪个。(第一种编号:从左往右扫字母编号;第二种编号方式:a~z编号0~25)

查找

从左往右依次扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个前缀;前缀扫完则表示有这个前缀。可以查找前缀、单词是否出现过,或者查找前缀出现的次数(开一个数组sum[]存储),查找某个单词等等。

Problem - 1251 (hdu.edu.cn)

题意:给定一些单词,再给出一些前缀询问这些前缀出现次数?

#include<iostream>
#include<stdio.h>
using namespace std;
#define maxn 1000005 
int sum[maxn] = { 0 };//求每个前缀出现次数
int trie[maxn][26];
char s[15];
int pos = 1;//编号
//插入
void add()
{
	int c = 0;
	for (int i = 0; s[i]; i++)
	{
		int x = s[i] - 'a';
		if (trie[c][x] == 0)
			trie[c][x] = pos++;
		c = trie[c][x];
		sum[c]++;//更新前缀出现次数
	}
}
//查询
void query()
{
	int c = 0;
	for (int i = 0; s[i]; i++)
	{
		int x = s[i] - 'a';
		if (trie[c][x] == 0)
		{
			printf("0\n");
			return;
		}
		c = trie[c][x];
	}
	printf("%d\n", sum[c]);
}
int main()
{
	while (gets(s)&& s[0] != NULL)
		add();
	while (gets(s))
		query();

}


01字典树

1.适用问题

01字典树主要用于解决求异或最值的问题。

01字典树和普通的字典树原理类似,只不过把插入字符改成了插入二进制串的每一位(0或1)。

2.01字典树形式

(1)01字典树是一棵最多32层的二叉树,其每个节点的两条边分别表示二进制的某一位的值是 0 还是 1,将某个路径上边的值连起来就得到一个二进制串。

(2)节点个数为 1 的层(最高层)节点的边对应着二进制串的最高位。

(3)以上代码中,trie[i] 表示一个节点,trie[i][0] 和 trie[i][1] 表示节点的两条边指向的节点,val[i] 表示节点的值。

(4)每个节点主要有 4个属性:节点值、节点编号、两条边指向的下一节点的编号。

(5)节点值 val为 0 时表示到当前节点为止不能形成一个数,否则 val[i]=数值。

(6)可通过贪心的策略来寻找与x异或结果最大(最小)的数即优先找和x的二进制的未处理的最高位值不同(相同)的边对应的点,这样保证结果最大。 

例子:Problem - 4825 (hdu.edu.cn)

题意:给出n个数和m次询问,每次询问给出一个数x,问在n个数中哪个数与x异或值最大?

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1000006;
int a[maxn], vis[maxn * 3];
int trie[maxn * 3][3];
int pos;
//插入
void add(int x, int id)
{
	int c = 0;
	int op;
	for (int i = 31; i >= 0; i--)
	{
		op = ((x & (1 << i)) != 0); //取出x的二进制的第i位(从右往左数(即先取出最高位)
		if (trie[c][op] == 0)
			trie[c][op] = pos++;
		c = trie[c][op];
	}
	vis[c] = id;
}
int get(int x)
{
	int c = 0;
	int op;
	for (int i = 31; i >= 0; i--)
	{
		op = ((x & (1 << i)) != 0);
		if (trie[c][op ^ 1]) //尽可能走与当前位不同的点(因为要让异或值尽可能大)
			c = trie[c][op ^ 1];
		else
			c = trie[c][op];
	}
	return a[vis[c]];

}
int main()
{
	int t;
	scanf("%d", &t);
	for (int j = 1; j <= t; j++)
	{
		int n, m;//集合中有n个数,发起m次询问
		scanf("%d%d", &n, &m);
		pos = 1;
		memset(trie, 0, sizeof(trie));
		memset(vis, 0, sizeof(vis));
		for (int i = 1; i <= n; i++)
		{
			scanf("%d", &a[i]);
			add(a[i], i);
		}
		printf("Case #%d:\n", j);
		for (int i = 1; i <= m; i++)
		{
			int c;
			scanf("%d", &c);
			printf("%d\n", get(c));
		}
		
	}
	return 0;
}

莫队

参考:莫队详解 - JSOI爆零珂学家yzhang - 博客园 (cnblogs.com)

I Love Counting 题解

法一.采用莫队+分块
仅考虑第j位
1)b为1,a为1,那么c为1即满足c^a<b

2)b为1,a为0,那么c为0即满足c^a<b

3)b为0,a为1,那么c只能为1才满足c^a>=b

4)b为0,a为0,那么c只能为0才满足c^a>=b

最后要加上c^a=b的数量

#include <iostream>
#include<algorithm>
#define N 100005
using namespace std;
struct query 
{
	int l, r, id, bl;
	int a, b;
}q[N];
int ans[N];
int sum[N];//记录每个分块有多少种c
int cnt[N];//每个c有多少个
int c[2*N];//c[i]表示每个i这个数的种类
int v[N];
int k;//记录分块大小
void add(int x)
{
	if (++cnt[x] == 1)
	{
		sum[x/ k]++;
		c[x]++;
	}
}
void del(int x) {
	if (--cnt[x] == 0)
	{
		sum[x / k]--;
		c[x]--;
	}
}
bool cmp(query x, query y)//排序第一关键字是询问的左端点所在块的编号,第二关键字是询问的右端点本身的位置,都是升序
{
	if (x.bl!= y.bl)return x.bl < y.bl;
	else return x.r < y.r;
}
int ask(int x) {
	int res = 0;
	for (int i = x / k * k; i <= x; i++)res += c[i];
	for (int i = 0; i < x / k; i++)res += sum[i];
	return res;
}
int main()
{
	int n , m;
	cin >> n ;
	k = sqrt(n);//分块大小
	for (int i = 1; i <= n; ++i)
		for (i = 1; i <= n; i++)scanf_s("%d", &v[i]);
	cin >> m;//m次询问
	for (int i = 1; i <= m; ++i)
	{

		scanf_s("%d%d%d%d", &q[i].l, &q[i].r, &q[i].a, &q[i].b);
		q[i].bl = (q[i].l - 1) / k + 1;
		q[i].id = i;
	}
	sort(q + 1, q + m + 1, cmp);
	int l = 1, r = 0;
	for (int i = 1; i <= m; ++i)
	{
		int ll = q[i].l, rr = q[i].r;
		while (l < ll)
			del(v[l++]);
		while (l > ll)
			add(v[--l]);
		while (r < rr)
			add(v[++r]);
		while (r > rr)
			del(v[r--]);
		int s = 0;
		int a = q[i].a, b = q[i].b;
		for (int j = 19; j >= 0; j--) 
		{
			if (b >> j & 1) //如果b的j位是1
            {
				int p = s;
				if (a >> j & 1)//a的j位是1
                    p |= 1 << j;//p的j位设为1
				else 
                    s |= 1 << j;
			     ans[q[i].id] += ask(p + (1 << j) - 1) - ask(p - 1);
			}
			else //b的j位置是0
                if (a >> j & 1)s |= 1 << j;
		}
		ans[q[i].id] += c[q[i].a ^ q[i].b];
	}
	for (int i = 1; i <= m; ++i)
		cout << ans[i]<<endl;
	return 0;
}

法二.字典树(之后补)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值