关于树结构的非线性表编程在数据结构中可以说占据了半壁*,其中涉及的知识点繁杂,但也是数据结构体现运算优化的核心所在,下面我们将较为初步且系统得讨论数据结构中一系列有关树的表示。
首先我们再次明确树的形式化概念:
树是n个节点的有限集合,这个集合满足以下的条件:
1) 有且仅有一个节点没有前件。
2) 除根外,其他的所有节点都有且仅有一个前件。
3) 除去根以外,其他每个节点都通过唯一的路径连接根上。每个节点的前件称为该节点的父节点,后件称为该节点的子节点。
这篇文章主要包含如下4个知识点:
(1) 用树的遍历求解层次性问题。
(2) 用树结构支持并查集。
(3) 用树状数组同济子树权和。
(4) 用四叉树求解二维空间问题。
首先我们讨论树在计算机中的表示,这不仅仅在处理一些数据结构的题目中很重要,在一些树形动态规划的问题上,我们首先要完成的也是树的表示。
方法1:双亲表示法。
在高级语言中我们容易操作和定义的是线性结构,即数组这样的数据结构,因此我们考虑非线性结构的程序语言表示的时候,基本原则就是将其向线性数据结构转化。给出一个树结构,根据树的定义我们知道,除了根节点,每个节点有且仅有一个父节点。因此我们定义双亲表示数组f[].其中f[i]表示节点i的父节点的节点序号。
方法2:多重链表法
这里我们常常用到c++语言stl库中的vector类。对于给定的一棵树,我们利用”vector<int> tree[n]”这样一条语句,然后定义tree[i].push_back(j)表示j作为i的一个子节点。通俗来说,这里我们将树结构扩成了一个二维数组,而利用vector类则是为了更好的节约空间资源。
一般题目中,输入的树结构往往是对于给定的n个节点的树结构,输入n-1个有序节点对用于表示父子关系,这时候往往用方法2表示树是比较常见的,而往往在一般题目中,需要多种表示方法并用,因为各个树结构的表示方式都有各自的优点(访问树自身某方面的信息时比较快)。
用树的遍历求解层次性问题:
最近公共祖先问题:
给出一个n节点的树结构的n-1对有序顶点对<x , y>,表示树结构中的一条边且x是父节点,y是子节点。最后输入一对节点序号<a, b>,编写程序
最近公共祖先代码如下:
//poj 1330.
#include<cstdio> #include<cstring> #include<vector> using namespace std; const int N = ; vector<int > a[N]; int f[N] , r[N]; void DFS(int u , int dep)//从dep层的u节点处罚,先序遍历计算每个节点的层次 { r[u] = dep; for(vector<int>::iterator it = a[u].begin();it != a[u].end();++it){ DFS(*it , dep + ); } } int main(){ int casenum , num , n , i , x , y; scanf("%d" , &casenum); for(num = ;num < casenum;num++){ scanf("%d" , &n); for(i = ;i < n;i++) a[i].clear(); memset(f , - , sizeof(f)); for(i = ;i < n - ;i++){//n个节点的树 , n-1条边 scanf("%d %d" , &x , &y);//树种的一个边<x , y>,x是父节点,y是子节点; x--;y--; a[x].push_back(y); f[y] = x; } for(i = ;f[i] >= ;i++); DFS(i , ); scanf("%d %d" , &x , &y); x--;y--; while(x != y){ if(r[x] > r[y]) x = f[x]; else y = f[y]; } printf("%d\n" , x + ); } }
用树结构支持并查集:
在一般的数据结构的教材中,集合与图、树一样,都是群聚类的非线性表,但是集合更加侧重于包含关系而忽略一个集合中各个元素之间的前后件关系。在一些问题中,我们需要将n个元素划分成若干组,每个组视为一个集合,通常需要涉及集合的合并与查找,因此我们称其为并查集。
并查集需要支持如下的操作:
(1) Make_set(x):加入单个元素x到集合S中。
(2) Join(x,y),把x、y所在的不同集合进行合并。
(3) Set_find(x):得到x所在集合S的代表元。
下面我们来讨论并查集的存储结构。理论上来说,并查集有链结构和树结构两种,但是树结构在完成各个操作时的效率更加优良,我们便直接介绍树结构。
我们用一棵树结构表示集合S={s1,s2,s3,…,sn}.由于我们仅仅是用树结构来表示集合,因此这棵树中的边关系,仅仅是体现了合并操作的前后顺序,边关系不同时,所表达的集合是完全等价的。紧接着,由于树结构依然不能直接存储,我们还要想办法将其转化成线性存储结构。我们如何来表征这样一个集合的特征呢?我们选择一个集合的代表元(也就是树结构表示的并查集的根节点),我们定义set[x]表示元素x所在集合的代表元,而如果x是集合S的代表元,则令set[x] = -1(这种表示方法可以理解为并查集 -> 树结构表示 -> 双亲表示法).那么基于这样的定义,我们能够看到,set[x]与 set[y]的相等关系便可以作为两个元素x、y是否在同一集合的指标了。
查找过程:
对于给出的元素x,我们想要找到x所在集合的代表元,也可以说成是x所在树结构的根节点。直接访问set[x]我们发现存在这样一个问题,x的根节点y在某一次合并操作之后合并到了一个更大的集合,原本是根节点的y变成了树结构中的分支节点,因此我们需要沿树结构继续向上找,我们会遇到相同的问题,因此需要设计一个递归机制。同时为了以后访问的方便,我们采用路径压缩的手法,来使得从x出发找到根节点r的路径经过的所有节点i的set[i]都变成r.
int set_find(int p){ if(set[p] < ) return p; //找到根节点/集合代表元 return set[p] = set_find(set[p]); }
合并过程:
合并过程就显得很简单,假设我们想要合并元素x、y所在集合,我们只需要分别找到x、y所在集合的代表元p , q,使set[p] = q即可,当然yekeyi 交换p、q的位置。即从x、y所在集合的代表元中选择任意一个成为新的集合的代表元。
void join(int x , y){ p = set_find(p); q = set_find(q) if(p != q) set[q] = p; }