【PAT】PAT总结《树、并查集》

广义树

这种题一般给出每个结点的子结点,如果字段比较多可以使用结构体,然后使用一个指针向量来存储每个结点的子结点,当然还可以根据需要建立父结点的指针字段。广义树常用BSF层次遍历,可以视为是一种特殊结构的BFS题目(一般的BFS题结点不一定是树状结构)。

常用结构体模板

struct Node{
    // 其他字段
    vector<Node*> children;// 子结点
}

常用层次遍历模板

    queue<Node*> q;
    q.push(nodes);
    Node* cur;
    while (!q.empty()) {
        for(int i = 0, l = (int)q.size(); i < l; i++){
            for(auto lt : cur->children){
                    q.push(lt);
                }
            q.pop();
        }
    }

这个其实就是搜索那一章使用的一次取出一层的层次遍历方法,如果可能一个子结点存在多个父结点的话,还需要使用marked来防止重复入队。

供应商问题

A1079 Total Sales of Supply Chain
A1090 Highest Price in Supply Chain
A1106 Lowest Price in Supply Chain
这三题都差不多,套上面那个模板就可以了

A1094 The Largest Generation

A1094 The Largest Generation
水题,就是考BFS。

A1130 Infix Expression

A1130 Infix Expression
水题,考DFS遍历。

二叉树

要考虑什么时候需要建树,什么时候不需要建树。使用静态写法还是结构体写法。
如果题目直接给出每个结点的左右子结点,那么使用静态写法比较方便,否则使用结构体写法递归建树。要熟练掌握四种遍历顺序的概念和递归输出的方法。
要注意是否会出现重复结点值,如有的话可能插在等于它的结点的右子结点也可能不插入,视具体题目而定。
注意逆后序不是前序

建树模板

void insert(Node*&x, int value){
    if(x == NULL){
        x = new Node;
        x->value = value;
    }else if(value < x->value){
        insert(x->left, value);
    }else{
        insert(x->right, value);
    }
}

也可以不使用引用指针,而是使用return来返回根结点,其实差不多。

路径模板

// 递归检查
void dfs(Node*root){
    if(root == NULL){
       // 出口
    }
    if(root->color == BLACK) cnt++;// 累计黑色结点个数
    dfs(root->left);
    dfs(root->right);
    if(root->color == BLACK) cnt--;// 回溯
}

这个来自于红黑树判断那道题,我们可以用这种方法记录路径(如果是记录路径的话就是加入向量和从向量结尾弹出这一对操作)、计数等等。注意如果在一般的dfs里,如果出口不是无效结点,而是最后一个有效结点,在return的前面需要回溯,否则会出错。

中序遍历

我们可以发现,中序遍历的结点值正好是整棵二叉树结点值从小到大的排列。

完全二叉树

要注意完全二叉树不是满二叉树,然后完全二叉树的验证方法是遇到一个空结点,那么它同层次的后面不能有空结点。(例如遍历到一个有没有左子结点的结点,它不能有右子结点,它后面的结点也必须是叶子结点)。

寻找根结点

一般题目给出每个结点左右子结点的时候可能不给出根结点,可以通过读取的时候同时更新每个结点的父结点(没有被更新的初始化为-1,当前前提是所有结点值都是正数),然后遍历一遍找出根结点(它没有父结点)。

反转

如果只是反转,我们仅需要在读取的时候交换左右即可。如A1102。如果是交替反转,我们可以在遍历的时候总是正向入队,在输出的时候交替正向反向输出,如A1127。

AVL树模板

int height(Node* root){
    if(root == NULL) return 0;
    return root->height;
}
void updateHeight(Node* root){
    root->height = max(height(root->left), height(root->right)) + 1;
}

int getBalanceFactor(Node *root){
    return height(root->left) - height(root->right);
}

void L(Node*& root){
    Node* x = root->right;
    root->right = x->left;
    x->left = root;
    
    updateHeight(root);// 一定要先更新下层结点再更新上层结点
    updateHeight(x);
    
    root = x;
}
void R(Node*& root){
    Node* x = root->left;
    root->left = x->right;
    x->right = root;
    
    updateHeight(root);
    updateHeight(x);
    
    root = x;
}

void insert(int v, Node*& root){
    if(root == NULL){
        root = new Node(v);
        return;
    }
    if(v < root->value){
        insert(v, root->left);
        updateHeight(root);
        if(getBalanceFactor(root) == 2){
            if(getBalanceFactor(root->left) == 1){
                R(root);
            }else{
                L(root->left);
                R(root);
            }
        }
    }else{
        insert(v, root->right);
        updateHeight(root);
        if(getBalanceFactor(root) == -2){
            if(getBalanceFactor(root->right) == -1){
                L(root);
            }else{
                R(root->right);
                L(root);
            }
        }
    }
}

