文章目录
1. Trie 字典树
高效地存储和查找字符串、数字等集合的数据结构。
一般在每个词的结尾打一个标记。
- 实际存储中,图中的每个字符都是被存储在边上的,节点是不存储字符信息的,节点存储的是
idx
,即节点编号。 - 查询某个字符串S是否在这个字符串集合中是否存在的话,可以从根开始遍历,当遍历到空节点或者S已经遍历结束但是trie树中对应节点不是字符串的话,说明不存在该字符串。
#include <iostream>
using namespace std;
const int N = 1e5 + 10; //注意这里的N是所有输入的字符串最大总长度,不是指单个字符串的最长长度
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
int son[N][26], cnt[N], idx;
char str[N];
void insert(char str[])
{
int p = 0;
for (int i = 0; str[i]; i++) //字符数组的结尾是 \0
{
int u = str[i] - 'a'; //将每个字符映射到0~25
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]; //字符数组的结尾是 \0
scanf("%s%s", op, str); //用scanf读入字符数组时不用加&
if (op[0] == 'I') insert(str); //注意是 op[0],op[2] = \0
else printf("%d\n", query(str));
}
return 0;
}
最大异或对
Trie还可以存储数字,将十进制数字转换为位数相同的二进制数,对于每一位找不同的数异或结果最大(0找1,1找0)。
#include <iostream>
using namespace std;
const int N = 1e5 + 10, M = 31e5 + 10; //每个数转换为二进制最多有31位
int n;
int a[N], son[M][2], idx;
void insert(int x)
{
int p = 0; //根节点
for (int i = 30; i >= 0; i--) //从最高位开始存储,统一位数
{
int u = x >> i & 1;
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
//长度都一样就不用cnt数组标记最后一位了
}
int search(int x)
{
int p = 0, res = 0;
for (int i = 30; i >= 0; i--)
{
int u = x >> i & 1;
if (son[p][!u]) //取不同的数异或结果最大
{
p = son[p][!u];
res = res * 2 + 1; //异或的结果 左移1位再+1
}
else //如果不存在
{
p = son[p][u];
res = res * 2 + 0;
}
}
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int res = 0;
for (int i = 0; i < n; i++)
{
insert(a[i]);
res = max(res, search(a[i]));
}
printf("%d", res);
return 0;
}
2. 并查集
- 将两个集合合并
- 询问两个元素是否在一个集合中
基本原理:每个集合用一棵树表示。树根的编号就是整个结合的编号。每个节点存储它的父节点,p[x]
表是x
的父节点。
- 如何判断树根:
if(p[x]==x)
。除了根节点外,p[x]
都不等于x
。 - 如何求
x
的集合编号:while (p[x] != x) x = p[x]
,只要p[x]
不等于x
,就一直往上走。 - 如何合并两个集合:把一棵树插到另一棵树的根节点上。px是x的集合编号,py是y的集合编号,
p[x]=y
。
优化:路径压缩 find(int x)
通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是,当从一个节点一直往上找找到根节点时,即找到了一条路,将x到根节点路径上的所有点的pre(上级)都设为根节点。
例题
合并集合
这里用字符数组(字符串)op[2]
不用字符op
,是因为scanf
读入字符%c
时会读入空格或回车等一些字符,太麻烦了。但是scanf
读入字符串时会自动忽略空格和回车,所以用scanf
读入一个字符时建议还是用字符串的形式。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
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]; //这里用字符数组op[2](字符串)不用字符op是因为scanf读入%c时会读入空格或回车等一些字符,太麻烦了。但是scanf读入字符串时会自动忽略空格和回车,所以用scanf读入一个字符时建议还是用字符串的形式。
int a, b;
scanf("%s%d%d", op, &a, &b);
if (op[0] == 'M') p[find(a)] = find(b); //将a所在集合的根的父亲设置为b所在集合的根
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
连通块中点的数量
注意顺序
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int p[N], si[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;
si[i] = 1;
}
while (m--)
{
char op[3];
int a, b;
scanf("%s", op);
if (op[0] == 'C')
{
scanf("%d%d", &a, &b);
if (a == b || find(a) == find(b)) continue;
//注意下面两句顺序不能换,必须要先加连通块大小再操作集合,
//否则操作完集合后,a和b的根结点将会重叠,导致连通块大小计算错误
si[find(b)] += si[find(a)];
p[find(a)] = find(b);
}
else if (op[1] == '1')
{
scanf("%d%d", &a, &b);
if (a == b || find(a) == find(b)) printf("%s\n", "Yes");
else printf("%s\n", "No");
}
else
{
scanf("%d", &a);
printf("%d\n", si[find(a)]);
}
}
return 0;
}
食物链
用并查集维护额外信息d[x]
:每个节点到根节点的距离。
依题意知,总共ABC三类动物,A吃B,B吃C,C吃A成一个循环关系,由此来定义节点到根节点的距离:将每个节点到根节点的距离模3,余1:可以吃跟根节点,余2:可以被根节点吃,余3:与根节点同类。
根节点到自己的距离为0,所以第1层吃第0层,第2层吃第1层,第3层(是第0层的同类)吃第2层,第4层(是第1层的同类)吃第3层…以此类推。
注意:
- find函数和数组d的含义:
- 判断的时候不能用
d[px]%3 != d[py]%3
,因为在更新d[px]
时有可能会出现负数的情况,-1与2是同一类,但是如果这样写就判断错误;如果用减法再取模就正确(if
条件中,负数不为0也成立)。
#include <iostream>
using namespace std;
const int N = 50010;
int p[N], d[N]; //parent数组和辅助数组(维护i节点到根节点的距离,因为初始时每个节点都是根节点,所以初始距离都为0)
int n, k;
int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]); //t暂存p[x]的根节点
d[x] += d[p[x]]; //d[x]更新为x到根节点的距离
p[x] = t; //更新父节点为根节点
}
return p[x];
}
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++) p[i] = i;
int res = 0;
while (k--)
{
int r, x, y; //r是说法的种类D,不用d为了不和d数组命名重复
cin >> r >> x >> y;
if (x > n || y > n) res++;
else
{
int px = find(x), py = find(y); //px和py都是根节点了
if (r == 1)
{
//如果在同一棵树上,且到根节点的距离不相等,说明不是同一种类
if (px == py && (d[x] - d[y]) % 3) res++;
else if (px != py)
{
p[px] = py; //让x的根节点的父节点指向y的根节点
//更新距离,定义px到py之间的距离:因为x和y是同类,
//所以x合并到y的集合中后,(d[x]+?)%3==d[y]%3 => d[y]-d[x]=?,
//其中?为x的祖宗节点px到y的祖宗节点的距离
d[px] = d[y] - d[x];
}
}
else if (r == 2)
{
//x到根节点的距离比y到根节点的距离多1 =>(d[x]-d[y]-1)%3==0
if (px == py && (d[x] - d[y] - 1) % 3) res++;
else if (px != py)
{
p[px] = py;
//因为x吃y,所以d[x]+?-d[y]-1==0 => ?=d[y]+1-d[x],其中?为d[px]
d[px] = d[y] + 1 - d[x];
}
}
}
}
cout << res << endl;
return 0;
}
3. 堆
堆是一棵完全二叉树
大根堆:
每个节点的值都大于或等于其左右孩子节点的值。
小根堆:
每个结点的值都小于或等于其左右孩子结点的值。
存储:下标从1开始
用一个一维数组,下标为1的点是根节点,x的左儿子:2x,x的右儿子:2x+1。
down() 和 up() 操作
如何手写一个堆(以小根堆为例)
- 插入一个数
heap[++size] = x; up(size);
- 求集合中的最小值
heap[1];
- 删除最小值:用整个堆的最后一个元素覆盖掉堆顶元素(因为一维数组删除头结点很困难,删除尾结点很方便)
heap[1] = heap[size]; size--; down(1);
- 删除任意一个元素
heap[k] = heap[size]; size--; down(k); up(k);
虽然写了down和up,只会执行一个,大了就down,小了就up。 - 修改任意一个元素
heap[k] = x; down(k); up(k);
例题
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], siz;
void down(int u)
{
int t = u;
// 如果u的左子节点存在且小于u,替换
if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= siz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t) //不相等说明根节点不是最小的
{
swap(h[u], h[t]); //交换
down(t); //递归处理
}
}
void up(int u) //up操作的话只用跟父节点比,往上走
{
while (u / 2 && h[u / 2] > h[u]) //父节点存在且父节点大于它
{
swap(h[u / 2], h[u]);
u /= 2;
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
siz = n;
// 从 n/2 开始down
// 因为n是最大值,n/2是n的父节点,所以n/2是最大的有子节点的父节点,
// 所以从n/2往前遍历,就可以把整个数组遍历一遍
for (int i = n / 2; i; i--) down(i);
while (m--)
{
printf("%d ", h[1]); //h[1]为最小值
h[1] = h[siz]; //删除最小值,用整个堆的最后一个元素覆盖掉堆顶元素
siz--;
down(1);
}
return 0;
}
堆模拟
复杂情况:因为第4、5个操作涉及到第k个插入的数,需要引入两个数组ph[]
和hp[]
。
ph[k]=i
表示第k
个插入的点在堆(一维数组)里的下标为i
,(pointer->heap),
hp[i]=k
表示在堆里下标为i
的数是第k
个插入的,(heap->pointer)。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
//h代表heap(堆),ph(point->heap)可以获得第几个插入的元素现在在堆的那个位置
//hp(heap->point)可以获得在堆的第n个元素存的是第几个插入的元素
//siz是大小
int h[N], ph[N], hp[N], siz;
int n, idx = 0; //idx-每个元素的插入次序
// 堆的全新的交换方式
void heap_swap(int a, int b)
{
//先由hp找到对应的插入次序,然后交换ph数组中记录的两个元素的下标
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]); //交换hp数组中记录的两个元素的插入次序
swap(h[a], h[b]); // 最后交换堆中的两个元素
}
void down(int u)
{
int t = u; //让t代指u以及其两个儿子(三个点)中的最大值,先初始化为u
if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= siz && 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, u / 2);
u /= 2;
}
}
int main()
{
scanf("%d", &n);
while (n--)
{
string op;
int k, x;
cin >> op;
//插入一个数 x
if (op == "I")
{
scanf("%d", &x);
siz++;
idx++;
ph[idx] = siz; //堆尾插入,故第idx次插入的元素下标为siz
hp[siz] = idx; //当前下标为siz的元素为第idx次插入
h[siz] = x; //当前插入的值,即h[ph[idx]=x
up(siz); //从堆尾向上调整
}
//输出当前集合中的最小值
else if (op == "PM") printf("%d\n", h[1]);
//删除当前集合中的最小值
else if (op == "DM")
{
heap_swap(1, siz); //用堆尾元素覆盖头元素
siz--;
down(1);
}
//删除第 k 个插入的数
else if (op == "D")
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, siz);
siz--;
down(k), up(k); //只会执行一个
}
//修改第 k 个插入的数,将其变为 x
else if (op == "C")
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k), up(k);
}
}
return 0;
}