Tree
树
树的定义与性质
定义
树是没有重边(两个节点间最多有1条边)以及自环(Self-loop)的图(Graph)

树的根是没有父节点的结点
1. 考虑结点K。根A到结点K的唯一路径上的任意结点,称为结点K的祖先。
路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子。根A是树中唯一没有双亲的结点。
有相同双亲的结点称为兄弟。
树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。
度大于0的结点称为分支结点(又称非终端结点);度为0(没有子女结点)的结点称为叶子结点(又称终端结点)。
在分支结点中,每个结点的分支数就是该结点的度。
2.结点的深度、高度和层次。
结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推。双亲在同一层的结点互为堂兄弟
结点的深度是从根结点开始自顶向下逐层累加的。
结点的高度是从叶结点开始自底向上逐层累加的。
树的高度(或深度)是树中结点的最大层数。图中树的高度为\(4\)。
3.有序树和无序树。
树中结点的各子树从左到右是有次序的, 不能互换, 称该树为有序树,否则称为无序树。
假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
路径和路径长度。树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
森林是\(m (m≥0)\)棵互不相交的树的集合。森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给\(m\)棵独立的树加上一个结点,并把这\(m\)棵树作为该结点的子树,则森林就变成了树
性质:
1. 树中的结点数等于所有结点的度数加 1.
2. 度为\(m\)的树中第\(i\)层上至多有\(m^{i-1}\) 个结点( $i ≥ 1 $)
3. 高度为\(h\)的\(m\)叉树至多有\(\frac{m^h-1}{m-1}\)结点。
4. 具有\(n\)个结点的\(m\)叉树的最小高度为$ \log m ( n ( m − 1 ) + 1 ) $。
二叉树
定义
二叉树是一种树形结构,特点是每个结点至多有两棵子树
二叉树也以递归的形式定义。二叉树是\(n(n≥0)\)个结点的有限集合:
1. 或者为空二叉树,即\(n=0\);
2. 或者有一个根结点和两个互不相干的左子树和右子树构成,其中左右子树都为二叉树。
特殊的二叉树
斜树
所有的结点只有左子树的二叉树叫做左斜树
所有的结点只有右子树的二叉树叫做右斜树
满二叉树

一棵高度为\(h\),且含有 \(2^h - 1\) 的二叉树称作满二叉树,即树中的每层都含有最多的结点。可以对满二叉树按层序编号:约定编号从根结点(根结点编号为\(1\))起,自上而下,自左向右。
对于每一个满二叉树上的结点\(i\),如果有双亲,则双亲为\(floor(i/2)\),如有孩子,则左孩子为\(2i\),右孩子为\(2i+1\)
完全二叉树
高度为\(h\)、有\(n\)个结点的二叉树称作完全二叉树,当且仅当每个结点都与高度为\(h\)的满二叉树中编号为\(1\)~\(n\)的结点一一对应

