数据结构之——拓补排序和并查集

1.拓补排序

1.有向无环图
如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(Airected Acyclic Graph,ADG)。

AOV网(Activity On Vertex,AOV):指用顶点表示活动,而用边集表示活动间优先关系的有向图,例如下图中数学先导课程示意图就是AOV网,其中图的顶点表示各项课程,也就是“活动”;有向边表示课程的先导关系,也就是“活动间的优先关系”。显然,图中不应该存在有向环,否则会让优先关系出现逻辑错误,任何活动不能以它自己作为自己的前驱和后继。

2.拓补排序

拓补排序是将有向无环图G的所有顶点排成一个线性序列,使得对图G中的任意两个顶点u、v,如果存在边u->v,那么在序列中u一定在v前面。这个序列称为拓补排序。

以下图数学专业的某几门课程学习先后顺序为例,可以获知, “数学分析”是“复变函数”、“常微分方程”、“计算方法”的先导课程,“复变函数”是“实变函数”和“泛函函数”的先导课程,“实变函数”又是“泛函分析”的先导课程。显然,对一门课程来说,必须要先学习它的先导课程才能很好地学习这门课程,而且课程之间不能够形成环(如果“泛函分析”同时又是“空间解析几何”的先导课程,就乱套了)。

在这里插入图片描述

同时发现,如果课程之间没有直接或间接的先导关系,那么这两门学习的先后顺序是任意的,(例如“复变函数”与“计算方法”的学习顺序就是任意的)。于是可以把上面的课程排成一个学习的先后序列,使得这个序列中的课程顺序满足下图中的课程顺序。

在这里插入图片描述

拓补排序步骤如下:
(1)定义一个队列Q,并把所有入度为0的结点加入队列。
(2)取队首结点,输出,然后删去所有从它出发的边,并令这些边到达的顶点的入度减1,如果某个顶点的入度减为 0,则将其加入队列。
(3)反复进行(2)操作,直到队列为空,如果队列为空时入过队列的结点数目恰好为N,说明拓补排序成功,图G为有向无环图;否则,拓补排序失败,图G中有环。

由于需要记录结点的入度,因此需要额外建立一个数组inDegree[MAXV],并在程序一开始读入图时就记录好每个结点的入度。

拓补排序的代码如下:

vector<int> G[MAXV];   //邻接表
int n, m, inDgree[MAXV];   // 顶点数、入度

//拓补排序
bool topologicalSort(){
	int num = 0;  //记录加入拓补排序的顶点数
	
	queue<int> q;
	for(int i=0; i<n; i++){
		if(inDegree[i] == 0){
			q.push(i);     //将所有入度为0的顶点入队 
		} 
	}  
	
	while(!q.empty()){
		int u = q.front();  //取队首顶点
		q.pop();
		for(int i=0; i<n; i++){
			int v = G[u][i];   //u的后继结点v
			inDegree[v]--;     //顶点v的入度减1
			if(inDegree[v] == 0){
				q.push(v);
			} 
		} 
		
		G[u].clear();   //清空顶点u的所有出边
		num++;    //加入拓补排序的顶点数加1 
	} 
	if(num == n) return true;  //加入拓补排序的顶点数为n,说明拓补排序成功
	else return false;   //加入拓补排序的顶点数小于n,说明拓补排序失败 
} 

拓补排序很重要的应用就是判断一个给定的图是否是有向无环图,正如上面的代码,如果topologicalSort()函数返回true,则说明拓补排序成功,给定的图是有向无环图;否则,说明拓补排序失败,给定的图中有环。如果要求有多个入度为0的顶点,选择编号最小的顶点,那么把queue改成priority_queue,并保持队首元素(堆顶元素)是优先队列中最小的元素。

2.并查集

1.定义

并查集是一种维护集合的数据结构,它的名字中“并”、“查”、“集”分别取自Union(合并)、Find(查找)、Set(集合)这3个单词。并查集对于kuskal算法中如何判断测试边的两个端点是否在不同连通块中和如何将测试边加入最小生成树中有非常巧妙的运用。

在这里插入图片描述