理解来记忆,是这样的:

  • height() 用来获取一个结点的高度,主要是对于空结点返回0,避免空结点的判断,非空结点直接返回成员属性就可以了。
  • updateHeight() 调用height来更新一棵子树的高度,注意一棵树的高度等于它的左子树和右子树最大高度加1
  • L() 左旋是把右子结点作为根结点。将根结点左子树的左子结点赋值为x,然后将x的右子树,插在原根结点的左子树(因为x是原根结点的左结点,所以它的右子树也比x小),然后更新结点高度,要注意先更新下层(root,此时它实际在下层了)再更新上层(x,此时它是实际的根节点),最后将x赋值为root完成根结点的替换。
  • getBalanceFactor(Node* x) 求x的左子树和右子树高度差(值为-2, -1, 0, 1, 2)
  • R()和上面对称。
  • insert(),在递归建树的代码里加入了旋转的检测。
    • 在左子树插入的时候。若factor为2代表左左插入,应该右旋,为1代表右左插入,应该先左旋再右旋。
    • 对于右子树,和上面是对称,只是值变为负数。

A1086 Tree Traversals Again

A1086 Tree Traversals Again
这题主要是通过栈给出树的先序遍历和后序遍历,当然我们也可以直接通过栈的入栈出栈情况直接建树,我用了这一方法。

A1102 Invert a Binary Tree

A1102 Invert a Binary Tree
找出根结点遍历即可,对于反转,我们只需要在读取的时候交换左右即可。

A1064 Complete Binary Search Tree

A1064 Complete Binary Search Tree
这题很巧妙,利用了中序遍历的特点,可以避开完全二叉树这个问题。

A1099 Build A Binary Search Tree

A1099 Build A Binary Search Tree
和上面一题一样,要利用中序遍历的特点,填充各个结点的值,然后使用队列层次遍历即可。

A1127 ZigZagging on a Tree

A1127 ZigZagging on a Tree
这道题如果层次遍历的时候交替正反入队会很麻烦(因为反向的时候需要先加入右子结点,而且下一层又需要反向遍历子结点,否则出错)而且容易出错,所以我们可以采取正向入队,然后交替反向输出即可。

A1138 Postorder Traversal

A1138 Postorder Traversal
这道题有点意思,要认真思考不同遍历顺序之间的联系,从而由前序和中序得到后序的第一个结点。

A1119 Pre- and Post-order Traversals

A1119 Pre- and Post-order Traversals
这道题要考虑后序的前一个结点在中序中的位置,如果他在中序中的位置的左边紧接着就是根结点,那么我们就无法区分它是左子树还是右子树,那么答案不唯一,否则唯一。

LCA

看来姥姥很喜欢把最近一年的题目改改就做新题,A1143考的是广义树的LCA,因为是广义树所以没有二叉树的规律可以利用,所以使用的是先找出一个结点的路径再找第二个的路径的同时更新LCA,而A1151因为有二叉树的规律,可以直接利用。这两题实际还是考察搜索算法,主要是DFS
A1143 Lowest Common Ancestor
A1151 LCA in a Binary Tree

AVL树

A1066 Root of AVL Tree
A1123 Is It a Complete AVL Tree

红黑树判定

A1135 Is It A Red-Black Tree

并查集

一般直接上压缩的并查集算法就可以了。其实这个算法本质就是一棵高度最大为3的树。要注意在这个算法里求i的根节点不能直接调用parent[i],而要使用find(i)。这是因为它的压缩是惰性的,只有在搜索到的时候才可能压缩,所以两个同一集合的结点可能指向根节点,也可能指向根节点的子结点(也就是它是孙子结点)。

模板

int parent[MAX_K + 1], rank[MAX_K + 1], size[MAX_K + 1];
void init(int N){
    for(int i = 1; i <= MAX_K; i++){
        parent[i] = i;
        rank[i] = 0;
        size[i] = 0;
    }
}
int find(int p){
    while (p != parent[p]) {
        parent[p] = parent[parent[p]]; // 路径压缩
        p = parent[p];
    }
    return p;
}
bool connected(int p, int q){
    return find(p) == find(q);
}
// union是保留关键字,所以函数名不能为union
void Union(int p, int q){
    int rootP = find(p), rootQ = find(q);
    if(rootP == rootQ){
        return;
    }
    // 总是将小树合并到大树上,如果两棵树高度一样,则任意合并,合并后树的高度加1,合并树的同时合并计数
    if(rank[rootP] < rank[rootQ]){
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP]; // 合并人数
    }else if(rank[rootP] > rank[rootQ]){
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ]; // 合并人数
    }else{
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ]; // 合并人数
        rank[rootP]++;
    }
}

可以在合并的时候同时统计人数。
连通分量=结点个数-合并次数

题目

A1107 Social Clusters

A1107 Social Clusters
合并的同时统计人数

A1114 Family Property

A1114 Family Property
并查集然后计算一下就好了,要注意使用set存储id并去重,要记得过滤无效id(-1)

A1118 Birds in Forest

A1118 Birds in Forest
合并的同时还要计算连通分量,因为我们事先不知道所有鸟的总数,所以可以累计合并次数。然后根据连通分量=结点个数-合并次数得到答案。

A1034 Head of a Gang

A1034 Head of a Gang

LA3644 化合物爆炸

LA3644 化合物爆炸
并查集的应用,用到connected(),上面的题都没有用到。

值得二刷的题目

  • A1064 Complete Binary Search Tree利用中序遍历建树
  • A1123 Is It a Complete AVL Tree同时复习AVL树和完全二叉树的遍历
  • A1034 Head of a Gang并查集最难的一题了,会做这题够了。
  • A1119 Pre- and Post-order Traversals反正我是看了柳婼的博客

你可能感兴趣的:(PAT)