有向无环图之拓扑排序——贪心算法

拓扑排序主要可以判断有向无环图(DAG)中是否存在环路,其可以用来判断一个有着先后关系的工程能否顺利进行。
1. 有向无环图(DAG)

一个不存在环路的有向图称作有向无环图(Directed Acycline Graph),简称DAG图。
判断一个图是否存在环路可以有以下两种思路:

  • 对于无向图,若深度优先遍历过程中遇到回边(即指向已经访问过的顶点的边),则必存在环路;
  • 对于有向图,可以构造其顶点的拓扑排序序列,若图中所有顶点都在它的拓扑排序序列中,则必不存在环路;
2. 拓扑排序
2.1 拓扑排序的概念

拓扑排序(Topological Sort) 即由一个集合上的一个偏序得到该集合上的一个全序。
(偏序是指集合中仅有部分成员之间可比较,而全序指集合中全体成员之间均可比较)

2.2.拓扑排序的基本思想

拓扑排序的算法如下:

  • 1)在有向图中选择一个没有前驱(入度为0)的顶点输出;
  • 2)从图中删除该顶点和所有以它为尾的弧;
  • 3)重复(1)(2),直至全部顶点均已输出,或当前图不存在无前驱的顶点为止。
    后一种情况表示有向图中存在环。
3.拓扑排序的实现
3.1 有向图的邻接表实现

若采用邻接表作有向图的存储结构,则可以在头结点数组中增加一个存放顶点入度的数组,入度为0的顶点即没有前驱的顶点,删除顶点以以它为尾的操作可以等价于以弧头当点的入度减1来实现。
同时,为了避免重复检测入度为0的顶点,可以另设一个栈来存储所有入度为0的顶点.

3.1.1 图的邻接表存储

邻接表定义

typedef struct node {
	//边节点
	int adjvex;  // 顶点下标
	struct node* nextarc; // 指向下一顶点
}node;
typedef struct headNode{
	// 表头节点
	int indegree; // 入度
	string info; // 顶点的信息,如名字
	node* firstarc; // 第一邻接点
}headNode,Adjlist[MAX_VERTEX_NUM];
// 邻接表
typedef struct {
	Adjlist vertex;   // 头节点表
	int vexnum, arcnum;  // 点的数目和边的数目
}ALGraph;

创建邻接表

void createALGraph(ALGraph &G) {// 创建邻接表
	printf("输入顶点数目和边的数目:\n");
	cin >> G.vexnum >> G.arcnum;
	printf("输入顶点信息:\n");
	for (int i = 0; i < G.vexnum; i++) {
		cin >> G.vertex[i].info;
		G.vertex[i].indegree = 0;
		G.vertex[i].firstarc = NULL;
	}
	printf("输入边的信息,以V1->V2的形式:\n");
	for (int i = 0; i < G.arcnum; i++) {
		string v1, v2;
		cin >> v1 >> v2;
		int index1 = Locate(G, v1);
		int index2 = Locate(G, v2);
		G.vertex[index2].indegree += 1;
		// 使用头插法插入新节点
		node* temp = new node;
		temp->adjvex = index2;
		temp->nextarc = G.vertex[index1].firstarc;
		G.vertex[index1].firstarc = temp;
	}
} 

找邻接表中某节点的下标

