基础实验4-2.1 树的同构
给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的,因为我们把其中一棵树的结点A、B、G的左右孩子互换后,就得到另外一棵树。而图2就不是同构的。
图1(同构)图2(非同构)
算法分析
对于二叉树的算法,在我做题的过程中总结出了两个很重要的思想。
- 树本身的递归定义引出的退化分析,泛化验证法。
- “哨兵”思想。
退化分析,泛化验证
由于树本身是递归定义的,那么假设问题所要求的算法在复杂的树 $T$ 上成立,那么对于 $T$ 的每一棵子树,也一定是成立的。再通俗一点,对于任何一棵符合题目的条件的树,一定也是成立的,哪怕那棵树简化地不能再简化了。
下面用这个题目作为例子,详细叙述一下怎么分析与验证。
分析
首先我们画一棵极端退化的树,来研究题目中的同构条件。退化到什么情况呢,如下图,树中只有两个节点。
很明显,在不考虑节点的数据域的时候,两个节点的二叉树只有上图两种情况。对于同构的情况也只有两种。
第一种情况
对于图左的二叉树 $T_1$ , 它自己与它自己一定是同构的,同样,对于图右的二叉树 $T_2$ ,也与自己同构。根据二叉树的递归性质,它的所有子树也一定与自己同构。
第二种情况
还有一种情况,我们发现,$T_1$ 与 $T_2$ 也是同构的。即 $T_1$ 的左子树与 $T_2$ 的右子树同构,$T_1$ 的右子树(null)与 $T_2$ 的左子树(null)同构。
通过两个节点的二叉树的分析,我们发现了同构的条件。即:
- 两棵树 $T_1$ 和 $T_2$ 当前节点的数据域相等。
- $T_1$ 的左子树与 $T_2$ 的左子树同构,$T_1$ 右子树与 $T_2$ 右子树同构。
- $T_1$ 的左子树与 $T_2$ 的右子树同构,$T_1$ 右子树与 $T_2$ 左子树同构。
只要满足条件 $1\&\&2$ 或者 $1\&\&3$ 则两棵树就是同构的。
因此。通过简化二叉树的形态,我们可以直观又快速地分析出一个行而有效的算法(先不论性能如何)。
泛化验证
然后可以把我们得出的算法带入更复杂的二叉树进行验证,看等否得到反例,最近在忙着复习考研,时间有限这一步就留给大家自己试试。
哨兵思想
上一步还有一个小细节不知道大家有没有注意到,就是第二种情况的时候,如果有子树为空,我们并不去判空,我们只默认,空子树也是同构的,即 null 同构于 null。
这一思想给我们的代码实现带来了极大的方便。我们不需要去判断是不是叶结点,也不用去纠结递归的时候当前节点没有某棵子树要不要特殊处理的情况,而是用了一个逻辑上的“哨兵”来进行判断。
“哨兵”这个概念在很多经典算法中都有使用。例如快速排序,KMP算法等。活用哨兵,有助于简化我们递归算法的实现。
输入处理
此外,这道题还有一点小陷阱,就是输入数据并不是按前中后序或是层次遍历的顺序给出的。应该是随机给出的。我们需要在输入数据中找出根节点。
因为是树形结构,我们知道,树是一种半线性结构,每个节点有 $1$ 到 $n$ 个后继,但每个节点至多只有 $1$ 个前驱。即除了根节点,每个节点的入度都是 $1$。换句话说,除了根节点,每个节点都有指向它的指针或者说引用。于是我们可以通过统计入度或者指向节点的引用的方式来判断哪个是根节点。唯一没有被指向的就是根节点。
代码
#include <stdio.h>
typedef struct Node{
char c;
int lchild;
int rchild;
}BTNode;
int input_tree(int n, BTNode node[]);
/*
@params node1 树1, node2 树2, i, 树1当前节点的下标, j 树2当前节点下标
@return 同构返回1, 否则返回0
*/
int is_same(BTNode node1[], BTNode node2[], int i, int j);
int main(){
BTNode node1[11]={0}, node2[11]={0};
int n;
scanf("%d", &n);
int root1 = input_tree(n, node1); //读入树,返回树根的下标
scanf("%d", &n);
int root2 = input_tree(n, node2);
int ans = is_same(node1, node2, root1, root2); //判断node1与node2是否同构
if(ans == 1) printf("Yes\n");
else printf("No\n");
return 0;
}
int input_tree(int n, BTNode node[]){
char ch, lc, rc;
int a[11]={0};
for(int i=0; i<n; i++){
scanf(" %c %c %c", &ch, &lc, &rc);
node[i].c = ch;
node[i].lchild = -1;
node[i].rchild = -1;
if (lc != '-'){
node[i].lchild = lc - 48; // 数字的ascii码减去48便是数字的值
a[lc-48] = 1;
}
if (rc != '-'){
node[i].rchild = rc - 48;
a[rc-48] = 1;
}
}
int root = -1;
for(int i=0; i<n; i++){ // a数组为0的下标即根节点的下标
if(a[i]==0){
root = i;
break;
}
}
return root;
}
int is_same(BTNode node1[], BTNode node2[], int i, int j){
if ((i == -1 && j != -1) || (i == -1 && j != -1)) return 0; //一边null一边非null
if(i==-1 && j==-1) return 1; //当前节点都是null
if (node1[i].c != node2[j].c) return 0; //字符不相等
int ll = is_same(node1, node2, node1[i].lchild, node2[j].lchild); //左与左同构
int rr = is_same(node1, node2, node1[i].rchild, node2[j].rchild); //右与右同构
int lr = is_same(node1, node2, node1[i].lchild, node2[j].rchild); //左与右同构
int rl = is_same(node1, node2, node1[i].rchild, node2[j].lchild); //右与左同构
if((ll==1 && rr==1) || (lr==1 && rl==1)) return 1; // 左左右右同构或者左右右左同构,则子树同构
else return 0;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。