字典树(Trie树)
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
它有3个基本性质:
根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。
高效地存储和查找字符串集合的数据结构,降低存储和查询字符串的复杂度
int son[N][26], cnt[N], idx;
//0号点既是根结点,又是空结点
//son[][]存储树中每个结点的子节点
//cnt[]存储以每个结点结尾的单词数量
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];
}
cnt[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 cnt[p];//字符串出现次数,可能为0
}
练习
并查集
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
并查集支持的两个操作:
1.将两个集合合并
2.询问两个元素是否在一个集合当中(近乎O(1))
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。对于每个结点都存储它的父节点,p[x]代表x的父节点。
判断树根:if(p[x]==x)
求x的集合编号(根节点):while(p[x]!=x)x=p[x];
合并两个集合:p[x]是x的集合编号,p[y]是y的集合编号。p[x]=y;
(1)朴素并查集:
int p[N];//存储每个点的父结点(根节点值即代表集合编号)
int find(int x)//返回x的根结点
{
if (p[x] != x)p[x] = find(p[x]);//路径压缩优化
return p[x];//如果没有到达根节点,则一直向上查找,否则返回根节点
}
//初始化,假定结点编号是1~n
for (int i = 1; i <= n; i++)p[i] = i;//初始根节点是自身
//合并a和b所在的两个集合:
p[find(a)] = find(b);
(2)维护size的并查集:
int p[N], size[N];//size[]只有根节点的有意义
int find(int x)
{
if (p[x] != x)p[x] = find(p[x]);
return p[x];
}
for (int i = 1; i <= n; i++)//初始化
{
p[i] = i;
size[i] = 1;
}
size[find(b)] += size[find(a)];//合并集合大小
p[find(a)] = find(b);
(3)维护到根节点距离的并查集:
int p[N], d[N];//d[x]存储x到p[x]的距离
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
for (int i = 1; i <= n; i++)
{
p[i] = i;
d[i] = 0;
}
p[find(a)] = find(b);
d[find(a)] = distance;//根据具体问题,初始化find(a)的偏移量
练习
相关资料
堆(优先队列)
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆支持的操作:
1.插入一个数 heap[++size]=x;up(size);
2.求集合当中的最小值 heap[1];//1+2操作复杂度为O(n^2),利用堆优化为O(logn)
3.删除最小值 heap[1]=heap[size--],down(1);//可用来排序,最差时间复杂度优于快排
4.删除任意一个元素 heap[k]=heap[size--];up(k),down(k);//up(),down()只会执行一个
5.修改任意一个元素 heap[k]=k;up(k),down(k);
关于建堆:如果所有子树都为最小堆,整棵数为最小堆(递归定义),所以从最后一个结点开始,依次判断以此结点为根的子树是否符合最小堆特性。因为叶节点没有儿子,所以所有以叶节点为根节点的子树都符合最小堆的特性,即父结点的值比子结点的值小,所以无需处理,直接从第n/2个结点开始处理(最后一个非叶节点是n/2)。可证时间复杂度为O(n)。
//h[N]存储堆中的值,h[1]是堆顶,x的左儿子是2x,右儿子是2x+1
//ph[k]存储第k个插入的点在堆中的位置
//hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
//交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= size&h[u * 2] < h[t])t = u * 2;//如果小于左儿子
if (u * 2 + 1 <= size&h[u * 2 + 1] < h[t])t = u * 2 + 1;//如果小于右儿子
if (u != t)//最小结点不是自己
{
heap_swap(u, t);
down(t);//下降到堆底
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])//左右儿子(2*u,2*u+1)父结点都为u/2,因为'/'向下取整
{
heap_swap(u, u / 2);
u >>= 1;
}
}
for (int i = n / 2; i; i--)down(i);//O(n)建堆
练习
哈希表(散列表)
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
将大数据结合映射为小数据集合,可以在数据范围很大的情况下用来判重
1.一般哈希
(1)拉链法
int h[N], e[N], ne[N], idx;//h[]存储哈希值,e[]只需创建一个,N一般取质数,且距离2^n尽可能远
//向哈希表中插入一个数
void insert(int x)//头插法
{
int k = (x%N + N) % N;//x可能为负,所以+N%N映射到正数域
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
//在哈希表中查询某个数是否存在
int find(int x)
{
int k = (x%N + N) % N;
for (int i = h[k]; i != -1; i = ne[k])//遍历h[k]链
if (e[i] == x)
return 1;
return 0;
}
(2)开放寻址法
int h[N];
const int null = 0x3f3f3f3f;//哈希值可能为负数,所以以足够大的数为空
//如果x在哈希表中,返回x的下标:如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x%N + N) % N;
while (h[t] != null&&h[t]!=x)//当前位置已有值且不是x
{
t++;
if (t == N)t = 0;
}
return t;
}
练习
2.字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
UUL h[N], p[N];//h[k]存储字符串前k个字母的哈希值,p[k]存储P^k mod 2^64
//初始化
p[0] = 1;
for (int i = 1; i <= n; i++)
{
h[i] = h[i - 1] * p + str[i];
p[i] = p[i - 1] * p;
}
//计算子串str[l~r]的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
模板参考acwingP1551 亲戚