2.2数据结构 | Trie、并查集、堆
这是我的一个算法网课学习记录,道阻且长,好好努力
2.2.1 Trie
Trie字典树又叫前缀树(prefix tree),是一种多叉树结构,用以较快速地进行单词或前缀查询。
Trie树的基本性质:
- 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符互不相同。
通常在实现的时候,会在节点结构中设置一个标志,用来标记该结点处是否构成一个单词(关键字)。
Trie树的核心思想是空间换时间,利用字符串的公共前缀来减少无谓的字符串比较以达到提高查询效率的目的。
插入和查询的时间复杂度都为O(m)
,m为待插入/查询的字符串的长度。
例题:AcWing 835. Trie字符串统计
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。
共有 N 个操作,输入的字符串总长度不超过 10的5次方,字符串仅包含小写英文字母。
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;
// idx 当前操作用到的节点
// son 表示编号为N的节点的子节点们的编号
// cnt 表示编号为N的节点结尾的字符串在集合中出现的次数
char str[N];
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];
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
char op[2];
scanf("%s%s", op, str);
if (op[0] == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
类似于之前的链表,进入的数据都有idx下标,同时存入带有唯一编号的节点中,形成快速存储和查找字符串的数据结构。对于传进来的字符串从根节点开始遍历,遇到不存在的点就新开辟一个。对于传入的字符串,它会遍历特定的路径,对应终点cnt[p]++
,即该字符串储存数量加1。
2.2.2 并查集
定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
功能:
1.将两个集合合并。
2.询问两个元素是否在一个集合当中。
核心:find函数(递归,返回祖宗节点)
基本原理:
每个集合用一棵树表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
优化:
路径压缩,在查找到根节点之后就把p[x]储存成根节点;按秩排序(这里没有实现)
例题1:AcWing 836. 合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合当中。
现在要进行m个操作,操作分为两种:
M a b
,将编号为a和b的两个数所在的区间合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为a和b的两个数是否在同一个集合中;
#include <iostream>
using namespace std;
const int N = 10010;
int n, m;
int p[N];
int find(int x) // 返回x的祖宗节点 + 路径优化
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
while (m -- )
{
char op[2]; // 过滤空格和回车等字符
int a, b;
scanf("%s%d%d", op, &a, &b);
if (op[0] == 'M') p[find(a)] = find(b);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
例题2:AcWing 837. 连通块中点的数量
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。
现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
输出格式
对于每个询问指令”Q1 a b”,如果a和b在同一个连通块中,则输出“Yes”,否则输出“No”。
对于每个询问指令“Q2 a”,输出一个整数表示点a所在连通块中点的数量
每个结果占一行。
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N], size[N];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1; // 最开始的时候每个size集合里面只有一个点
}
while (m -- )
{
char op[5];
int a, b;
scanf("%s", op);
if (op[0] == 'C')
{
scanf("%d%d", &a, &b);
if (find(a) == find(b)) continue; // 如果已经在一棵树当中了,后面的操作就不用进行了
size[find(b)] += size[find(a)]; // 如何维护size集合-->只保证根节点的size函数是有意义的,进行根节点的size加和
p[find(a)] = find(b);
}
else if (op[1] == '1')
{
scanf("%d%d", &a, &b);
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else
{
scanf("%d", &a);
printf("%d\n", size[find(a)]); // 返回根节点的size
}
}
return 0;
}
2.2.3 堆
定义:
必须是完全二叉树,用数组实现(下面是一维数组),任一结点的值是其子树所有结点的最大值或最小值(衍生出大根堆和小根堆)。
(像一颗倒立的大树。)
初始化堆的时间复杂度是O(n)
的,之后操作是O(log n)
例题1:AcWing 838. 堆排序
输入一个长度为n的整数数列,从小到大输出前m小的数。
输入格式:
第一行包含整数n和m。
第二行包含n个整数,表示整数数列。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], size;
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)
{
swap(h[u], h[t]); // 交换
down(t); // 递归
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
size = n;
for (int i = n / 2; i; i -- ) down(i);
while (m -- )
{
printf("%d ", h[1]);
h[1] = h[size];
size -- ;
down(1);
}
return 0;
}
例题2:AcWing 839. 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
-
I x
,插入一个数x; -
PM
,输出当前集合中的最小值; -
DM
,删除当前集合中的最小值(当最小值不唯一时,删除最早插入的最小值); -
D k
,删除第k个插入的数; -
C k x
,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N], ph[N], hp[N], size;
// ph[k]是第k个插入数在堆中的下标
// hp[k]的含义是堆中第k个点是第几个插入的点
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 / 2] > h[u])
{
heap_swap(u / 2, u);
u /= 2;
}
}
int main()
{
int n, m = 0;
scanf("%d", &n);
while (n -- )
{
char op[10];
int k, x;
scanf("%s", op);
if (!strcmp(op, "I"))
{
scanf("%d", &x);
size ++ ;
m ++ ;
ph[m] = size, hp[size] = m;
h[size] = x;
up(size);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, size);
size -- ;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, size);
size -- ;
down(k), up(k);
}
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k), up(k);
}
}
return 0;
}
单纯swap交换数字,不会改变位置指针,所以需要ph数组来用作位置指针。题目中是删除第k个插入的元素,所以我们需要另外开两个不同的数组,建立hp[k]与ph[k]数组(这两是是反函数的关系)。