This commit is contained in:
Zengtudor 2024-10-24 12:09:47 +08:00
parent 575b5ff4a5
commit 3638a0c290
2 changed files with 371 additions and 55 deletions

View File

@ -779,7 +779,7 @@ public:
UnionFind(int size) {
parent.resize(size); // 动态数组存储每个节点的父节点
rank.resize(size, 1); // 动态数组存储每个节点的秩(树的高度)
// 初始化每个节点的父节点为自己
for (int i = 0; i < size; i++) {
parent[i] = i;
@ -1272,101 +1272,417 @@ int main() {
## 笛卡尔树
### [维基百科用途解释](https://zh.wikipedia.org/wiki/%E7%AC%9B%E5%8D%A1%E5%B0%94%E6%A0%91)
![1729697900947](image/3.数据结构/1729697900947.png)
### [bilibili其它up解释](https://www.bilibili.com/video/BV1Fh411E7A4)
笛卡尔树是一种基于树结构的数据结构,结合了二叉搜索树和堆的特性。它通常用于动态集合的操作,比如合并和查找。笛卡尔树的基本原理是:
1. **树的结构**:对于每个节点,满足左子树的所有节点小于该节点,右子树的所有节点大于该节点(符合二叉搜索树的性质),同时每个节点的值也是该节点的优先级(符合堆的性质)。
2. **构建方法**:通过将元素依次插入,保持上述性质。插入时,如果新节点的优先级大于当前节点,则将其插入到当前节点的右子树,否则插入到左子树。
3. **合并操作**:可以通过递归方法实现合并两个笛卡尔树,比较根节点的优先级,并将较小的树作为左子树合并。
下面是一个简单的C++实现示例:
![1729735106127](image/3.数据结构/1729735106127.png)
### 自己的代码解释
笛卡尔树Cartesian Tree是一种特殊的二叉树它满足以下两个性质
1. **堆性质**:对于每个节点,节点的值大于其子节点的值(或小于,取决于构建的方式)。
2. **二叉搜索树性质**:对于每个节点,左子树的值小于节点的值,右子树的值大于节点的值。
以下是使用 C++ 构建笛卡尔树的详细代码,带有注释:
```cpp
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
// 定义笛卡尔树节点
struct Node {
int value; // 节点的值
Node* left; // 左子树
Node* right; // 右子树
Node(int val) : value(val), left(nullptr), right(nullptr) {} // 构造函数
};
// 函数:插入节点到笛卡尔树中
Node* insert(Node* root, Node* node) {
// 如果树为空,直接返回新节点
if (!root) {
return node;
}
// 根据值的大小关系插入节点
if (node->value > root->value) {
// 插入到右子树
root->right = insert(root->right, node);
} else {
// 插入到左子树
root->left = insert(root->left, node);
}
return root;
}
// 函数:构建笛卡尔树
Node* buildCartesianTree(const vector<int>& arr) {
if (arr.empty()) return nullptr;
// 使用栈来帮助构建树
stack<Node*> nodeStack;
Node* root = nullptr;
for (int val : arr) {
Node* newNode = new Node(val); // 创建新节点
// 确保新节点满足堆性质
while (!nodeStack.empty() && nodeStack.top()->value < newNode->value) {
// 当前节点小于新节点,作为新节点的左子树
newNode->left = nodeStack.top();
nodeStack.pop();
}
// 如果栈不为空,当前节点作为新节点的右子树
if (!nodeStack.empty()) {
nodeStack.top()->right = newNode;
} else {
// 栈为空,当前节点成为根节点
root = newNode;
}
// 将新节点压入栈中
nodeStack.push(newNode);
}
return root;
}
// 函数:中序遍历打印树节点
void inorderTraversal(Node* root) {
if (!root) return;
inorderTraversal(root->left);
cout << root->value << " ";
inorderTraversal(root->right);
}
// 主函数
int main() {
vector<int> arr = {3, 1, 4, 2}; // 示例数组
Node* root = buildCartesianTree(arr);
// 打印中序遍历结果
cout << "Inorder Traversal of Cartesian Tree: ";
inorderTraversal(root);
cout << endl;
return 0;
}
```
### 代码说明:
1. **Node 结构体**:定义了树的节点,包括值、左子树和右子树指针。
2. **insert 函数**:负责将新节点插入到笛卡尔树中,确保树的性质。
3. **buildCartesianTree 函数**:遍历输入数组,使用栈构建笛卡尔树。根据堆性质和二叉搜索树性质逐步插入节点。
4. **inorderTraversal 函数**:用于打印树的中序遍历结果。
5. **main 函数**:测试构建和遍历笛卡尔树的功能。
### [模板题代码](https://www.luogu.com.cn/problem/P5854)
```cpp
#include <iostream>
#include <istream>
#include <ostream>
#include <stack>
#ifdef ONLINE_JUDGE
#define DEBUG(code)
#else
#define DEBUG(code)code
#endif
using ll = long long;
const ll max_n = 1e7+5;
struct Node{
ll left_child, right_child, val;
friend std::ostream& operator<<(std::ostream &os, const Node &n){
os<<"Node { val: "<<n.val<<", left_child: "<<n.left_child<<", right_child: "<<n.right_child<<" }";
return os;
}
}nodes[max_n];
ll n;
std::stack<ll> stk;
ll root, ans1, ans2;
int main(){
std::iostream::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
std::cin>>n;
for(ll i{1};i<=n;i++){
std::cin>>nodes[i].val;
}
for(ll i{1};i<=n;i++){
while(!stk.empty() && nodes[i].val < nodes[stk.top()].val){
nodes[i].left_child = stk.top();
stk.pop();
}
if(!stk.empty()){
nodes[stk.top()].right_child = i;
}else{
root = i;
}
stk.push(i);
}
DEBUG(
std::cout<<root<<'\n';
)
for(ll i{1};i<=n;i++){
DEBUG(
std::cout<<nodes[i]<<'\n';
)
ans1 ^= i * (nodes[i].left_child + 1);
ans2 ^= i * (nodes[i].right_child + 1);
}
std::cout<<ans1<<' '<<ans2<<'\n';
}
```
## 平衡树(AVL, treap, splay)
### 什么是平衡树
平衡树是一种自我调整的二叉搜索树,其特点是保持树的高度相对较低,从而保证高效的查找、插入和删除操作。常见的平衡树包括:
1. **AVL树**每个节点的左子树和右子树的高度差至多为1。AVL树通过旋转操作保持平衡。
2. **红黑树**是一种带颜色属性的平衡树保证任何路径上的黑色节点数量相同并且红色节点不能连续出现。红黑树的高度相对较低确保操作的时间复杂度为O(log n)。
3. **B树**:一种自平衡的多路搜索树,常用于数据库和文件系统中,适合于块存储,能够减少磁盘访问次数。
平衡树的优势在于能提供高效的动态集合操作,适用于需要频繁插入和删除的场景。你对平衡树的哪一方面感兴趣呢?
### [B站动画](https://www.bilibili.com/video/BV1tZ421q72h)
### AVL树概述
**AVL树**Adelson-Velsky and Landis Tree是一种自平衡的二叉搜索树。它通过维护每个节点的高度平衡因子来保证树的高度在 \(O(\log n)\) 范围内,从而确保高效的查找、插入和删除操作。
### 用途
- **高效查找**AVL树提供了对动态数据集的高效查找适合需要频繁查询的应用。
- **动态数据结构**:适合需要频繁插入和删除的场景,例如实时数据分析。
- **数据库系统**作为索引结构的一部分AVL树能够保持较好的平衡减少访问时间。
### 原理
1. **高度平衡因子**:对于每个节点,计算其左子树和右子树的高度差(称为平衡因子),定义为:
\[
\text{balance\_factor} = \text{height(left\_subtree)} - \text{height(right\_subtree)}
\]
平衡因子的值只能为 -1、0 或 1。
2. **旋转操作**:当插入或删除操作导致某个节点的平衡因子超出上述范围时,需要进行旋转以恢复平衡。常见的旋转操作包括:
- **右旋**:针对左重的情况。
- **左旋**:针对右重的情况。
- **左右旋**:针对左重的右子树。
- **右左旋**:针对右重的左子树。
下面是带有详细注释的 AVL树 C++ 实现代码:
```cpp
#include <iostream>
using namespace std;
// AVL树节点结构
struct Node {
int value;
int priority;
Node *left, *right;
int key; // 节点的键值
Node* left; // 左子树指针
Node* right; // 右子树指针
int height; // 节点的高度
Node(int v) : value(v), priority(rand()), left(nullptr), right(nullptr) {}
// 构造函数,初始化节点
Node(int value) : key(value), left(nullptr), right(nullptr), height(1) {}
};
Node* merge(Node* left, Node* right) {
if (!left) return right;
if (!right) return left;
// 获取节点的高度
int getHeight(Node* node) {
return node ? node->height : 0; // 如果节点为空高度为0
}
if (left->priority > right->priority) {
left->right = merge(left->right, right);
return left;
// 获取节点的平衡因子
int getBalance(Node* node) {
return node ? getHeight(node->left) - getHeight(node->right) : 0; // 左右子树高度差
}
// 右旋操作
Node* rightRotate(Node* y) {
Node* x = y->left; // 取左子树为新根
Node* T2 = x->right; // 保存x的右子树
// 进行旋转
x->right = y; // y变为x的右子树
y->left = T2; // 将T2作为y的左子树
// 更新节点高度
y->height = max(getHeight(y->left), getHeight(y->right)) + 1;
x->height = max(getHeight(x->left), getHeight(x->right)) + 1;
return x; // 返回新的根节点
}
// 左旋操作
Node* leftRotate(Node* x) {
Node* y = x->right; // 取右子树为新根
Node* T2 = y->left; // 保存y的左子树
// 进行旋转
y->left = x; // x变为y的左子树
x->right = T2; // 将T2作为x的右子树
// 更新节点高度
x->height = max(getHeight(x->left), getHeight(x->right)) + 1;
y->height = max(getHeight(y->left), getHeight(y->right)) + 1;
return y; // 返回新的根节点
}
// 插入节点
Node* insert(Node* node, int key) {
// 1. 正常的二叉搜索树插入
if (!node) return new Node(key); // 如果树为空,创建新节点
if (key < node->key) {
node->left = insert(node->left, key); // 插入到左子树
} else {
right->left = merge(left, right->left);
return right;
node->right = insert(node->right, key); // 插入到右子树
}
// 2. 更新当前节点的高度
node->height = max(getHeight(node->left), getHeight(node->right)) + 1;
// 3. 获取平衡因子并进行旋转以保持树的平衡
int balance = getBalance(node);
// 左左情况:右旋
if (balance > 1 && key < node->left->key) return rightRotate(node);
// 右右情况:左旋
if (balance < -1 && key > node->right->key) return leftRotate(node);
// 左右情况:先左旋再右旋
if (balance > 1 && key > node->left->key) {
node->left = leftRotate(node->left);
return rightRotate(node);
}
// 右左情况:先右旋再左旋
if (balance < -1 && key < node->right->key) {
node->right = rightRotate(node->right);
return leftRotate(node);
}
return node; // 返回(可能已平衡的)当前节点
}
// 中序遍历函数
void inOrder(Node* root) {
if (root) {
inOrder(root->left); // 遍历左子树
cout << root->key << " "; // 输出当前节点的键值
inOrder(root->right); // 遍历右子树
}
}
void split(Node* root, int key, Node*& left, Node*& right) {
if (!root) {
left = right = nullptr;
return;
}
if (root->value < key) {
left = root;
split(root->right, key, left->right, right);
} else {
right = root;
split(root->left, key, left, right->left);
}
}
void insert(Node*& root, int value) {
Node *newNode = new Node(value);
if (!root) {
root = newNode;
return;
}
if (root->priority > newNode->priority) {
split(root, value, root->left, root->right);
root = newNode;
root->left = root->left;
root->right = root->right;
} else {
insert(root->value < value ? root->right : root->left, value);
}
}
// 使用示例
int main() {
Node* root = nullptr;
insert(root, 10);
insert(root, 20);
insert(root, 15);
// 可以继续实现其他操作,如查找、删除等
Node* root = nullptr; // 初始化根节点为空
// 插入节点
root = insert(root, 10);
root = insert(root, 20);
root = insert(root, 30);
root = insert(root, 40);
root = insert(root, 50);
root = insert(root, 25);
// 中序遍历结果
cout << "中序遍历结果: ";
inOrder(root); // 输出: 10 20 25 30 40 50
return 0;
}
```
这个示例展示了如何插入节点并保持笛卡尔树的性质。可以根据需要扩展实现其他功能,比如查找和删除。
## 平衡树(AVL, treap, splay)
### 代码说明
1. **节点结构**:定义了一个`Node`结构体,包含键值、左右子树指针和高度信息。
2. **高度和平衡因子计算**`getHeight`和`getBalance`函数分别用于获取节点的高度和平衡因子。
3. **旋转操作**:实现了右旋和左旋操作,以保持树的平衡。
4. **插入操作**:插入节点时,首先按照二叉搜索树的规则插入,然后更新高度并检查是否需要旋转来保持平衡。
5. **中序遍历**:实现了中序遍历,输出树的节点值,确保按升序排列。
# 常见图
## 稀疏图
稀疏图sparse graph是指边的数量相对较少的图。在图论中通常用图的顶点数量 \( V \) 和边数量 \( E \) 来描述图的稀疏性。对于稀疏图,边的数量 \( E \) 通常满足以下条件:
\[ E \ll V^2 \]
这意味着对于一个拥有 \( V \) 个顶点的图,边的数量远远小于 \( V^2 \)。例如,在一个稀疏图中,边的数量可能在 \( O(V) \) 或 \( O(V \log V) \) 的数量级,而不是 \( O(V^2) \)。
稀疏图的特点包括:
1. **存储效率**由于边的数量较少稀疏图通常使用邻接表adjacency list存储而不是邻接矩阵adjacency matrix以节省存储空间。
2. **算法选择**:在处理稀疏图时,一些算法(如 Dijkstra 算法和 Prim 算法)在稀疏图上会有更好的性能,因为它们可以更高效地处理较少的边。
3. **应用场景**:稀疏图常见于实际应用中,例如社交网络、道路网络和互联网连接等,这些场景中节点(顶点)相对较多,但连接(边)通常较少。
## 偶图(二分图)
二分图Bipartite Graph是一个特殊的图结构其顶点可以被分为两个不相交的子集 \( U \) 和 \( V \),并且图中每条边连接的两个顶点分别来自这两个子集。换句话说,二分图中的每条边都连接一个属于 \( U \) 的顶点和一个属于 \( V \) 的顶点。
### 特征
1. **颜色标记**:二分图可以用两种颜色标记顶点,使得任何一条边连接的两个顶点颜色不同。
2. **无奇环**二分图中不存在长度为奇数的环。这是二分图的一个重要性质能够通过深度优先搜索DFS或广度优先搜索BFS来验证。
### 应用
二分图在许多实际问题中非常有用,例如:
- **匹配问题**:如婚姻匹配、任务分配等。
- **推荐系统**:用户和物品之间的关系。
- **网络流问题**:如流量分配和最小割问题。
### 例子
假设我们有一个二分图,顶点集 \( U = \{A, B, C\} \) 和 \( V = \{1, 2\} \),边集为 \( \{(A, 1), (A, 2), (B, 2), (C, 1)\} \)。在这个图中:
- \( A \) 和 \( B \) 都连接到 \( 2 \),而 \( C \) 连接到 \( 1 \)。
- 我们可以将 \( U \) 的顶点涂成一种颜色(例如红色),而 \( V \) 的顶点涂成另一种颜色(例如蓝色),确保相连的顶点颜色不同。
## 欧拉图
欧拉图是指一种特殊的图,它包含一条经过图中每一条边恰好一次的闭合路径,称为欧拉回路。若图中存在这样的路径但不要求回到起点,则称为欧拉路径。对于一个连通的图,存在欧拉回路的必要条件是每个顶点的度数为偶数;而存在欧拉路径的条件是最多有两个顶点的度数为奇数。欧拉图在图论和网络分析中有广泛的应用。
## 有向无环图
有向无环图Directed Acyclic Graph简称 DAG是一种图结构其特点是
1. **有向性**:图中的边是有方向的,即每条边都有一个起始顶点和一个终止顶点。也就是说,从一个顶点到另一个顶点的边只能单向访问。
2. **无环性**在图中不存在从某个顶点出发通过若干条边再次回到该顶点的路径。换句话说DAG 中没有环。
由于这些特性DAG 被广泛应用于多个领域,包括:
- **任务调度**在多任务环境中DAG 可以用来表示任务之间的依赖关系,确保在执行某个任务之前,所有依赖的任务都已完成。
- **版本控制系统**:例如 Git 使用 DAG 来管理提交历史,确保每个提交都有一个线性的历史。
- **数据处理管道**在数据处理或流处理系统中DAG 用于表示数据流的处理步骤。
- **拓扑排序**DAG 可以进行拓扑排序,生成一个线性序列,使得对于每一条有向边,起始顶点在终止顶点之前。
## 连通图与强连通图
在图论中,连通图和强连通图是描述图中节点连接性质的两个重要概念。
### 连通图
一个无向图称为**连通图**,如果从图中的任意一个节点出发,都可以通过图中的边到达其他任意节点。这意味着图中的任意两个节点之间都有路径相连。如果一个无向图不是连通的,它会被分成多个连通分量,每个连通分量都是一个连通的子图。
### 强连通图
一个有向图称为**强连通图**,如果图中的任意两个节点 \( u \) 和 \( v \) 都存在路径从 \( u \) 到 \( v \) 且从 \( v \) 到 \( u \)。也就是说,强连通图中的任意两个节点都是彼此可达的。如果一个有向图不是强连通的,它也可以分成多个强连通分量,每个强连通分量都是一个强连通的子图。
### 示例
- **连通图**:考虑一个无向图,节点 A、B、C 和 D 之间的边连接形成一个连通图,因为从任意节点出发都可以访问到其他节点。
- **强连通图**:考虑一个有向图,节点 A、B、C 之间有边 A→B、B→C 和 C→A这样就形成了一个强连通图因为每对节点之间都可以找到相应的路径。
## 双连通图
双连通图biconnected graph是图论中的一个重要概念。一个无向图是双连通的如果它是连通的并且去掉任意一个顶点后剩下的图仍然是连通的。换句话说在双连通图中任意两个顶点之间有至少两条不同的路径连接它们这些路径不会经过同一个顶点。
双连通图的一个重要性质是它的桥bridge即边的删去会导致图不连通。双连通图没有桥因此任何边的删去都不会破坏图的连通性。
双连通分量biconnected component是双连通图的一个子图它是最大双连通子图的集合。可以使用深度优先搜索DFS来找到图的所有双连通分量。
# 哈希表
## 数值哈希函数构造

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB