并查集
(1) 朴素并查集:
int p[N]; // 存储每个节点的祖宗节点,初始时每个节点的祖宗是它自己
// 查找操作,带路径压缩
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); // 如果x不是自己所在集合的祖宗,则递归地找到祖宗
return p[x]; // 返回x的祖宗节点
}
// 初始化操作
for (int i = 1; i <= n; i++) p[i] = i; // 每个节点的初始祖宗节点是它自己
// 合并操作,将a和b所在的集合合并
p[find(a)] = find(b); // 找到a和b的祖宗节点,并将它们合并
(2) 维护size的并查集:
int p[N], size[N]; // p[]存储每个节点的祖宗节点,size[]存储每个祖宗节点所在集合的大小
// 查找操作,带路径压缩
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); // 如果x不是祖宗节点,递归地找到祖宗
return p[x]; // 返回x的祖宗节点
}
// 初始化操作
for (int i = 1; i <= n; i++) {
p[i] = i; // 每个节点的初始祖宗节点是它自己
size[i] = 1; // 每个节点所在集合的初始大小为1
}
// 合并操作,将a和b所在的集合合并,并更新大小
size[find(b)] += size[find(a)]; // 将a和b所在集合的大小相加,更新合并后集合的大小
p[find(a)] = find(b); // 将a的祖宗节点指向b的祖宗节点,完成两个集合的合并
(3) 维护到祖宗节点距离的并查集:
int p[N], d[N]; // p[]存储每个节点的祖宗节点,d[]存储每个节点到其祖宗节点的距离
// 查找操作,带路径压缩和距离更新
int find(int x) {
if (p[x] != x) {
int u = find(p[x]); // 递归地找到p[x]的祖宗节点
d[x] += d[p[x]]; // 更新x到其祖宗节点的距离,等于x到p[x]的距离加上p[x]到其祖宗节点的距离
p[x] = u; // 更新x的祖宗节点
}
return p[x]; // 返回x的祖宗节点
}
// 初始化操作
for (int i = 1; i <= n; i++) {
p[i] = i; // 每个节点的初始祖宗节点是它自己
d[i] = 0; // 每个节点到其祖宗节点的初始距离是0
}
// 合并操作,将a和b所在的集合合并,并更新距离
p[find(a)] = find(b); // 找到a和b的祖宗节点,并将它们合并
// 根据具体问题,初始化find(a)的偏移量
// d[find(a)] = distance; // 这行代码需要根据实际情况添加到合并操作中,用于设置合并后a所在集合的偏移量
堆
// 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]]); // 交换第a个和第b个插入元素在ph数组中存储的位置
swap(hp[a], hp[b]); // 交换hp[a]和hp[b]的值,更新映射关系
swap(h[a], h[b]); // 交换堆中a和b位置的元素
}
// 堆下沉操作,用于维护最小堆性质
void down(int u)
{
int t = u; // t暂存u节点的值
// 如果左儿子存在,并且左儿子的值小于t的值,则更新t为左儿子的索引
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
// 如果右儿子存在,并且右儿子的值小于t的值,则更新t为右儿子的索引
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
// 如果t已经更新,说明u节点的值不是其子树的最小值,需要交换并继续下沉
if (u != t)
{
heap_swap(u, t); // 交换u和t在堆中的位置及其映射关系
down(t); // 递归地对t进行下沉操作
}
}
// 堆上浮操作,用于维护最小堆性质
void up(int u)
{
// 只要父节点存在,并且u节点的值小于父节点的值,就继续上浮
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2); // 交换u和其父节点在堆中的位置及其映射关系
u >>= 1; // 更新u为父节点的索引,右移一位相当于除以2
}
}
// O(n)时间复杂度建堆,从最后一个非叶子节点开始,向上进行下沉操作
for (int i = n / 2; i; i--) down(i);
一般哈希
(1) 拉链法
int h[N], e[N], ne[N], idx;
// h[N] 存储哈希表的散列数组,每个元素指向一个链表的头结点
// e[N] 存储每个链表中存储的元素值
// ne[N] 存储每个链表中元素的next指针,指向同一链表的下一个节点
// idx 记录当前插入的元素索引
// 向哈希表中插入一个数x
void insert(int x) {
int k = (x % N + N) % N; // 计算哈希值,并通过模N保证索引在数组范围内
e[idx] = x; // 将元素值x存储在e数组的idx位置
ne[idx] = h[k]; // 将当前散列表k索引位置的头结点指针赋给idx位置的ne,形成链表
h[k] = idx++; // 更新散列表k索引位置的头结点指针为idx,并将idx递增
}
// 在哈希表中查询某个数是否存在
bool find(int x) {
int k = (x % N + N) % N; // 计算哈希值
for (int i = h[k]; i != -1; i = ne[i]) // 遍历散列表k索引位置的链表
if (e[i] == x) // 如果找到元素x
return true; // 返回true
return false; // 如果链表中没有找到x,返回false
}
(2) 开放寻址法
int h[N];
// h[N] 存储哈希表的散列数组
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x) {
int t = (x % N + N) % N; // 计算哈希值
while (h[t] != -1 && h[t] != x) // 循环直到找到一个空位或者找到x
{
t++; // 线性探测,移动到下一个位置
if (t == N) t = 0; // 如果到达数组末尾,循环回到开头
}
return t; // 返回x的下标或者x应该插入的位置
}
字符串哈希
typedef unsigned long long ULL;
// 定义一个类型别名ULL,代表无符号长长整型,用于存储大数
ULL h[N], p[N]; // h数组存储字符串前k个字符的哈希值
// p数组存储P的k次幂模2^64的结果
// 注意:这里的P应该是一个常量,代表进制基数,经验值131或13331
// 初始化操作
p[0] = 1; // p[0]初始化为1,因为任何数的0次幂都是1
for (int i = 1; i <= n; i++)
{
// 计算字符串前i个字符的哈希值
h[i] = h[i - 1] * P + str[i]; // 哈希值h[i]是前一个哈希值h[i-1]乘以P再加上当前字符的值str[i]
// 计算P的i次幂模2^64的结果
p[i] = p[i - 1] * P; // p[i]是p[i-1]乘以P的结果
}
// 计算子串str[l ~ r]的哈希值
ULL get(int l, int r)
{
// 利用之前计算的哈希值h[r]和h[l-1],以及p数组来避免大数乘法
// h[r] - h[l - 1] * p[r - l + 1] 计算子串的哈希值
// h[l-1] * p[r - l + 1]是将h[l-1]乘以P的(r-l+1)次幂,即乘以子串长度+1次P
// 这样,h[r] - 这个结果就等于子串str[l ~ r]的哈希值
return h[r] - h[l - 1] * p[r - l + 1];
}