Trie字典树
Trie又称字典树用来高效的存储与查找字符串。例题:字符串统计
模板如下:
int son[maxn][N],cnt[maxn],idx; //其中N的取值和题目有关,例如字符串全是小写字母则N = 26
//son[][]数组用来存储字符串,本题中它存储的是字符串中每个字符的下标,cnt[i]表示以i结尾的字符串总共出现的次数。
void insert(char str[]){
int p = 0;
for(int i = 0; str[i] ;i++){ //因为字符数组最后一个元素是'\0'被视为空
int u = str[i] - 'a';
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
return ;
}
int query(str []){
int p = 0;
for(int i = 0;str[i];i++){
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
并查集
并查集很常用,因为代码较短,但是需要精巧的思路。它的作用是:
1.将两个集合合并。 2.询问两个集合是否在一个集合中。
将以上两个操作在近似O(1)的时间内完成。
并查集将每个集合用树表示,树根的编号是集合的编号。利用p[x]表示x父节点这样一来,
判断树根:if(p[x] == x)
求出集合x编号:while(p[x] != x) x = p[x];其中,这一步的时间复杂度与树的高度有关,为了优化引入:路径压缩。
合并集合:x的根节点指向y的根节点p[x] =y。或者,p[y] = x;
例题:并查集
模板如下:
#include<iostream>
using namespace std;
const int maxn = 1e5 + 10;
int n,m,p[maxn];
int find(int x){ //核心,已经包含了路径压缩
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
for(int i = 1;i<=n;i++) p[i] = i;
while(m--){
string opt;
cin>>opt;
int a,b;
scanf("%d%d",&a,&b);
if(opt == "M"){
p[find(a)] = find(b);
}else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
有时并查集还需要维护其他信息,比如一个集合中元素的个数。代码如下:
#include<iostream>
using namespace std;
const int maxn = 1e5 + 10;
int p[maxn],num[maxn],n,m; //num数组动态维护集合中元素数量,只维护根节点
int find(int x){
if(p[x] != x) p[x] =find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
for(int i = 1; i<=n ;i++){
p[i] = i;
num[i] = 1;//初始化为1
}
while(m--){
string opt;
cin>>opt;
int a,b;
if(opt == "C"){
scanf("%d%d",&a,&b);
if (find(a) != find(b)){
num[find(b)] += num[find(a)]; //只修改根节点的num值
p[find(a)] = find(b);
}
}else if(opt == "Q1"){
scanf("%d%d",&a,&b);
if(p[find(a)]==p[find(b)]) puts("Yes");
else puts("No");
}else{
scanf("%d",&a);
cout << num[find(a)]<< endl; //输出根节点
}
}
return 0;
}
堆
先介绍下完全二叉树,除了最后一层外,每层节点都有两个子节点。且最后一层节点只能从左到右依次排布。完全二叉树可以用一维数组存储,下标从1开始。
堆是特殊的完全二叉树,它可以被分为小根堆和大根堆。小根堆满足:每个节点的若有子节点,根节点的值小于子节点的值。
堆的基本操作,堆构建,调整堆(向上调整up,向下调整down)
模板如下:例题堆排序
#include<iostream>
#include<algorithm>
int len,heap[maxn]; //len表示堆的长度,heap表示堆,以小根堆为例。
void down(int u){
int t = u;
if(2 * u <= len && heap[t]<=heap[2*u]) t = 2*u;
if(2*u + 1 <= len && heap[t]<=heap[2*u+1]) t = 2*u + 1;
if(u!=t){
swap(heap[t],heap[u]);
down(t);
}
}
void up(int u){
while(u/2 && heap[u]<heap[u/2]){
swap(heap[u],heap[u/2]);
up(u/2);
}
}
int main(){
for(int i = 1; i<=n ;i++) cin>>heap[i]; //下标从1开始!
len = n;
//构建堆,复杂度是O(n),可证明。
for(int i = n/2; i>=1 ;i--) down(i);
while(m--){ //堆排序过程,用最后一个元素覆盖第一个元素,然后down一遍
cout<<heap[1]<<" ";
heap[1] = heap[len--];
down(1);
}
}
哈希表
哈希表主要用来将较大范围的数,映射到较小范围。这里的范围值得是数量。举个例子,将0-1000的数映射到0-50。哈希表主要操作:1.哈希函数,它可以被定义为直接取模 eg. x mod p。其中对于p而言,要选择质数,可以证明这样选择冲突概率最小。2.冲突处理,可以分为拉链法和开放寻址法。
拉链法
当发生冲突的时候,将冲突元素用链表存储。链表要存储原始值。一般而言,链表的长度不会太长,因此它可以看做O(1)的。如果需要删除元素的话,一般而言设置标志位,而不直接在链表中删除。
模板,例题:模拟散列
const int maxn = 1e5 + 3; //需要根据题目找p
int h[maxn],val[maxn],ne[maxn],idx;
void insert(int x){
int k = (x%maxn + maxn) % maxn; //将负数和正数取模的结果,统一转换为正数
val[idx] = x;
ne[idx] = h[k],h[k] = idx++; //h[k]作为头指针,相当于head
}
bool find(int x){
int k = (x%maxn + maxn)%maxn;
for(int i = h[k]; i!=-1 ;i = ne[i])
if(val[i] == x) return true;
return false;
}
开放寻址法
开放寻址法,当发生冲突时自动寻找到下一位,直到找到空为止。因此,通常需要较大的空间来避免冲突,一般而言要高于题目数据量的2-3倍。对于删除而言,同样是加标记。
模板:
const int maxn = 3e5 + 3,null = 0x3f3f3f3f; //null满足在题目中,数据元素外即可
int h[maxn];
int find(int x){
int k = (x % maxn + maxn)% maxn;
while(h[k]!=null && h[k] != x){ //当前位置为空且不是x,继续寻找
k++;
if(k == maxn) k = 0; //找到边界,从头寻找。
}
return k;
}
字符串哈希
字符串哈希首先将字符串视为一个P进制的数,接着将将其转换为10进制数,然后对较小的数字Q取模,最终映射到0-Q-1之间。
需要注意:
1.一般情况不要将,某一个字母映射为0.
2.一般,p取133或13331,Q取2^64不会发生冲突。但是不是100%。
用法,快速判断某两串字符串是否相同。例题字符串哈希
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 1e5 + 10,P = 131; //取P = 131
int n,m;
char str[maxn];
unsigned long long h[maxn],p[maxn]; //利用unsigned long long存储字符串的哈希值,当哈希值可以被自动取模,而不需要手动取模
unsigned long long Hash(int l ,int r){ //返回处于区间[l,r]的字符串的哈希值
return h[r] - h[l - 1] *p[r - l + 1];
}
int main(){
cin>>n>>m;
scanf("%s",str + 1);
p[0] = 1;
for(int i = 1;i<=n;i++){
p[i] = p[i-1] * P; //构造P的n次幂,加速计算
h[i] = h[i-1] * P + str[i]; //构造前缀哈希值
}
while(m--){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(Hash(l1,r1) == Hash(l2,r2)) puts("Yes"); //假定不会冲突。
else puts("No");
}
return 0;
}
本章,涵盖了《数据结构》的很多考点。作为计算机专业的学生必须熟悉。至于图论将在下一章讲解。本篇全部使用手撕的方式实现这些数据结构,它们的一个好处是速度快。下节我们使用STL实现数据结构。