-
字典树(Tire树)
- 用来高效存储和查找字符串集合的数据结构
-
基本性质:
- 根节点不包含字符
- 除根节点外每一个节点都只包含一个字符:
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串,每个节点的所有子节点包含的字符都不相同
- 有的节点有标记,表示以改节点为尾存在一个单词
-
代码:
int son[N][26], cnt[N], idx;//son[p][u] = x p表示父节点的编号,u表示p节点的儿子的名字,x表示儿子的编号 //cnt[p]表示以p节点为结尾的单词的个数 //编号0既代表根节点又代表空节点,所有没有儿子的节点都指向空节点 void insert(char str[]){ int p = 0;//编号指针 for(int i = 0; str[i]; ++ i){ //遍历字符串的每一个字符 int u = str[i] - 'a'; //求出每个字符的名字(a~z名字对应0~25) if(!son[p][u]) son[p][u] = ++ idx; //p节点没有u儿子,就添加u儿子并且给儿子分配编号为++idx p = son[p][u]; //指向儿子节点 } cnt[p] ++ ; //循环完毕以p节点为结尾的单词个数加一 } //查询某个字符串的次数 int query(char str[]){ int p = 0; for(int i = 1; str[i]; ++ i){ int u = str[i] - 'a'; if(!son[p][u]) return 0; p = son[p][u]; } return cnt[p]; //虽然有些节点的名字一样但是编号各不相同,所以可以保证cnt[p]就是所要查询的字符串的个数 }
-
并查集:
-
作用:
- 将两个集合合并O(1)
- 询问两个元素是否在一个集合当中O(1)
- 求集合元素的数量
-
原理:
- 每个集合用树来存储,树根的编号代表整个集合的编号,每个节点存储他的父节点
- 如何判断树根:树根的父节点就是自己即fa[x] = x,x就是树根
- 如何求x在哪个集合:当x的父节点不为根节点时,令x = fa[x],直到x为根节点时,x就是所在集合的编号
- 如何合并两个集合:直接让根节点的父节点指向另一个集合的根节点
- 如何统计一个集合的元素个数:添加一个size数组,初始时每个元素自成一个集合,size初始化为1,根节点存储所在集合的元素个数,每合并两个集合a,b,让size(find(x)) += size(find(b))
-
代码:
int size[N], f[N]; //size统计根节点所在集合的元素个数,f存储每个节点的祖先节点 //初始化,每个节点的父节点为自己 void init(){ for(int i = 1; i <= n; ++ i) f[i] = i; } //查找祖宗节点 + 路径压缩 //路径压缩:在找祖宗节点的过程中将路径上节点的父节点都修改为祖宗节点 int find(int x){ return x == f[x] ? x : (f[x] = find(f[x])); } //合并x,y所在的集合 void merge(int x, int y){ f[find(x)] = f[find(y)]; } //判断x,y是否在同一个集合中 if(f[find(x)] == f[find(y)]) 在 else not 在 //需要统计集合元素个数时 //初始化 void init(){ for(int i = 1; i <= n; ++ i){ f[i] = i; size[i] = 1; } //合并集合 void merge(int x, int y){ f[find(x)] = f[find(y)]; size[find(x)] += size[find(y)]; }
-
-
堆:
- 堆是一棵完全二叉树
- 上层节点全满,最后一层节点从左到右排
- 用一维数组模拟堆 x的左儿子下标为2x,右儿子下标为2x+1
-
主要操作:
- 插入一个数
- 求集合中的最小值
- 删除最小值
- 删除任意一个元素
- 修改任意一个元素
-
小根堆:
-
根节点的值最小,子树的根节点的值是子树中最小值
-
基本操作:
- 插入一个数: h e a p [ + + s i z e ] = x ; u p ( s i z e ) heap[ ++ size] = x; up(size) heap[++size]=x;up(size)
- 求最小值: h e a p [ 1 ] heap[1] heap[1]
- 删除最小值: h e a p [ 1 ] = h e a p [ s i z e ] ; s i z e − − ; d o w n ( 1 ) heap[1] = heap[size]; size -- ; down(1) heap[1]=heap[size];size−−;down(1)
- 删除下标为k的节点: h e a p [ k ] = h e a p [ s i z e ] ; s i z e − − ; u p ( k ) ; d o w n ( k ) heap[k]=heap[size];size--;up(k);down(k) heap[k]=heap[size];size−−;up(k);down(k)
- 修改下标为的k节点的值: h e a p [ k ] = x ; u p ( k ) ; d o w n ( k ) heap[k] = x;up(k);down(k) heap[k]=x;up(k);down(k)
- up函数将大数往上移动,down函数将小数往下移动,size表示最后一个节点的下标,heap数组存储每个节点的值,节点下标从1开始
- down函数实现:每次比较根节点与左右儿子节点的大小,更新根节点的值为这三个节点中的最小值
-
进阶操作:
- 删除第k个插入的节点:根据ph数组求出节点在堆中的下标,根据下标删除节点
- 修改第k次插入的节点的值:根据下标修改值
- 第k个插入的节点不一定是下标为k的节点,因为每次进行修改或者删除操作只交换了值,而没有改变下标,而第k个插入的节点的值是确定的,而在堆中下标为k的节点的值已经被改变了,所以要另开hp数组专门存储第k个插入的节点在堆中的下标,hp数组在修改删除元素时动态维护。由于每次删除或修改是根据节点在堆中的下标进行的,为了动态维护hp数组,此时需要知晓节点是第几次插入的,因此还要另开ph数组来根据节点在堆中的下标求出其第几次插入
- 主要体现在交换节点值的操作上:每次交换节点的值相当于节点在堆中的下标进行了交换,首先通过hp数组求出节点的插入次序,同时根据插入次序交换节点的ph数组的值,以保证第k个插入的节点的值不变
-
代码:
//一般操作版 int h[N], idx;//h数组存储每个节点的值,idx表示最后一个节点的下标,所有节点下标从1开始 //down函数 void down(int u){ int t = u, l = 2 * u, r = 2 * u + 1;//t用来指向三个节点中的最小值节点的下标,l,r表示u的左右儿子的下标 if(l <= idx && h[l] < h[u]) t = l;//左儿子存在且值小于u,t更新为左儿子 if(r <= idx && h[r] < h[u]) t = r;//右儿子存在且值小于u,t更新为右儿子 if(t != u) swap(h[u], h[t]), down(t);//发生了更新说明需要u下移,递归处理 } //up函数 void up(int u){ //不论u是左右儿子,其父节点下标均为u/2 while(u / 2 && h[u / 2] > h[u]){//父节点存在且值比u小,就交换值 swap(h[u], h[u / 2]) u /= 2; //更新下标 } } //插入值x void add(int x){ h[ ++ idx] = x;//赋值 up(idx); //上升 } //删除下标为k的节点 void del(int u){ h[u] = h[idx]; idx -- ; up(u);down(u);//要么往上要么往下 } //将下标为u的节点的值改为x void change(int k, int x){ h[u] = x; up(u);down(u); } //初始化 void init(){ //输入 int x; for(int i = 1; i <= n; ++ i){ cin >> x; h[ ++ idx] = x; } //建堆 //建堆O(n),i不必从n开始因为有n/2的元素本来就在最底层 for(int i = idx / 2; i; -- i) down(i); } //高级操作版 //支持修改第k个插入的节点 //h数组存储每个节点的值 //hp[i] = k;表示下标为i的点是第k个插入的节点 //ph[k] = i;表示第k个插入的节点的下标为i //idx表示最后一个节点的下标,所有节点下标从1开始 //k记录第几次插入 int h[N], hp[N], ph[N], idx, k; void swap_head(int u, int v){ swap(h[u], h[v]);//交换值 swap(ph[u], ph[v]);//交换下标 swap(hp[ph[u]], hp[ph[v]]);//交换次序 } //上面代码中的swap函数均需要替换为swap_head()函数 //down函数 void down(int u){ int t = u, l = 2 * u, r = 2 * u + 1;//t用来指向三个节点中的最小值节点的下标,l,r表示u的左右儿子的下标 if(l <= idx && h[l] < h[u]) t = l;//左儿子存在且值小于u,t更新为左儿子 if(r <= idx && h[r] < h[u]) t = r;//右儿子存在且值小于u,t更新为右儿子 if(t != u) swap_head(u, t), down(t);//发生了更新说明需要u下移,递归处理 } //up函数 void up(int u){ //不论u是左右儿子,其父节点下标均为u/2 while(u / 2 && h[u / 2] > h[u]){//父节点存在且值比u小,就交换值 swap_head(u, u / 2); u /= 2; //更新下标 } } //插入x void add(int x){ ++ idx; ++ k; h[idx] = x;//赋值 ph[k] = idx; hp[idx] = k; up(idx); } //删除下标为k的节点 void del(int u){ h[u] = h[idx]; idx -- ; up(u);down(u); } //将下标为u的节点的值改为x void change(int k, int x){ h[u] = x; up(u);down(u); } //初始化 void init(){ //赋值 int x; for(int i = 1; i <= n; ++ i){ cin >> x; ++ idx; ++ k; h[idx] = x; hp[idx] = k; ph[k] = idx; } //建堆 //建堆O(n),i不必从n开始因为有n/2的元素本来就在最底层 for(int i = idx / 2; i; -- i) down(i); }
-
-
大根堆:
- 与小根堆互逆不多赘述
字典树/并查集/堆
于 2023-07-30 18:38:28 首次发布