特点:
1. 若\(i \le \frac n2\),则节点\(i\)为分支节点,否则为叶子结点
2. 叶子结点只可能出现在层次最大的两层上
对于最大层次中的叶子结点,都依次排列在该层最左侧的位置上
3. 若有度为\(1\)的结点,则只可能有一个,且该结点仅有左孩子而没有右孩子
4. 按层次编号后,一旦出现某节点 \(i\) 是叶子结点或者仅有左孩子
5. 若\(n\)为奇数,则每个分支结点都有左右孩子;若\(n\)为偶数,则编号最大的分支结点仅有左孩子,且编号为\(\frac n2\)
二叉排序树
左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过\(1\)。
性质
- 任意一棵树,若结点树为\(n\),则有边\(n-1\)条
- 非空二叉树上的叶子结点等于度为\(2\)的结点数加一,即\(n_0=n_2+1\)
- 非空二叉树上\(k\)层至多\(2^k-1\)个结点\((k≥ 1)\)
- 高度为\(h\)的二叉树至多有\(2^h-1\)个结点\((h≥1)\)
- 对完全二叉树按从上到下,从左到右的准许依次编号\(1,2...n\),则:
- \(i> 1\)时,结点\(i\)的双亲的编号为\(floor(i/2)\)
- 当\(2i\le n\)时,结点\(i\)的左孩子为\(2i\);否则无左孩子
- 当\(2i+1\le n\)时,结点\(i\)的右孩子为\(2i+1\);否则无右孩子
- 结点\(i\)所在层次为\(\{\log_2i\}+1\)
- 具有\(n(n>0)\)个结点的完全二叉树的高度为\(\{\log_2n\}+1\)
模版代码,包含了基础的操作
#include <vector>
#include <queue>
#include <stack>
#include <iostream>
#include <stdexcept>
namespace BT { // BinaryTree 命名空间
template <typename T>
class Tree {
private:
struct Node {
T val; // 值
int l = -1; // 左子节点索引
int r = -1; // 右子节点索引
int p = -1; // 父节点索引
bool ok = false; // 节点是否有效
};
std::vector<Node> nd; // 节点存储数组
int rt = -1; // 根节点索引
int nxt = 0; // 下一个可用索引
public:
// 创建新节点
int crtNode(const T& v) {
if (nxt >= nd.size()) {
nd.push_back({v, -1, -1, -1, true});
} else {
nd[nxt] = {v, -1, -1, -1, true};
}
return nxt++;
}
// 设置根节点
void setRt(int idx) {
if (idx >= 0 && idx < nd.size() && nd[idx].ok) {
rt = idx;
} else {
throw std::invalid_argument("Invalid node idx");
}
}
// 获取根节点索引
int getRt() const {
return rt;
}
// 设置左孩子
void setL(int p, int c) {
if (p >= 0 && p < nd.size() && nd[p].ok &&
c >= 0 && c < nd.size() && nd[c].ok) {
nd[p].l = c;
nd[c].p = p;
} else {
throw std::invalid_argument("Invalid node idx");
}
}
// 设置右孩子
void setR(int p, int c) {
if (p >= 0 && p < nd.size() && nd[p].ok &&
c >= 0 && c < nd.size() && nd[c].ok) {
nd[p].r = c;
nd[c].p = p;
} else {
throw std::invalid_argument("Invalid node idx");
}
}
// 获取节点值
T& getVal(int idx) {
if (idx >= 0 && idx < nd.size() && nd[idx].ok) {
return nd[idx].val;
}
throw std::out_of_range("Invalid node idx");
}
// 获取左孩子索引
int getL(int idx) const {
if (idx >= 0 && idx < nd.size() && nd[idx].ok) {
return nd[idx].l;
}
throw std::out_of_range("Invalid node idx");
}
// 获取右孩子索引
int getR(int idx) const {
if (idx >= 0 && idx < nd.size() && nd[idx].ok) {
return nd[idx].r;
}
throw std::out_of_range("Invalid node idx");
}
// 前序遍历
void preOrd(int idx, std::vector<T>& res) const {
if (idx == -1) return;
res.push_back(nd[idx].val);
preOrd(nd[idx].l, res);
preOrd(nd[idx].r, res);
}
// 中序遍历
void inOrd(int idx, std::vector<T>& res) const {
if (idx == -1) return;
inOrd(nd[idx].l, res);
res.push_back(nd[idx].val);
inOrd(nd[idx].r, res);
}
// 后序遍历
void postOrd(int idx, std::vector<T>& res) const {
if (idx == -1) return;
postOrd(nd[idx].l, res);
postOrd(nd[idx].r, res);
res.push_back(nd[idx].val);
}
// 层序遍历
void lvlOrd(std::vector<T>& res) const {
if (rt == -1) return;
std::queue<int> q;
q.push(rt);
while (!q.empty()) {
int cur = q.front();
q.pop();
res.push_back(nd[cur].val);
if (nd[cur].l != -1) q.push(nd[cur].l);
if (nd[cur].r != -1) q.push(nd[cur].r);
}
}
// 获取树的高度
int getHgt(int idx) const {
if (idx == -1) return 0;
return 1 + std::max(getHgt(nd[idx].l), getHgt(nd[idx].r));
}
// 获取节点数量
int getSize() const {
return nxt;
}
};
} // namespace BT
int main() {
return 0;
}
二叉搜索树/排序树(Binary Search Tree)
对于插入操作
- 从根节点开始比较
- 小于当前节点则向左子树插入
- 大于当前节点则向右子树插入
- 直到找到空位置插入新节点
对于查找操作 - 从根节点开始比较
- 等于当前节点:找到
- 小于当前节点:在左子树继续查找
- 大于当前节点:在右子树继续查找
- 遇到空节点:不存在
模版代码:
namespace BST {
template <typename T>
class binary_search_tree {
private:
struct Tree {
T val; //当前节点的值(Value)
int ls, rs; //左孩子,右孩子
};
Tree tree[Maxn];
int root = 0;
int idx = 1;
public:
int root_() {
return root;
} //返回节点
void insert(T x) { //插入
if (root == 0) {
root = idx;
tree[idx++] = {x, 0, 0};
return;
}
int p = 0, u = root;
while (u != 0) {
p = u;
if (x == tree[u].val)
return ;
if (x < tree[u].val) u = tree[u].ls;
else u = tree[u].rs;
}
int newNode = idx++;
tree[newNode] = {x, 0, 0};
if (x < tree[p].val) tree[p].ls = newNode;
else tree[p].rs = newNode;
}
int find(T x){//查找
int u = root;
while (u) {
if(x == tree[u].val)
return u;
if (x < tree[u].val)
u = tree[u].ls;
else
u = tree[u].rs;
}
return 0;
}
void preOrder(int u) {//先序遍历
cout << tree[u].val << " ";
if (tree[u].ls)
preOrder(tree[u].ls);
if (tree[u].rs)
preOrder(tree[u].rs);
}
void inOrder(int u) {//中序遍历,输出的是有序的
if (tree[u].ls)
inOrder(tree[u].ls);
cout << tree[u].val << " ";
if (tree[u].rs)
inOrder(tree[u].rs);
}
void postOrder(int u) {//后序遍历
if (tree[u].ls)
postOrder(tree[u].ls);
if (tree[u].rs)
postOrder(tree[u].rs);
cout << tree[u].val << " ";
}
};
}
插入可以选择递归实现,编码简单,所以不放上来
表达式树(Expression Tree)
表达式树是一棵二叉树
其先序遍历是前缀表达式(波兰式)
其中序遍历是中缀表达式
其后缀遍历是后缀表达式(逆波兰式)
后缀转表达式树,遇到运算符,以其为根节点,连接两个数字,具体的看代码:
//例题,后缀表达式转中缀表达式
#include<bits/stdc++.h>
using namespace std;
bool isOperator(char ch) {
return ch == '+' || ch == '-' || ch == '*' || ch == '/';
}
int precedence(string op) { //优先级
if (op[0] == '+' || op[0] == '-')return 1;
if (op[0] == '*' || op[0] == '/')return 2;
return 0;
}
struct ET {
int ls, rs;
string value;
int pre;
};
vector<ET> et;
int cnt = 0;
void inOrder(int u, int prece) {
if (et[u].ls) {
int temp = et[et[u].ls].pre;
if (temp > 0 && temp < et[u].pre) // 优先级低的需要加上括号
cout << "(";
inOrder(et[u].ls, et[u].pre);
if (temp > 0 && temp < et[u].pre)
cout << ")";
}
cout << et[u].value;
if (et[u].rs) {
int temp = et[et[u].rs].pre;
if (temp > 0 && temp <= et[u].pre) //注意,右子树在优先级相同的情况下也要加括号
cout << "(";
inOrder(et[u].rs, et[u].pre);
if (temp > 0 && temp <= et[u].pre)
cout << ")";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
et.push_back((ET) {
0, 0, "", 0
});
string postfix;
stack<int> s;
while (cin >> postfix) {
if (isdigit(postfix[0])) {
// int num = stoi(postfix); // Transform Int
et.push_back((ET) {
0, 0, postfix, 0
});
s.push(++cnt);
} else { // 当前字符是表达式,考虑以其作为根节点
int r = s.top();// 右子树,是数字
s.pop();
int l = s.top();// 左子树,也是数字
s.pop();
et.push_back((ET) {
l, r, postfix, precedence(postfix)
});
s.push(++cnt);
}
}
inOrder(s.top(), 0); // 中序遍历
return 0;
}
堆
堆是一种树形结构,根节点是当前序列的最优值(Optimization)
通常我们用完全二叉树来存储堆
对于插入操作,将新元素放在最后一个节点,然后根据性质维护堆的性质
对于删除操作,将堆顶与最后一个节点交换,然后删除最后一个节点,维护堆
手写堆
namespace Heap {
const int Maxn = 1e6 + 5;
const int Null = 0;
template<typename T>
class heap_min { // 小根堆
T minheap[Maxn];
int cnt = 0; // 初始化为 0
public:
int lson(int i) { // 左孩子
return (2 * i <= cnt) ? 2 * i : Null;
}
int rson(int i) { // 右孩子
return (2 * i + 1 <= cnt) ? 2 * i + 1 : Null;
}
int parent(int i) { // 父节点
return (i != 1) ? i / 2 : Null;
}
T top() { // 返回堆顶
if (cnt == 0) throw runtime_error("Heap is empty");
return minheap[1];
}
void Fix_up(int i) { // 自底向上调整(用于 push)
while (i != 1 && minheap[i] < minheap[parent(i)]) {
swap(minheap[i], minheap[parent(i)]);
i = parent(i);
}
}
void Fix_down(int i) { // 自顶向下调整(用于 pop)
while (true) {
int smallest = i;
int l = lson(i), r = rson(i);
if (l != Null && minheap[l] < minheap[smallest]) smallest = l;
if (r != Null && minheap[r] < minheap[smallest]) smallest = r;
if (smallest == i) break;
swap(minheap[i], minheap[smallest]);
i = smallest;
}
}
void push(T val) {
if (cnt + 1 >= Maxn) throw runtime_error("Heap overflow");
minheap[++cnt] = val;
Fix_up(cnt); // 调整堆
}
void pop() {
if (cnt == 0) throw runtime_error("Heap underflow");
swap(minheap[1], minheap[cnt--]);
Fix_down(1); // 调整堆
}
bool empty() const {
return cnt == 0;
}
int size() const {
return cnt;
}
};
template<typename T>
class heap_max { // 大根堆
T maxheap[Maxn];
int cnt = 0;
public:
int lson(int i) {
return (2 * i <= cnt) ? 2 * i : Null;
}
int rson(int i) {
return (2 * i + 1 <= cnt) ? 2 * i + 1 : Null;
}
int parent(int i) {
return (i != 1) ? i / 2 : Null;
}
T top() {
if (cnt == 0) throw runtime_error("Heap is empty");
return maxheap[1];
}
void Fix_up(int i) {
while (i != 1 && maxheap[i] > maxheap[parent(i)]) {
swap(maxheap[i], maxheap[parent(i)]);
i = parent(i);
}
}
void Fix_down(int i) {
while (true) {
int largest = i;
int l = lson(i), r = rson(i);
if (l != Null && maxheap[l] > maxheap[largest]) largest = l;
if (r != Null && maxheap[r] > maxheap[largest]) largest = r;
if (largest == i) break;
swap(maxheap[i], maxheap[largest]);
i = largest;
}
}
void push(T val) {
if (cnt + 1 >= Maxn) throw runtime_error("Heap overflow");
maxheap[++cnt] = val;
Fix_up(cnt);
}
void pop() {
if (cnt == 0) throw runtime_error("Heap underflow");
swap(maxheap[1], maxheap[cnt--]);
Fix_down(1);
}
bool empty() const {
return cnt == 0;
}
int size() const {
return cnt;
}
};
}
例题(堆)
题目描述
超市里有\(N\)件商品,每个商品都有利润\(p_i\)和过期时间\(d_i\),每天只能卖一件商品,过期商品(即当天\(d_i\le0\))不能再卖。
求合理安排每天卖的商品的情况下,可以得到的最大收益是多少。
输入格式
输入包含多组测试用例。
每组测试用例,以输入整数N开始,接下里输入\(N\)对\(p_i\)和\(d_i\),分别代表第i件商品的利润和过期时间。
在输入中,数据之间可以自由穿插任意个空格或空行,输入至文件结尾时终止输入,保证数据正确。
输出格式
对于每组产品,输出一个该组的最大收益值。
每个结果占一行。
思路很简单,按照时间排序,遇到更大的就反悔
#include <bits/stdc++.h>
using namespace std;
struct Product {
int value;
int date;
};
bool compare(const Product &a, const Product &b) {
return a.date < b.date;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int N;
while (cin >> N) {
vector<Product> products(N);
for (int i = 0; i < N; ++i)
cin >> products[i].value >> products[i].date;
sort(products.begin(), products.end(), compare);
priority_queue<int, vector<int>, greater<int>> heap; //小根堆,存价值
for (const auto &product : products) {
if (product.date > (int)heap.size())
heap.push(product.value);
else if (!heap.empty() && product.value > heap.top()) {
heap.pop();
heap.push(product.value);
}
}
int ans = 0;
while (!heap.empty()) {
ans += heap.top();
heap.pop();
}
cout << ans << "\n";
}
return 0;
}
STL(Standard Template Library)中自带堆,叫做优先队列(Priority Queue)
定义:
priority_queue<typename> yourarrayname;
默认是大根堆
而定义小根堆:
priority_queue<typename, vector<typename>, greater<typename>>
需要注意的是,放入优先队列的一定是有比较函数的类型
大根堆对应的是小于
小根堆对应的是大于(也别管为什么,理解起来有点难度)
在结构体中重载比较符,需要注意:
bool operator< /*>*/(const &node ano) const/*这很重要*/{ // 优先队列不允许修改
}
哈夫曼树
哈夫曼编码力求用最小的编码长度在不引起歧义的情况下进行编码
其实就是使\(\sum w_i l_i\)最小
具体的实现需要使用小根堆辅助
实现:
我们需要让出现频率最小的离根节点最远,而出现频率越大的离根结点越近
那么实现看代码
namespace Huffman {
const int Maxn = 1e5 + 5; // 最大节点数
const int Null = 0; // 空节点标识
template<typename T>
class huffman {
private:
// 哈夫曼树节点结构体
struct Node {
T weight; // 节点的权值(频率)
int id; // 节点唯一标识
int lson; // 左孩子节点ID
int rson; // 右孩子节点ID
// 重载<运算符(用于大根堆)
bool operator<(const Node& ano) const {
// 权值相同时,ID小的优先(保证稳定性)
if (weight == ano.weight)
return id > ano.id;
// 默认按权值降序
return weight > ano.weight;
}
// 重载>运算符(可用于小根堆)
bool operator>(const Node& ano) const {
if (weight == ano.weight)
return id < ano.id;
return weight < ano.weight;
}
};
string code[Maxn]; // 存储每个叶子节点的哈夫曼编码
Node tree[Maxn]; // 存储所有哈夫曼树节点
Heap::heap_max<Node> huff; // 使用大根堆(实际应为小根堆,这是潜在问题点)
public:
// 插入初始节点
void insert(T val, int id) {
tree[id] = Node{val, id, Null, Null}; // 创建叶子节点
huff.push(tree[id]); // 加入堆
}
// 递归生成哈夫曼编码
void Code(int id, string s) {
if (!id) return; // 空节点直接返回
// 如果是叶子节点(没有左右孩子),输出编码
if (tree[id].lson == Null)
cout << s << "\n";
code[id] = s; // 记录当前路径
// 递归处理左右子树
Code(tree[id].lson, s + "0"); // 左分支添加'0'
Code(tree[id].rson, s + "1"); // 右分支添加'1'
}
// 构建哈夫曼树
void buildtree(int n) {
int cnt = n; // 初始节点数
// 当堆中还有超过1个节点时继续合并
while (huff.size() > 1) {
// 取出权值最大的两个节点(注意:这里应该是最小的,是问题点)
auto l = huff.top();
huff.pop();
auto r = huff.top();
huff.pop();
// 创建新节点(权值为两者之和)
tree[++cnt] = Node{
l.weight + r.weight, // 新权值
cnt, // 新ID
l.id, // 左孩子
r.id // 右孩子
};
// 将新节点放回堆中
huff.push(tree[cnt]);
}
// 从根节点(最后剩下的节点)开始生成编码
Code(cnt, "");
}
// 打印所有编码(按原始ID顺序)
void print(int n) {
for (int i = 1; i <= n; i++)
cout << code[i] << "\n";
}
};
}

浙公网安备 33010602011771号