本文复习内容概括:
Trie树和并查集都是针对集合插入/处理/查询操作所提出的高效处理算法
其中Trie树侧重于字符串集合的维护,并查集侧重于数字集合的合并
而哈希(hash)是将一个大集合按照一定的对应关系映射到小集合中,从而实现一定的优化目的。
字符串哈希是可以高效代替KMP的做法(求循环节除外,这个只能用KMP)。虽然有点复杂,但是实用性比较高。
Trie树
基本思路示意:树的存储方式:声明一个二维数组int son[N][26]
。其意义是对于每个节点有26个子节点,但只有当这些节点上有非0值的时候才被视为真的子节点,然后再更新当前位置处理子节点的子节点。【在字符串结尾进行标记可区分abc与abcdf(如图所示)】
Tire树的插入与查询操作
全局变量声明
char str[N];
int son[N][26],counter[N],idx;//下标是0 的点,既是根节点,又是空节点
//son数组的本质是son[父节点]下有26个可能的分支,没有字母的话就一直是0不会被视为一个分支,
//如果有这个字母的话便创建这个节点给他赋值为非0
void insert(char *str)
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;//若无节点则创建节点
p=son[p][u];//让这个子节点成为下一个节点的父节点
}
counter[p]++;
}
int query(char *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 counter[p];
}
并查集
基本思路:
- 用树来维护集合,树根的编号是整个集合的编号
- 树每一个节点的父节点都是唯一的
- 用一个一维数组存放当前节点的父节点是谁
问题:
- 如何判断树根:
if(p[x] == x)
{除了树根以外的每一个点的父节点不会是本身} - 如何求x的集合编号?
while(p[x] != x) x=p[x]
- 如何合并两个集合?两个集合(两棵树)的两个根都是指向自己的,合并集合只需要把其中一个根指向另外一个根即可
p[x]=y
优化:
- 路径压缩:每当一个点“费尽艰辛”找到了他的根节点的时候,将它与他的根节点直接相连,这样可以在很大程度上避免重复查找。
- 路径压缩注:由于此函数是通过递归实现的,所以路径压缩过程完成后,那个点到根节点上的所有点的父节点都是根节点。
实现代码
其中p[x]中存放的是节点x的父节点
int find(int x) //返回x的祖宗节点 + 路径压缩
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
a b集合合并操作:
p[find(a)] = find(b);
查询ab集合是否在同一集合中:
if(find(a) == find(b))
哈希表
由于总体值域很大,而映射的目标的范围小,所以一定会产生冲突
冲突即是有不同的数映射到了同一个元素上,这样的话就需要通过方式以解决冲突问题
第一种方法:拉链法
开一个哈希数组存储所有哈希值。当把某一个数映射到哈希表上的某一个值时,在相应的数组下拉一条链用来储存当前槽上已有的数(未哈希之前的原数)
每个拉链的表现形式是一个单链表
----这个链表一般常用操作是插入和查询。如果需要删除操作,没用必要进行真正意义上的删除,即:
只需要为每个元素设立一个flag变量,删除操作时改变flag的值,也就是让链表忘记他,把他排除在外
注:哈希算法是一种期望算法,一般情况下每一条链的长度可看成常数。所以一般情况下哈希表的时间复杂度都很好,为O(1)。
小技巧:在取区间的长度时,区间的长度最好是一个质数,这样的话冲突的平均概率是最小的。这个结论可以通过数学证明出来(自己可以去要查一下)。方法二:开放寻址法
只开一个数组,但是这个数组的长度要开到题目所给数据范围的两倍到三倍 存放所有的数
处理冲突的方法是通过一个find函数实现的,具体为:
find函数:返回值有两种情况:
---- 第一种是在插入过程中,返回的int值代表find函数已经找到了一个可以放数x的空位,通知主函数把数放到返回值对应的位置上去,跳出while循环的判断条件是当前位置为空值
---- 第二种是在查询过程中,返回的int值代表已经在k位置上找到了x,如果没有找到的话此返回位置对应的地方是null,依次判断yes or no,跳出while循环的判断条件是当前位置k上的值==x;如果没有找到x,则会因为该位置上是null而退出
拉链法(核心是单链表及其主要操作)
void insert(int x)
{
int k=(x%N+N)%N; //一种哈希方式 由于x%N可能是负数,所以加上一个N(x%N+N)确保整体是正数
e[idx]=x;
ne[idx]=h[k];
h[k]=idx++;
}
bool find(int x)
{
int k=(x%N+N)%N;
for(int i=h[k];i!=-1;i=ne[i])
if(e[i]==x)
return true;
return false;
}
开放寻址法(null需要提前定义为无穷大并将数组的每一位初始化为无穷大)
int find(int x)
{
int k=(x%N+N)%N;
while(h[k]!=null&&h[k]!=x) //当前位置为空值或者h[k]==x时会跳出循环。这两种跳出循环的情况分别是插入和查询
{
k++;
if(k==N) k=0; //如果while循环到了末尾还是没找到能插的位置就从数组头开始找
}
return k;
}
字符串哈希
核心思想是预处理前缀哈希值
学习笔记:
对应代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100002,P=131;
typedef unsigned long long ULL;
int n,m;
char str[N];
ULL h[N],p[N];//h[i]数组存放的是前i个字符的hash值(预处理前缀和) || 初始化h[0]=0,因为根据定义
//p[i]对应的第i个元素是p的多少次方,这个次方数即是数组值
ULL get(int l,int r) //此函数用来计算[l,r]字符串的哈希值
{
return h[r]-h[l-1]*p[r-l+1]; //[l,r]字符串哈希的值为:h[r]-h[l]*(p^(k-l+1))
}
int main()
{
scanf("%d%d%s",&n,&m,str+1); //str[0]被略过
//预处理p数组过程
p[0]=1;
for(int i=1 ;i<=n;i++)
{
p[i]=p[i-1]*P;
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(get(l1,r1)==get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}