int Locate(ALGraph G, string v) {
	int i = 0;
	for (; i < G.vexnum && G.vertex[i].info != v; i++);
	if (i == G.vexnum) return -1;
	else return i;
}
3.1.2 邻接表的拓扑排序
int TopoSort(ALGraph G) {
	// 对各个顶点求入度 indegree[0..vexnum-1] 
	// 若初始化图的时候完成,则可省略
	//FindInDegree(G, indegree);
	stack<int> s; // 入度为0的节点的下标
	for (int i = 0; i < G.vexnum; i++) {
		if (G.vertex[i].indegree == 0) s.push(i);
	}
	// 打印不为0的节点
	int count = 0;// 当前已经打印的节点
	while (!s.empty()) {
		int j = s.top(); s.pop();
		//printf("当前找到的顶点%d ", j);
		count++;
		//cout << G.vertex[j].info << "-->" << endl;
		printf("%s-->",G.vertex[j].info.c_str());
		// 删除以该节点为尾的弧
		node* temp = G.vertex[j].firstarc;
		while (temp) {
			int index = temp->adjvex;
			G.vertex[index].indegree--;
			if (G.vertex[index].indegree == 0) s.push(index);
			temp = temp->nextarc;
		}
	}
	if (count < G.vexnum) return 0; // 有环路
	else return 1; // 无环路
}
3.2 有向图的邻接矩阵实现
3.2.1 图的邻接矩阵存储

邻接矩阵的定义

#define MAX_VERTEX_NUM 20 // 最大的顶点数目
// 定义邻接矩阵
typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //0 or 1表示是否邻接
typedef struct {
	AdjMatrix matrix;
	string vexs[MAX_VERTEX_NUM]; // 顶点向量
	int indegree[MAX_VERTEX_NUM]; // 顶点的入度
	int vexnum, arcnum; // 顶点数和边的数目
}MGraph;

邻接矩阵的创建

void createMGraph(MGraph &G) {
	int i, j;
	printf("输入顶点数和边的数目:\n");
	cin >> G.vexnum >> G.arcnum;
	// 初始化邻接矩阵
	for (i = 0; i < G.vexnum; i++) {
		G.indegree[i] = 0;
		for (j = 0; j < G.vexnum; j++) G.matrix[i][j] = 0;
	}
	printf("输入顶点信息\n");
	for (i = 0; i < G.vexnum; i++) cin >> G.vexs[i];
	printf("输入边的信息Vi-->Vj\n");
	for (i = 0; i < G.arcnum; i++) {
		string v1, v2;
		cin >> v1 >> v2;
		int l1 = LocateVex(G, v1);
		int l2 = LocateVex(G, v2);
		G.matrix[l1][l2] = 1;
		G.indegree[l2]++;
	}
}

顶点的定位

int LocateVex(MGraph G, string u) {
	int i;
	for (i = 0; i < G.vexnum && G.vexs[i] != u; i++);
	if (i == G.vexnum) return -1;
	else return i;
}
3.2.2 邻接矩阵的拓扑排序
int TopoSort(MGraph G) {
	int i, j;
	stack<int> s;
	for (i = 0; i < G.vexnum; i++) {
		if (G.indegree[i] == 0) s.push(i);
	}
	int count = 0; // 已排序的顶点
	while (!s.empty()) {
		j = s.top(); s.pop();
		count++;
		cout << G.vexs[j] << "-->";
		for (int k = 0; k < G.vexnum; k++) {
			if (G.matrix[j][k] == 1) {
				if (!(--G.indegree[k])) s.push(k); // 删除弧
			}
		}
	}
	if (count == G.vexnum) return 0;
	else return 1;
}
3.3 测试用例
6 8
V1 V2 V3 V4 V5 V6 
V1 V2
V1 V4
V1 V3
V3 V2
V3 V5
V4 V5
V6 V4
V6 V5

有向无环图示例

3.4 基于深度优先的拓扑排序

当有向图中无环的时,可以利用深度优先遍历进行拓扑排序,即从某点出发进行深度优先遍历的时候,
最先退出深度优先遍历的顶点即出度为0的顶点,是拓扑排序序列中最后一个顶点
因此,按照退出DFS的先后顺序记录下顶点序列,再逆序输出即可。

注意:这里有个小技巧,就是我们怎么在用DFS进行拓扑排序的时候考虑图中是否有环?

  • 我们可以用一个数组vis[],0表示未访问过,1表示已经访问过且其子节点也已经访问过
  • 然后我们用-1表示当前正在访问或者其子节点还未访问完,若
  • 某结点在DFS时回访到vis[]=-1的结点,则表示其又访问到了其祖先结点,表示存在环路。
