树
二叉树
三种遍历
前中后序遍历的区别其实就是根节点的位置不同:
前序遍历是 根–左子–右子
中序遍历是 左子–根–右子
后序遍历是 左子–右子–根
前序遍历(Preorder Traversal)
- 顺序:根节点 → 左子树 → 右子树
- 特点:先访问根节点,再递归遍历左右子树。
- 示例:
1 | 1 |
前序遍历结果:1, 2, 4, 5, 3
中序遍历(Inorder Traversal)
- 顺序:左子树 → 根节点 → 右子树
- 特点:先遍历左子树,再访问根节点,最后遍历右子树。
- 示例:
同一棵树的遍历结果:4, 2, 5, 1, 3
后序遍历(Postorder Traversal)
- 顺序:左子树 → 右子树 → 根节点
- 特点:先递归遍历左右子树,最后访问根节点。
- 示例:
同一棵树的遍历结果:4, 5, 2, 3, 1
完全二叉树
完全二叉树的性质:
对于任意一个节点,其父亲节点是,左子节点是,右子节点是。
树状数组
可以高效率地查询和维护前缀和(或区间和);当序列是动态变化的时,如果改变其中一个元素的值,那么后面的前缀和都要重新计算,查询一次的复杂度就会变成。但是树状数组可以用简短的代码大大提高效率,查询和维护的复杂度都为。
lowbit(x)
找x的二进制数的最后一个1对应的十进制是多少,找到的数一定是2的次幂。
lowbit(x) = x & -x
例如,lowbit(5) = 5 & -5 = 1
,
lowbit(6) = 6 & -6 = 2
,lowbit(8) = 8 & -8 = 8
编码
1 |
|
线段树
线段树的核心思想是分治,大区间的解可以由小区间的解合并而来,总复杂度为。有两个基本应用场景:
- 区间最值问题。长为的序列,需要以下操作:
- 求区间内的最值
- 修改为
- 区间和问题。长为的序列,先更改某些数的值,再求区间的和。
线段树建立在二叉树上,每次分治左右子树各一半,能利用二叉树的许多性质。
PS:对于任意一个节点其父亲节点是,其左孩子是,其右孩子是。
考察每个线段,是左端,是右端:
- ,说明这个节点只有一个元素,它是叶子节点。
- ,说明这个节点代表的不止一个点,那么它有两个孩子,左孩子区间为,右孩子区间为,其中。
线段树的构造:
1 | //定义根节点是tree[1],即编号为1的节点是根 |
区间查询:
1 | int query(int L,int R,int p,int pl,int pr){ |
Layz-Tag:
线段树的节点记录区间的值,那么可以再定义一个,用它统一记录区间的修改。每次修改的复杂度为,一共次操作,总复杂度。
区间修改update()
先找到具体区间,对该节点即以上的节点做修改,并给该节点打tag。下次经过某节点且该节点tag不为0,那么就要把tag[p]传递给左右子树,并清空tag[p],这个过程用push_down()
函数完成。
模板(洛谷 P3372)
1 |
|
以下是线段树模板类模板,目前是求区间和,以后会更新求区间最大值,最小值等等功能
1 |
|
并查集
将编号分别为的个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。在这个集合中,并查集的操作有初始化,合并,查找。并查集操作的时间复杂度是的。
合并:合并两个元素所属集合(合并对应的树)
查询:查询某个元素所属集合(查询对应的树的根节点,也就是祖先),这可以用于判断两个元素是否属于同一集合
用并查集的基础操作完成HDU 1213
题目
代码
1 |
|
路径压缩
在合并过程中,树可能会发生退化,退成链表,增加查询的时间复杂度。通过路径压缩,将所有的子节点直接连到根节点,可大幅缩短搜索路径,加快查询,使树扁平化
1 | int find_s(int x){ |
按秩合并
这是合并的优化,我们让小集合的根指向大集合的根,这样更好一些。定义size[i]
为以为根的集合的大小。
1 | vector <int> siz(N,1); |
模板
1 |
|