阅读笔记 | C++ | 算法竞赛入门经典 6.3 树和二叉树
刘汝佳《算法竞赛入门经典》6.3阅读笔记
二叉树的递归定义:二叉树要么为空,要么由根结点、左子树和右子树组成,左子树和右子树分别是一棵二叉树。
6.3.1 二叉树的编号
给定一棵高度为\(d\)的完全二叉树,则它包含\(2^d\)个结点,如果把结点从上到下从左到右编号为1,2,3......则结点\(k\)的左右子节点编号分别为\(2k\)和\(2k+1\).
该题目将“编号”的奇偶特征同模拟的方法和结果直接结合起来。
6.3.2 二叉树的层次遍历
例题6-7 树的层次遍历
输入一棵二叉树,按从上到下、从左到右的层次遍历顺序输出各个结点的值。结点个数不超过256。每个结点的左右括号之间没有空格,相邻结点之间用一个空格隔开,每棵树的输入用“()”结束。若从根到某个叶结点的路径上有的结点没有在输入中给出,或者给出超过一次,应当输出-1。
样例输入:
(11,LL) (7,LLL) (8,R) (5,) (4,L) (13,RL) (2,LLR) (1,RRR) (4,RR) ()
(3,L) (4,R) ()
样例输出:
5 4 8 11 13 4 7 2 1
-1
分析:结点最多有256个,如果这些结点形成了一条链,最后一个结点的编号将大到无法用整数表示,因此需要使用动态结构建树。
//处理输入
char s[maxn]; //保存读入结点 以(11,LL)为例
bool read_input() {
failed = false;
remove_tree(root);
root = newnode(); //创建根结点
for (;;) {
if (scanf("%s", s) != 1) return false; //若读不到字符串,整个程序的读入结束,通知main函数
if (!strcmp(s, "()")) break; //若读到(),该棵树的读入结束,退出循环
int v;
sscanf(&s[1], "%d", &v); //v中保存11
addnode(v, strchr(s, ',')+1); //实际调用addnode(11, "LL)")
}
return true;
}
可以用指针实现动态的二叉树
//定义结点
struct Node{
bool have_value; //是否被赋值过
int v; //结点值
Node *left, *right; //左右孩子
Node():have_value(false), left(NULL), right(NULL) {} //构造函数
};
//定义根结点
Node* root;
//创建新结点
Node* newnode() {return new Node();}
//释放一棵二叉树,避免内存泄漏
void remove_tree(Node* u) {
if (u == NULL) return;
remove_tree(u->left);
remove_tree(u->right);
delete u; //调用u的析构函数并释放u结点本身的内存
}
//按照序列寻找赋值结点的过程
void addnode(int v, char* s) {
int n = strlen(s);
Node* u = root; //从根结点出发,向下移动找到目标结点
for (int i = 0; i < n; i++) {
if (s[i] == 'L') {
if (u->left == NULL)
u->left = newnode(); //结点不存在,建立新结点
u = u->left; //向左移动
} else if (s[i] == 'R') {
if (u->right == NULL)
u->right = newnode();
u = u->right;
}
}
if (u->have_value) failed = true;
u->v = v;
u->have_value = true;
}
建树结束后,即可使用一个队列来完成进行层次遍历(宽度遍历BFS)。
初始时根节点入队,每次一个结点出队,就把他的左右子结点入队。
bool bfs(vector<int>& ans) {
queue<Node*> q;
ans.clear();
q.push(root);
while (!q.empty()) {
Node* u = q.front(); q.pop();
if (!u->have_value) return false; //若到某叶结点的路径上有结点没有被赋值过,输入有误
ans.push_back(u->v);
if (u->left != NULL) q.push(u->left);
if (u->right != NULL) q.push(u->right);
}
return true;
}
也可以使用数组实现二叉树
按照结点的生成顺序对结点编号,left[u]
和right[u]
分别表示u的左右子结点的编号。
const int root = 1;
void newtree() {
left[root] = right[root] = 0;
have_value[root] = false;
cnt = root;
}
int newnode() {
int u = ++cnt;
left[u] = right[u] = 0;
have_value[root] = false;
return u;
}
但是,用指针直接访问比“数组+下标”的方式略快,因此也可以使用这样一种方法,维护一个静态申请的结构体数组node
。
Node* newnode() {
Node* u = &node[++cnt];
u->left = u->right = NULL;
u->have_value = false;
return u;
}
很显然,如果使用这种写法,cnt
只能持续增加,如果有新建结点和删除结点的动作,那么被删除的结点占用的空间无法被回收。如果希望解决内存浪费的问题,可以使用“内存池”的方法,维护一个空闲列表,初始时把上述node
数组中所有元素的指针放到该列表中。
静态数组配合空闲列表可以实现一个简单的内存池
queue<Node*> freenodes;
Node node[maxn];
void init() {
for (int i = 0; i < maxn; i++)
freenodes.push(&node[i]); //初始化内存池
}
Node* newnode() {
Node* u = freenodes.front();
u->left = u->right = NULL;
u->have_value = false;
freenodes.pop();
return u;
}
void deletenode(Node* u) {
freenodes.push(u);
}
6.3.3 二叉树的递归遍历
二叉树有3种深度优先遍历:先序遍历、中序遍历和后序遍历
例题6-8 树
给定一颗点带权(权值各不相同)的二叉树的中序和后序遍历,找一个叶子使得它到根的路径上的和最小。如果有多解,该叶子本身的权应尽量小。
样例输入:
3 2 1 4 5 7 6
3 1 2 5 6 7 4
7 8 11 3 5 16 12 18
8 3 11 7 16 18 12 5
255
255
样例输出:
1
3
255
分析:给定二叉树的中序遍历和后序遍历,可以构造出这棵二叉树。方法是根据后序遍历找到树根(最后一个字符),然后在中序遍历中找到树根,从而找出左右子树的结点列表,然后递归构造左右子树。
以第一个样例输入为例。4为根结点,4将中序遍历划分为3 2 1
和5 7 6
。
对于左子树,2为根结点;对于右子树,7为根结点。如此递归。
注意学习以下写法中对输入的处理。当输入不确定行数、不确定每行的整数个数时,可以使用getline(cin,line)
库函数,标准输入中的一行被存入string line
中,若读取不到输入,则函数返回false
。然后,可以使用stringstream ss(line)
将字符串line
转化为流ss
,接着,就可以非常方便地使用ss >> x
将流中的整数读取出来。
#include<iostream>
#include<string>
#include<sstream>
#include<algorithm>
using namespace std;
const int maxv = 10000 + 10;
int in_order[maxv], post_order[maxv], lch[maxv], rch[maxv];
int n; //当前处理的树中的结点个数
bool read_list(int* a) {
string line;
if (!getline(cin, line)) return false;
stringstream ss(line);
n = 0;
int x;
while (ss >> x) a[n++] = x;
return n > 0
}
//把in_order[L1..R1]和post_order[L2..R2]建成一棵二叉树,返回树根
int build(int L1, int R1, int L2, int R2) {
if (L1 > R1) return 0; //空树
int root = post_order[R2];
int p = L1;
while (in_order[p] != root) p++;
int cnt = p - L1;
lch[root] = build(L1, p-1, L2, L2+cnt-1);
rch[root] = build(p+1, R1, L2+cnt, R2-1);
return root;
}
int best, best_sum;
void dfs(int u, int sum) {
sum+=u;
if (!lch[u] && !rch[u]) { //若为叶子
if (sum < best_sum || (sum == best_sum && u < best)) { //正确理解最优解的条件
best = u;
best_sum = sum;
}
}
if (lch[u]) dfs(lch[u], sum);
if (rch[u]) dfs(rch[u], sum);
}
int main() {
while (read_list(in_order)) {
read_list(post_order);
build(0, n-1, 0, n-1);
best_sum = 1000000000;
dfs(post_order[n-1], 0);
cout << best << "\n";
}
return 0;
}
例题6-9 天平
输入一个树状天平,根据力矩相等(\(W_lD_l=W_rD_r\))原则判断是否平衡,其中\(W_l\)和\(W_r\)分别为左右两边砝码的重量,D为距离。
采用递归(先序)的方式输入:每个天平的格式为\(W_l\),\(D_l\),\(W_r\),\(D_r\),当\(W_l\)或\(W_r\)为\(0\)时,表示该“砝码”实际是一个子天平,接下来会描述这个子天平。当\(W_l=W_r=0\)时,会先描述左子天平,然后是右子天平。
样例输入:
1
0 2 0 4
0 3 0 1
1 1 1 1
2 4 4 2
1 6 3 2
样例输出:
YES
递归输入,一边读入一边判断是否平衡,通过引用传值来精简代码
#include<iostream>
using namespace std;
//输入一个子天平,返回天平是否平衡,参数W修改为子天平的总重量
bool solve(int& W) {
int W1, D1, W2, D2;
bool b1 = true, b2 = true;
cin >> W1 >> D1 >> W2 >> D2;
if (!W1) b1 = solve(W1);
if (!W2) b2 = solve(W2);
W = W1 + W2;
return b1 && b2 && (W1 * D1 == W2 * D2);
}
int main() {
int T, W;
cin >> T;
while (T--) {
if (solve(W)) cout << "YES\n";
else cout << "NO\n";
if (T) cout << "\n";
}
return 0;
}