2022-11-13 Acwing每日一题

本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的。同时也希望文章能够让你有所收获,与君共勉!

字典树

今天来学习字典树Trie,Trie树主要支持两个操作,一个是构建Trie时需要的插入操作Insert,另一个是查询操作query
首先来看看什么是字典树Trie,标准的Trie树是字符串组成的,结点是每个字符。图片来自这里image
如果是数组来模拟Trie树,就是这样。image
既然是树,那么必然存在一个根节点,只不过这里是虚拟节点root,由于小写字符总共有26个,因此总共有26个子节点。数组是这样定义的:son[p][u],其中p可以认为Trie树的深度(从根节点开始算,root为-1层),也可以理解为p服从某种顺序。u是该层所存储的东西,可以是数字也可以是字母(需要映射为数字),而son[p][u]就可以理解为第p层上的字母u的下一层索引(,对于u来说其是固定的,如果存在该字符我们就可以直接使用并通过son[p][u]获得该字符下一层所对应的结点索引),除此之外还有一个idx,它的作用类似一个线性表的在当前位置上的索引,具体作用看代码,这样对于每一个字符就得到了一个具体而准确的定位。
Trie的insert操作:
主要分两种情况,从0开始遍历层数,如果下一层存在该字符,就继续往下走,如果不存在就在下一层该字符处创建新的索引。
Trie的query操作:直接从根节点遍历Trie树,如果某一个字符存在就会自己往下走,否则不存在就可以直接返回。

这就是Trie树的主体,如果对于每个单词,我们想知道他们出现的次数,我们还可以定义cnt数组,来标记第p层存在单词,并且用来表示存在几个单词,而对于是否存在单词我们是在遍历过程中才能发现的,如果不存在某一个单词,那么遍历都不会完成,深度也不会达到p的。

代码表示

#include <iostream>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100000,idx;
int son[N][26],cnt[N];
int n;


void insert(char* s){
	int p = 0;
	for(int i=0; s[i] ; ++i){
		int u = s[i] - 'a';
		if(!son[p][u])	son[p][u] = ++idx;	
		// idx可以看成是深度所形成的线性表的下标,++idx在第一次循环就表示根节点在第0层,第一个结点在第一层
		p = son[p][u];	// 就是去下一层
	}
	cnt[p] ++;
}

int query(char *s){
	int p = 0;
	for(int i=0 ; s[i] ; ++i){
		int u = s[i] - 'a';
		if(!son[p][u])	return 0;
		p  = son[p][u];
	}
	return cnt[p];
}

int main()
{
    cin >> n;
    while (n -- ){
        char op[2];
        char s[N];
        scanf("%s%s",op,s);
        if(*op == 'I'){
            Insert(s);
            
        }
        else{
            cout << Query(s) << endl;;
        }
    }
    return 0;
}

最大异或对

在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式
第一行输入一个整数 N。

第二行输入 N 个整数 A1~AN。

输出格式
输出一个整数表示答案。

数据范围
1≤N≤105,
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3

算法原理

遇到题我们先思考暴力怎么求解,然后再针对每一步考虑使用合适的数据结构,对于最大异或对我们先考虑暴力双重循环,两两之间比较异或值,最后选出最大的,核心代码max(res,a[i]^a[j])(其中^是异或运算符),但这个算法的时间复杂度太高,\(O(n^2)\),因此我们需要思考怎么进行优化,我们发现最外层遍历每一个数字不能优化,内层我们需要找出外层固定的这个数字的最大异或值,那么我们就是要针对找到最大异或值这一部分进行优化。我们知道两个数字的异或运算就是对位相反取1,相同取0,如2:10,3:11,2^3:01。那么什么时候取最大值呢,我们发现当每一位都位1时所对应的数字是最大的,高位为1时的数字比低位为1时要更大,所以对位相反时进行异或结果是最大的,我们更希望每一位都相反,因此我们可以先寻找是否存在该数字的最大异或值,这时我们就可以使用Trie树进行寻找,先来看个图就理解为什么要用Trie树啦。图片来自这里
image
这样做的话找到的不就是该数组存储的所有数中最大的异或值了嘛。

代码实现

#include <iostream>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 3300000;
int son[N][2],a[N];	// 只有0和1两个数,因此是二叉树且默认所有结点的下一层都为第0层(即不存在下一层)
int idx;


void insert(int x){	// 插入x的每一位,形成异或树
	int p = 0;
	for(int i=30; i>=0 ; --i){
		int u =  x>>i & 1;	// 获得x的第i位
		if(!son[p][u]){
			son[p][u] = ++idx;
		}
		p = son[p][u];
	}
}

int search(int x){	// 查询x的最大异或数
	int p = 0;
	int ans = 0;	// 所有位默认为0
	for(int i=30; i>=0 ; --i){
		int u = x >> i & 1;	// 取出第i位
		// 判断是否存在对位,如果存在就往下走,否则就走另一个
		if(son[p][!u])	{// 存在对位,说明该位可以是最大的,因此该位为1
			ans += 1<<i;	// 把1向左移到第i位
			p = son[p][!u]; // 走的不是原来的位置,而是对位
		}
		else{	// 不存在对位,所以该位就是0
			p = son[p][u];	// ans为0说明当前该位默认为0,直接移到下一位
		}
	}
	return ans;
}

int main()
{
    int n;
    cin >> n;
    int x;
    for(int i=0; i < n; ++i){
    	cin >> a[i];
		insert(a[i]); 
	}
	int res= 0;
	for(int i=0; i<n ; ++i) // 找到每一个数的最大异或值,再找到所有的数的最大异或值
	{
		res = max(res,search(a[i]));
	}
	cout << res << endl;
	return 0;
}
posted @ 2022-11-13 20:25  ZmQmZa  阅读(15)  评论(0编辑  收藏  举报