这篇文章开始讨论有关“树”的一些简单的概念和算法。算法
树是一种基本的数据结构,之因此叫树是由于来自于仿生——树枝分叉的结构或者树根分叉的结构,它很是好的表示出了各个节点之间的逻辑关系,它也是图论当中一个很重要的结构。从它的名字的角度,咱们发现不少科学思惟的生发都是源于对天然的敏锐的观察的,这给科研人员提供了一个很是好的方法。数组
咱们观察天然界的树结构,很容易发现没有哪棵树的两个枝叶长到了一块儿,而抽象化的树结构也是这样,从根节点出发,一条路径越走越深,是不会有回路的,基于这个性质,咱们能够外推树的不少别的特性。数据结构
1.一棵树中任意两个节点有且仅有一条路径连通。(很显然,若是不是这样,便会产生回路)函数
2.一棵树若是有n个节点,那么它刚好有n-1条边。spa
3.在一棵树中加入一条边将会构成一个回路。code
基于树的抽象模型,咱们在生活中各个方面其实都用到了这种结构,好比生物中的遗传系谱图、公司组织构图,书的目录,世界杯足球队的对阵等等,经过这种基本的数据结构,咱们可以将生活不少杂乱的数据变得有条理、有逻辑而造成体系,这即是抽象化的事物给咱们的现实生活带来的遍历,也是咱们科学知识的初衷。blog
二叉树:排序
树结构中一个最基本也是最好用的结构就是二叉树,性质如其名字,每一个节点至多有两个子节点的树结构叫作二叉树。递归
满二叉树:对于高度为h的二叉树,除了第h层之外,1~h-1层的节点的子节点数都达到最大(2个),那么这样的结构成为二叉树。get
彻底二叉树:对于一个二叉树,除去叶节点的剩余节点的子节点数都达到了最大(2个),那么这样的二叉树称为彻底二叉树。
彻底二叉树有一个奇妙的性质,咱们从根节点开始按照“s”型给各个节点标号,会发现,对于第k个节点,它的左儿子的标号是2k , 右儿子的标号是2k + 1,反过来,它的根节点是(int)k/2。基于这条很好的性质,咱们可以将一个彻底二叉树用一个一位数组存储记录。
基于彻底二叉树的堆排序:
首先咱们来给出最小堆的定义:对于一个彻底二叉树,这里咱们在每一个节点中放入权值,并用一个一维数组heap[]来记录,对于任意一个有子节点的节点i,都有heap[2i] > heap[i],heap[2i + 1] > heap[i]。这个定义通俗点来理解,就是说对于任意一个节点的权值都比它的两个子节点的权值小。最大堆也有着相似的定义。
基于最小堆的定义,咱们很容易看到,对于一个长度为n的最小堆,heap[1](也就是堆顶的元素),必定是最小的元素,那么基于此,咱们来想想如何利用这样一个最小堆来完成对数的排序。
step1:取出堆顶元素。
step2:将堆底最后一个元素拿到堆顶,此时显然破坏了最小堆的性质,咱们须要从新构造出一个有n-1个节点的最小堆。
step3:重复step1,循环直到堆变成了空集。
很显然,按照这样的输出顺序便将n个数从小到大进行了排序,这边是所谓的基于彻底二叉树的堆排序过程。
那么咱们如今要解决的一个重要问题即是,如何构造一个这样很是有利于排序的最小堆呢?
从定义出发,咱们尝试将这个大问题给子问题化,显然若是每一个有儿子的节点都知足最小堆的性质,那么这个彻底二叉树即是一个最小堆,所以咱们只须要遍历全部有儿子的节点,使其知足最小堆的性质便可。即找到当前节点及其两个儿子的最小权值,而后利用交换,使得当前的根节点记录这个最小权值便可。
简单的参考代码及注释以下。
#include<cstdio> int h[101]; //记录最小堆的二叉树 int n; void swap(int x , int y) //交换函数 { int t; t = h[x]; h[x] = h[y]; h[y] = t; } void siftdown(int i) //调整第i个节点,与其两个子节点,使其知足最小堆 { int t , flag = 0; while(2*i <= n && flag == 0) { if(h[i] > h[2*i]) t = 2*i; //将根节点i和左儿子比较 else t = i; if(2*i + 1 <= n) //将根节点i和右儿子比较 { if(h[t] > h[2*i + 1]) t = 2*i + 1; } if(t != i) { swap(t , i); i = t; } else flag = 1; } } void creat() //建立最小堆 , 遍历有儿子节点从第n/2往前面,第n/2是最后一个有儿子的节点 { int i; for(i = n/2;i >= 1;--i) siftdown(i); } int deletemin() { int t; //删除堆顶元素,并将堆底元素放到堆顶,从新构建最小堆 , 完成n个整数由小到大的排序 t = h[1]; h[1] = h[n]; n--; siftdown(1); return t; } int main() { int i , num ; scanf("%d",&num); for(i = 1;i <= num;i++) scanf("%d",&h[i]); n = num; creat(); for(i = 1;i <= num;i++) printf("%d ",deletemin()); return 0; }
并查集:
考虑这样一个谜题,如今警方已知n个黑社会,m条线索,每条线索表示A是B的boss,那么请问你这n个嫌疑人中,有多少个帮派,各个帮派的大boss又是谁?
咱们抽象化得来看谜题中给出的各个量之间的关系,n我的视为n个点,而每条线索其实就表征了两个点之间的关系,想象一下,所谓几个帮派是否是就是整个图中造成的不相交集合(这即是所谓并查集的内涵)?而在每一个小集合当中,咱们基于相关的线索,是否也可以构建出一个”有方向“的树结构,好比说,A是B的boss,那么就将A视为B的祖先,那么这棵”帮派树“构造下来,根节点即是这个帮派的大boss。
那么好了,如今总体思路有了,如今咱们面临这样一个问题,给出一条线索以后,咱们如何判断相关的两我的是否属于某个帮派呢?这边是并查集的核心所在了。其实很是相似咱们用一维数组记录二叉树,这里也是利用数组记录树结构,咱们设置f[i]记录vi的祖先,假设当前给出一条线索说,A是B的boss,咱们经过f[]数组来访问A、B所在帮派的大boss,若是相同,说明他们原本就在一个帮派里面了,若是不相同呢?那么须要把B加入到A的帮派当中,为何是B加入到A当中呢?由于A是B的boss嘛,帮派显然也是要从高层往下构建嘛。这里须要注意的是,这个过程当中咱们只关心A、B是否在一个帮派当中,所以咱们须要访问的是A、B两人所在树结构的根节点,也就是说,咱们在构建并查集的时候,对于f[i],咱们须要一直记录vi所在树结构的根节点。
那么如今问题又来了,如何访问vi所在树结构的根节点呢?这里也是并查集算法比较巧妙的一个地方,咱们初始化f[i] = i来表示每一个人都是本身的boss,而后当给出线索代表vj是vi的boss以后,咱们记录,f[i] = j。所以对于访问vi的根节点这件事情,就很好处理了,咱们访问vi的父节点vj,判断f[j]是否等于j,不然访问f[j]的父节点vk……,很显然嘛,这造成了一个递归,直到找到了根节点后返回。
其实上面基于的模型考虑到了边的方向性,其实在不少实际问题的处理中,即便没有边的方向性,并查集也是可以处理的,这里这样描述只是为了更清晰的引入并查集这个算法的过程。例如一开始的谜题,线索仅仅给出A和B是同伙,请你求解这n个黑社会中有几个帮派,就是一种忽略边的方向性的模型。
通过上文的分析,咱们可以简单的代码实现,参考以下。
#include<cstdio> int f[1000] = {0} , n , k , m , sum = 0; void init() { int i; for(i = 1;i <= n;i++) f[i] = i; } int getf(int v) { if(f[v] == v) return v; else { f[v] = getf(f[v]); return f[v]; } } void Merge(int v , int u) { int t1 , t2; t1 = getf(v); t2 = getf(u); if(t1 != t2) { f[t2] = t1; } } int main() { int i , x , y; scanf("%d %d",&n,&m); init(); for(i = 1;i <= m;i++) { scanf("%d %d",&x,&y); Merge(x , y); } for(i = 1;i <= n;i++) { if(f[i] == i) sum++; } printf("%d\n",sum); }
图的最小生成树:Kruskal算法
来考虑这样一个谜题:给出n个城镇和n个城镇之间修建m条道路的费用(每条道路的起点终点都是某个城镇),那么如今为了使n个城镇中任意两个城镇都有路走,咱们修建公路的最小费用是多少?
首先考虑这样一个问题,既然题目的要求仅仅是任意了两个城市有路可走,那么应该想到,咱们修出来的路是不须要构成环的,这很好理解,由于在印个环结构中,去掉任意一条边来破坏这个环结构,构成环的那些点依然是彼此连通的。诶,没有环结构,想想,是否是就是咱们提到树结构的特色呢?而咱们要找的就是原图(n个点、m条边)的一个子图,也就叫作生成树,结合权值之和最小,这即是所谓的“最小生成树”。
Kruskal算法给出了这样一个贪心算法:
step1:将m条边的权值由小到大进行排序。
step2:从最小的边开始想只有点的树结构中添加边,并删除该边,前提是添加该边不会使树结构出现环。
step3:很明显,n个节点造成的树结构的边数是n-1,所以该步骤是重复step2,直到当前树结构中有n-1条边。
Kruskal算法的正确性是不言自明的,就像上帝给的你一道乍现的灵光,让你感受一切的文字解释都显得累赘而丑陋。所以这里须要用到那个经典的词语,显然。
可是如今咱们面临一个重要的问题,整个过程一个核心步骤是判断是否有环出现(由于整个算法过程一直在回避圈,由于这个算法也成为“避圏法”),如何实现呢?结合咱们刚刚介绍过的并查集,能够看大,当前咱们想要添加ei,链接着vi和vj,若是咱们利用并查集的方法查一下vi和vj的根节点,若是相同,显然代表添加了ei会造成环;若是不相同,就能够放心大胆的将当前权值最小的边添加到正在构造的树结构里啦。
下面有简单的参考代码。
#include<cstdio> #include<algorithm> using namespace std; struct edge { int u; int v; int w; }; bool cmp(edge a , edge b) { return a.w < b.w; } struct edge e[10]; int n , m; int f[7]={0},sum = 0 , cnt = 0; int getf(int v) { if(f[v] == v) return v; else { f[v] = getf(f[v]); return f[v]; } } int Merge(int v , int u) { int t1 , t2; t1 = getf(v); t2 = getf(u); if(t1 != t2) { f[t2] = t1; return 1; } return 0; } int main() { int i; scanf("%d %d",&n,&m); for(i = 1;i <= m;i++) scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].w); sort(e + 1,e + m + 1,cmp);//给m个边的权值排序 for(i = 1;i <= n;i++)//并查集父节点初始化 f[i] = i; for(i = 1;i <= m;i++)//图的最小生成树:Kruskal算法 { if(Merge(e[i].u , e[i].v))//若是连通,则删除此边,不然选择该边 { cnt++; sum = sum + e[i].w; } if(cnt == n - 1) break; } printf("%d",sum); }