这种题一般给出每个结点的子结点,如果字段比较多可以使用结构体,然后使用一个指针向量来存储每个结点的子结点,当然还可以根据需要建立父结点的指针字段。广义树常用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
水题,就是考BFS。
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。
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);
}
}
}
}
理解来记忆,是这样的:
A1086 Tree Traversals Again
这题主要是通过栈给出树的先序遍历和后序遍历,当然我们也可以直接通过栈的入栈出栈情况直接建树,我用了这一方法。
A1102 Invert a Binary Tree
找出根结点遍历即可,对于反转,我们只需要在读取的时候交换左右即可。
A1064 Complete Binary Search Tree
这题很巧妙,利用了中序遍历的特点,可以避开完全二叉树这个问题。
A1099 Build A Binary Search Tree
和上面一题一样,要利用中序遍历的特点,填充各个结点的值,然后使用队列层次遍历即可。
A1127 ZigZagging on a Tree
这道题如果层次遍历的时候交替正反入队会很麻烦(因为反向的时候需要先加入右子结点,而且下一层又需要反向遍历子结点,否则出错)而且容易出错,所以我们可以采取正向入队,然后交替反向输出即可。
A1138 Postorder Traversal
这道题有点意思,要认真思考不同遍历顺序之间的联系,从而由前序和中序得到后序的第一个结点。
A1119 Pre- and Post-order Traversals
这道题要考虑后序的前一个结点在中序中的位置,如果他在中序中的位置的左边紧接着就是根结点,那么我们就无法区分它是左子树还是右子树,那么答案不唯一,否则唯一。
看来姥姥很喜欢把最近一年的题目改改就做新题,A1143考的是广义树的LCA,因为是广义树所以没有二叉树的规律可以利用,所以使用的是先找出一个结点的路径再找第二个的路径的同时更新LCA,而A1151因为有二叉树的规律,可以直接利用。这两题实际还是考察搜索算法,主要是DFS。
A1143 Lowest Common Ancestor
A1151 LCA in a Binary Tree
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
合并的同时统计人数
A1114 Family Property
并查集然后计算一下就好了,要注意使用set存储id并去重,要记得过滤无效id(-1)。
A1118 Birds in Forest
合并的同时还要计算连通分量,因为我们事先不知道所有鸟的总数,所以可以累计合并次数。然后根据连通分量=结点个数-合并次数得到答案。
A1034 Head of a Gang
LA3644 化合物爆炸
并查集的应用,用到connected(),上面的题都没有用到。