#include <iostream>
#include <string>
#include<stack>
using namespace std;

#define MAX_VERTEX_NUM 20 // 最大的顶点数目
// 定义邻接矩阵
typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //0 or 1表示是否邻接
typedef struct {
	AdjMatrix matrix;
	string vexs[MAX_VERTEX_NUM]; // 顶点向量
	int indegree[MAX_VERTEX_NUM]; // 顶点的入度
	int vexnum, arcnum; // 顶点数和边的数目
}MGraph;

//图的创建
void createMGraph(MGraph &G);
// 顶点的定位
int LocateVex(MGraph G, string v);
// 返回图G中u的第一个邻接点
int First(MGraph g, int u);
// 返回图G中V相对于u的下一邻接点
int Next(MGraph G, int v, int u);
// 拓扑排序
bool DFS_Master(MGraph G);
bool DFS(MGraph G, int v, int* visited, stack<int> &s);
int main() {
	MGraph G;
	createMGraph(G);
	DFS_Master(G);
	system("pause");
	return 0;
}
bool DFS_Master(MGraph G) {
	// 初始化访问节点
	int i, j;
	int* visited = new int[G.vexnum];
	// 逆序节点
	stack<int> s;
	for (i = 0; i < G.vexnum; i++) visited[i] = 0;
	for (i = 0; i < G.vexnum; i++) {
		if (visited[i] == 0) {
			if(!DFS(G, i, visited, s)) return false;
		}
	}
	while (!s.empty()) {
		j = s.top(); s.pop();
		cout << G.vexs[j] << "-->";
	}
	delete[] visited;
	return true;
}
bool DFS(MGraph G, int v, int* visited, stack<int> &s) {
	visited[v] = -1; // 表示正在访问链中
	int i;
	for (i = First(G, v); i != -1; i = Next(G, v, i)) {
		if (visited[i] < 0) return false; // 有环
		else if(visited[i] == 0 && !DFS(G, i, visited, s)) return false;
	}
	// 如果v没有邻接点i,那么它就会退出
	visited[v] = 1; 
	s.push(v);
	return true;
}
void createMGraph(MGraph &G) {
	int i, j;
	printf("输入顶点数和边的数目:\n");
	cin >> G.vexnum >> G.arcnum;
	// 初始化邻接矩阵
	for (i = 0; i < G.vexnum; i++) {
		G.indegree[i] = 0;
		for (j = 0; j < G.vexnum; j++) G.matrix[i][j] = 0;
	}
	printf("输入顶点信息\n");
	for (i = 0; i < G.vexnum; i++) cin >> G.vexs[i];
	printf("输入边的信息Vi-->Vj\n");
	for (i = 0; i < G.arcnum; i++) {
		string v1, v2;
		cin >> v1 >> v2;
		int l1 = LocateVex(G, v1);
		int l2 = LocateVex(G, v2);
		G.matrix[l1][l2] = 1;
		G.indegree[l2]++;
	}
}

int LocateVex(MGraph G, string u) {
	int i;
	for (i = 0; i < G.vexnum && G.vexs[i] != u; i++);
	if (i == G.vexnum) return -1;
	else return i;
}

int First(MGraph G, int u) {
	int i;
	for (i = 0; i < G.vexnum; i++) {
		if (G.matrix[u][i] != 0) break;
	}
	if (i == G.vexnum) return -1;  // 没有临界点
	else return i;
}

int Next(MGraph G, int v, int u) {
	int index;
	for (index = u + 1; index < G.vexnum && G.matrix[v][index] == 0; index++);
	if (index == G.vexnum) return -1;
	else return index;
}
参考资料

《数据结构 C语言版》 严蔚敏著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值