并查集是用数组实现的,int father[N],其中father[i]表示元素i的父亲结点,而父亲结点本身也是这个集合内的元素(1 <= i <=N)。例如father[1] = 2就表示元素1的父亲结点是元素2,以这种父系关系来表示元素所属的集合。如果father[i] == i,说明元素i是该集合的根节点,但对同一个集合来说只存在一个根节点,且将作为所属集合的标识。

2.并查集的基本操作

并查集的使用需要先初始化father[]数组,然后再根据需要进行查找操作。

(1)初始化
一开始,每个元素都是独立的一个集合,因此需要令所有father[i]等于i:

for(int i=1; i<=n; i++){
	father[i] = i;
}

(2)查找
由于规定同一个集合中只存在一个根节点,因此查找操作就是对给定的结点寻找其根节点的过程。实现的方式可以是递归或是递推,思路都是一样的,反复寻找父亲结点,直到找到根节点(即father[i] == i的结点)。
递推代码如下:

//findFather函数返回元素x所在集合的根结点
int findFather(int x){
	while(x != father[x]){  //如果不是根结点,继续循环 
		x = father[x];//获得自己的父亲结点 
	}
	return x;
} 

递归代码如下:

inf findFather(int x){
	if(x == father[x])  return x;  //找到根节点,返回根节点编号
	else  return findFather(father[x]);  //否则,递归判断x的父亲结点是否是根节点 
}

(3)合并

合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在集合合并。具体实现上一般是先判断两个元素是否属于同一个集合,只有当两个元素属于不同集合时才合并,而合并的过程一般是把其中一个集合的根节点的父亲结点指向另一个集合的根节点。

在这里插入图片描述

思路如下:

  1. 对于给定的两个元素a、b,判断它们是否属于同一集合。可以调用上面的查找函数,对这两个元素a、b分别查找根节点,然后再判断其根节点是否相同。
  2. 合并两个集合:在第一步中已经获得两个元素的根节点faA和faB,因此只需要把其中一个父亲节点指向另一个结点,例如可以令father[faA] = faB。

合并代码如下:

void Union(int a, int b){
	int faA = findFather(a);   // 查找a的根结点,记为faA
	int faB = findFather(b);   // 查找b的根结点,记为faB
	if(faA != faB){
		father[faA] = faB;  //合并它们 
	} 
}

3.路径压缩

上面的并查集查找函数没有优化,如果题目给出的元素数量很多且形成一条链,那么这个查找函数的效率就会非常低。如下图所示,总共有105个元素形成一条链,那么假设要进行105查询,且每次查询都查询最后面的结点的根节点,那么每次都要花费105的计算量查找,这显然无法承受。

在这里插入图片描述

由于findFather函数的目的就是查找根节点,例如下面这个例子:

father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 3;

因此,如果只是为了查找根结点,那么完全可以想办法把操作等价地变成:

father[1] = 1;
father[2] = 1;
father[3] = 1;
father[4] = 1;

对应的图形变化如下:

在这里插入图片描述

这样相当于把当前查询结点的路径上的所有结点的父亲都指向根节点,查找的时候就不需要一直回溯去找父亲结点了 ,查询的复杂度为O(1)。

转换过程步骤如下:
(1)按原先的写法获得x的根节点r。
(2)重新从x开始走一遍寻找根节点的过程,把路径上经过的所有结点的父亲结点全部改成根节点r。
代码如下:

int findFather(int x){
	//由于x在下面的while中会变成根节点,因此先把原先的x保存一下
	int a = x;
	while(x != father[x]){// 寻找根节点
	    x = father[x]; 
	} 
	//到这里,x存放的是根节点。下面把路径上的所有结点的father都改成根节点
	while(a != father[a]) {
		int z = a;  //因为a要被father[a]覆盖,所以先保存a的值,已修改father[a] 
		a = father[a];  //a回溯父亲结点 
		father[z] = x;  //将原先的结点a的父亲改成根节点 
	}
	return x; //返回根节点 
}

递归代码如下:

int findFathe(int v){
	if(v == father[v])  return v;//找到根节点
	else{
		int F = findFather(father[v]);  //递归寻找father[v]的根节点F
		int father[v] = F;  //将根节点F赋给father[v]
		return F;   //返回根结点F 
	} 
}

猜你喜欢

转载自blog.csdn.net/xiaoyong5854/article/details/106582651