阅读笔记 | 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 15 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;
}
posted @ 2021-02-22 00:19  杞棠  阅读(132)  评论(0编辑  收藏  举报