算法系列笔记6(有关图的算法一—搜索,拓扑排序和强连通分支)

简单概念:对于图G(V,E),通常有两种存储的数据结构,一种是邻接矩阵,此时所需要的存储空间为O(V^2);第二种是邻接表,所需要的存储空间为O(V+E)。邻接表表示法存在很强的适应性,但是也有潜在的不足,当要快速的确定图中边(u,v)是否存在,只能在顶点u的邻接表中搜索v,没有更快的方法,此时就可以使用邻接矩阵,但要以占用更多的存储空间作为代价;此外当图不是加权的,采用邻接矩阵存储还有一个优势:在存储邻接矩阵的每个元素时,可以只用一个二进位,而不必用一个字的空间。

图的搜索算法

搜索一个图示有序地沿着图的边访问所有的顶点,主要有两种搜索算法,广度优先遍历(bfs,也称为宽度遍历)和深度优先遍历(dfs)。

广度优先(bfs)

从源点s对图进行广度优先遍历,得到的路径为从源点s到其它各点的最短路径,也生成了一棵广度优先树。广度优先遍历需要一个队列,先进先出。

代码如下:

// 广度遍历图
void Graph::bfs(int s){
	queue<int> q;
	q.push(s);
	visited[s] = 1;
	while(!q.empty()){
		int u = q.front();
		q.pop();
		cout << u <<" ";
		GNode *p = edges[u].next;
		while(p != NULL){
			if(!visited[p->val]){    // 未被访问,则将其加入队列中并标志为访问过
				q.push(p->val);
				visited[p->val] = 1;
			}
			p = p->next;
		}
	}

}

void Graph::bfsTravel(){
	memset(visited, 0, sizeof(int)*vertexNum);
	for(int i = 0; i < vertexNum; i++){
		if(!visited[i]){
			bfs(i);
			cout << endl;
		}
	}
}

时间复杂度为O(V+E)

深度优先(dfs)

深度优先搜素形成了一个由数棵深度优先树所组成的深度优先森林,每条边被称为树边。此外深度遍历对于每个节点会有个时间戳,用于标识该结点开始访问和结束访问的时间。一个重要的特性就是发现和完成时间具有括号结构。

代码如下:

// 深度优先遍历
void Graph::dfs(int s){
	visited[s] = 1;
	time += 1;
	beginTime[s] = time;
	cout << s << "(" << beginTime[s] << " ";              // shen
	GNode *p = edges[s].next;
	while(p != NULL){
		if(!visited[p->val])
			dfs(p->val);
		p = p->next;
	}
	time += 1;
	endTime[s] = time;
	topSort.push_back(s);
	cout << endTime[s] << ")" <<" ";
}

void Graph::dfsTravel(){
	memset(visited, 0, sizeof(int)*vertexNum);
	memset(beginTime, 0, sizeof(int)*vertexNum);  // 结点开始访问的时间
	memset(endTime, 0, sizeof(int)*vertexNum);    // 结点结束访问的时间
	for(int i = 0; i < vertexNum; i++){
		if(!visited[i]){
			dfs(i);
			cout << endl;
		}
	}
}

时间复杂度O(V+E)

注意:

对于深度优先遍历,其边还可以划分为4类。

(1)树边,深度遍历森林中的每条边就是树边。

(2)前向边,u到其后裔的非树边(u,v)。

(3)反向边,u到其祖先的边(u,v)。

(4)横向边,一个顶点就不是另外一个顶点的祖先或者后裔。

性质:(1)一个有向图是无回路的,当且仅当对该图的深度优先搜索没有产生反向边

(2)对一个无向图G进行深度优先搜索的过程中,G的每一条边要么是树边,要么是反向边。

拓扑排序

有向无回路图(DAG,directed acyclic graph)的拓扑排序是深度优先搜索的一个应用。拓扑排序是对图G的所有顶点的一个线性序列,如果对于图G中的边(u,v),则顶点u排在顶点v的前面。在很多应用中,有向无回路图用于说明事件发生的先后次序。

算法基本思想:通过对DAG图进行深度优先遍历以得到完成访问每个结点的时间,其逆序就是DAG图的拓扑排序。

代码如下:已经在深度遍历中体现。

时间复杂度为O(V+E)。

强连通分支

强连通分支为深度优先搜索的另一个经典应用。有向图G=(V,E)的一个强连通分支就是一个最大顶点C是V的子集,使得C中任意两个顶点可以相互到达

图G的转置:GT=(V,ET),ET={(u,v):(u,v) ∈E}.由ET是由G的边改变方向后所组成的。建立GT所需要的时间复杂度也为O(V+E)

算法的基本思想:首先对图G进行深度优先搜索,据此得到图G的拓扑排序序列,然后将图GT按照此序列进行深度遍历,得到的括号结构便是所有的强连通分支。时间复杂度仍然为O(V+E)

代码如下:

// 创建图g的转置
void Graph::buildTransGraph(Graph &g){
	this->vertexNum = g.vertexNum;
	this->edgesNum = g.edgesNum;
	for(int i = 0; i < vertexNum; i++){
		this->vertex[i] = g.vertex[i];
		this->edges[i].val = g.edges[i].val;
		this->edges[i].weight = g.edges[i].weight;
		this->edges[i].next = NULL;
	}

	for(int i = 0; i < vertexNum; i++){
		GNode *p = g.edges[i].next;
		while(p != NULL){
			GNode *newNode = new GNode();
			newNode->val = i;
			newNode->next = NULL;
			newNode->weight = p->weight;
			GNode *q = &edges[p->val];
			while(q->next != NULL) q = q->next;
			q->next = newNode;
			p = p->next;
		}
	}
}

//强连通分量
void Graph::componentSC(){
	//time = 0;
	//dfsTravel();              // 对图g进行深度搜索得到完成x访问所需要的时间 并由此得到其拓扑排序
	Graph g2;
	g2.buildTransGraph(*this);        // 得到图G的转置
	time = 0;
	memset(g2.visited, 0, sizeof(int)*vertexNum);
	cout << "强连通分量: " << endl;
	for(vector<int>::reverse_iterator iter = topSort.rbegin(); iter != topSort.rend(); iter++){  // 对转置图g2进行深度搜索得到强连通分量
		if(!g2.visited[*iter])
			g2.dfs(*iter);
	}
	cout << endl;
}

完整代码:

graph.h

#ifndef GRAPH_H
#define GRAPH_H
#include <iostream>
#include <vector>
using namespace std;
#define maxSize 10
#define maxInt 0x80000000  // 将此值设为权值的最大值

struct GNode{
	int val;
	int weight;
	GNode *next;
};
class Graph{
public:
	void createGraph(int n, int e);
	void destroyGraph(GNode *p);
	~Graph(){
		for(int i = 0; i < vertexNum; i++){
			destroyGraph(edges[i].next);
			//cout << "析构:" << i << endl;
		}
	}
	void showGraph();
	void bfsTravel();      // 广度遍历
	void dfsTravel();      // 深度遍历
	void showTopSort();   //  输出拓扑序列
	void componentSC();      // 建立图g的强连通分量

	void prim();

private:
	int vertex[maxSize];      // 存放顶点
	GNode edges[maxSize];    //  存放邻接表
	int vertexNum;        //顶点个数
	int edgesNum;          //边条数

	//bfs and dfs 遍历
	int visited[maxSize];
	void bfs(int s);
	void dfs(int s);
	int beginTime[maxSize];       // 深度开始访问x的时间
	int endTime[maxSize];          // 结束访问x的时间
	static int time;
	vector<int> topSort;      // topSort的逆序为有向无回路的拓扑排序
	void buildTransGraph(Graph &g);   // 建立图g的转置

	// prim
	int lowcost[maxSize];
};

#endif

graph.cpp

#include <iostream>
#include "graph.h"
#include <queue>
using namespace std;

int Graph::time = 0;
void Graph::createGraph(int n, int e){
	vertexNum = n;
	edgesNum = e;
	for(int i = 0; i < n; i++){
		vertex[i] = i;
		edges[i].val = i;
		edges[i].weight = 0;
		edges[i].next = NULL;
	}

	for(int i = 0; i < e; i++){
		int source, dest, wei;
		cin >> source >> dest >> wei;
		GNode *newNode = new GNode();
		newNode->val = dest;
		newNode->weight = wei;
		newNode ->next = NULL;
		GNode *p = &edges[source];
		while(p->next != NULL) p = p->next;
		p->next = newNode;

		//  无向图     有向图就将这段删除掉
		/*GNode *newNode2 = new GNode();
		newNode2->val = source;
		newNode2->weight = wei;
		newNode2 ->next = NULL;
		GNode *p2 = &edges[dest];
		while(p2->next != NULL) p2 = p2->next;
		p2->next = newNode2;*/

	}
}

void Graph::destroyGraph(GNode *p){
	if(p == NULL) return;
	else{
		destroyGraph(p->next);
		delete p;
	}
}

void Graph::showGraph(){
	for(int i = 0; i < vertexNum; i++){
		int j = i;
		cout << i << "->";
		GNode *p = edges[j].next;
		while( p != NULL) {
			cout << "(" << p->val <<"," << p->weight << ")" ;
			p = p->next;
		}
		cout << endl;
	}
}

// 广度遍历图
void Graph::bfs(int s){
	queue<int> q;
	q.push(s);
	visited[s] = 1;
	while(!q.empty()){
		int u = q.front();
		q.pop();
		cout << u <<" ";
		GNode *p = edges[u].next;
		while(p != NULL){
			if(!visited[p->val]){    // 未被访问,则将其加入队列中并标志为访问过
				q.push(p->val);
				visited[p->val] = 1;
			}
			p = p->next;
		}
	}

}

void Graph::bfsTravel(){
	memset(visited, 0, sizeof(int)*vertexNum);
	for(int i = 0; i < vertexNum; i++){
		if(!visited[i]){
			bfs(i);
			cout << endl;
		}
	}
}

// 深度优先遍历
void Graph::dfs(int s){
	visited[s] = 1;
	time += 1;
	beginTime[s] = time;
	cout << s << "(" << beginTime[s] << " ";              // shen
	GNode *p = edges[s].next;
	while(p != NULL){
		if(!visited[p->val])
			dfs(p->val);
		p = p->next;
	}
	time += 1;
	endTime[s] = time;
	topSort.push_back(s);
	cout << endTime[s] << ")" <<" ";
}

void Graph::dfsTravel(){
	memset(visited, 0, sizeof(int)*vertexNum);
	memset(beginTime, 0, sizeof(int)*vertexNum);  // 结点开始访问的时间
	memset(endTime, 0, sizeof(int)*vertexNum);    // 结点结束访问的时间
	for(int i = 0; i < vertexNum; i++){
		if(!visited[i]){
			dfs(i);
			cout << endl;
		}
	}
}

//  输出拓扑排序
void Graph::showTopSort(){
	for(vector<int>::reverse_iterator iter = topSort.rbegin(); iter != topSort.rend(); iter ++)
		cout << *iter << " ";
	cout << endl;
}

// 创建图g的转置
void Graph::buildTransGraph(Graph &g){
	this->vertexNum = g.vertexNum;
	this->edgesNum = g.edgesNum;
	for(int i = 0; i < vertexNum; i++){
		this->vertex[i] = g.vertex[i];
		this->edges[i].val = g.edges[i].val;
		this->edges[i].weight = g.edges[i].weight;
		this->edges[i].next = NULL;
	}

	for(int i = 0; i < vertexNum; i++){
		GNode *p = g.edges[i].next;
		while(p != NULL){
			GNode *newNode = new GNode();
			newNode->val = i;
			newNode->next = NULL;
			newNode->weight = p->weight;
			GNode *q = &edges[p->val];
			while(q->next != NULL) q = q->next;
			q->next = newNode;
			p = p->next;
		}
	}
}

//强连通分量
void Graph::componentSC(){
	//time = 0;
	//dfsTravel();              // 对图g进行深度搜索得到完成x访问所需要的时间 并由此得到其拓扑排序
	Graph g2;
	g2.buildTransGraph(*this);        // 得到图G的转置
	time = 0;
	memset(g2.visited, 0, sizeof(int)*vertexNum);
	cout << "强连通分量: " << endl;
	for(vector<int>::reverse_iterator iter = topSort.rbegin(); iter != topSort.rend(); iter++){  // 对转置图g2进行深度搜索得到强连通分量
		if(!g2.visited[*iter])
			g2.dfs(*iter);
	}
	cout << endl;
}

main.cpp

#include <iostream>
#include "graph.h"
using namespace std;

int main(){
	Graph g;
	g.createGraph(8, 13);

	cout << "邻接表: " << endl;
	g.showGraph();

	cout << "广度遍历的结果: " << endl;
	g.bfsTravel();

	cout << "深度遍历的结果: " << endl;   //  具有括号结果  其中x(a b)  x代表结点  a代表开始访问x的时间  b代表完成访问x的时间
	g.dfsTravel();                      // 深度遍历完成访问x的时间的逆序就是有向无回路的拓扑排序

	cout << "拓扑排序: " << endl;
	g.showTopSort();

	g.componentSC();

	return 0;
}

图例:

待传...

输入:

0 1 1

1 2 1

2 0 1

1 3 1

3 4 1

4 3 1

2 5 1

5 6 1

6 5 1

3 6 1

6 7 1

7 7 1

4 7 1

输出:

其中0(1 2(2 3) 4)表示在深度遍历中第0个结点开始访问结点的时间为1,结束访问结点的时间为4;2结点开始访问的时间为2,结束访问的时间为3.

时间: 2024-12-08 04:24:47

算法系列笔记6(有关图的算法一—搜索,拓扑排序和强连通分支)的相关文章

算法系列笔记8(有关图的算法二—最短路径问题)

图的最短路径问题主要分为两类,单源最短路径问题和全对最短路径问题.单源最短路径问题指给点单个源点,求其到所有其它顶点之间的最短距离.而全对最短路径问题指所有顶点之间的最短路劲问题.此外对于单对最短路径问题,从渐进意义上来看,目前还没有比最好的单元算法更快的算法来解决这一问题. 一:单源最短路径问题 单源最短路劲问题根据其权重分为四类,当图G=(V,E)为无权图,直接使用广度优先遍历(这里不做介绍):当权值为非负值,则使用Dijkstra算法:存在负权值及负权环,可以使用Bellman-Ford算

算法系列笔记10(有关图的算法三—最大流与二分图)

本次主要记录流网络以及最大流的简单概念(以后可能会将最大流的实现算法补充),重点讲解用匈牙利算法来求二分图的最大匹配. 1:流网络 流网络是G(V, E)是一个有限的有向图,它的每条边(u, v)∈E都有一个非负值实数的容量c(u, v)≥0.如果(u, v)不属于E,我们假设c(u, v) = 0.我们区别两个顶点: 一个源点s和一个汇点t..并假定每个顶点均处于从源点到汇点的某条路径上. 形式化的定义:一道网络流是一个对于所有结点u和v都有以下特性的实数函数::满足下面两条性质: 容量限制:

算法系列笔记5(扩展数据结构-动态顺序统计和区间树)

在编程中,我们往往使用已有的数据结构无法解决问题,这是不必要急着创建新的数据结构,而是在已有数据结构的基础上添加新的字段.本节在上一次笔记红黑树这一基础数据结构上进行扩展,得出两个重要的应用-动态顺序统计和区间树. 动态顺序统计 在算法系列笔记2中我们在线性时间内完成了静态表的顺序统计,而这里我们在红黑树上进行扩展,在O(lgn)时间内完成该操作,主要包括返回第i 排名的元素os_select(i)和给定一个元素x,返回其排名(os_rank(x)). 思想:添加新项:在红黑树的结点上记录下该结

算法系列笔记1(排序)

本次主要记录一些经典的排序算法,其中包括冒泡排序.直接选择排序.插入排序.归并排序.快速排序.堆排序.希尔排序.桶排序以及计数排序和基数排序.首先会给出这些排序算法的基本思想,然后给出实现的代码,最后会给出其时间复杂度. 1:冒泡排序 思想: (1):比较相邻的前后两个元素,如果后面的数据小于前面的数据,则交换这两个数据的位置.这样经过一次遍历,最小的元素将在第0个位置,属于"冒泡". (2):重复第一步,依次将第二小-的元素排列到数组的顶端. // 交换数据的三种方法 void sw

算法系列笔记9(字符串匹配)

字符串匹配指有一个文本串S和一个模式串P,现在要查找P在S中的位置. 主要有以下算法: 其中朴素算法和KMP算法我们在这边bloghttp://blog.csdn.net/lu597203933/article/details/41124815中已经讲解过.RP算法时间复杂度较高,我也没看,想看可以看算法导论.这里主要讲解有限自动机的字符串匹配算法. 有限自动机的定义: 有限自动机字符串匹配主要是构建一个状态转移函数.&(q,a)表示状态q<其中状态q表示已经匹配成功q个字符了>接收字

[算法系列之二十七]Kruskal最小生成树算法

简介 求最小生成树一共有两种算法,一个是就是本文所说的Kruskal算法,另一个就是Prime算法.在详细讲解Kruskal最小生成树算法之前,让我们先回顾一下什么是最小生成树. 我们有一个带权值的图,我们要求找到一个所有生成树中具有最小权值的生成树.如下图所示,T是图G的生成树.但不是具有最小权值的生成树. 我们可以把他们想象成一组岛屿和连接它们的可能的桥梁.当然修桥是非常昂贵和费时的,所以我们必须要知道建设什么样的桥梁去连接各个岛.不过有一个重要的问题,建设这样一组连接所有岛屿的桥梁的最低价

算法学习笔记 二叉树和图遍历—深搜 DFS 与广搜 BFS

图的深搜与广搜 马上又要秋招了,赶紧复习下基础知识.这里复习下二叉树.图的深搜与广搜.从图的遍历说起,图的遍历方法有两种:深度优先遍历(Depth First Search), 广度优先遍历(Breadth First Search),其经典应用走迷宫.N皇后.二叉树遍历等.遍历即按某种顺序访问"图"中所有的节点,顺序分为: 深度优先(优先往深处走),用的数据结构是栈, 主要是递归实现: 广度优先(优先走最近的),用的数据结构是队列,主要是迭代实现: 对于深搜,由于递归往往可以方便的利

算法系列笔记2(静态表顺序统计-随机选择算法)

问题:当给定存在静态表(如数组)中的n个元素,如何快速找到其中位数.最小值.最大值.第i小的数? 首先想到的方法是先对数组元素进行排序,然后找到第i小的元素.这样是可行的,但比较排序最快也需要O(nlgn),能否在线性时间内解决呢.这就是随机的分治法-随机选择. 思想:利用随机划分(在快速排序中介绍过)找到主元r,这样就将小于等于r的元素放在了其左边,大于r的元素放在了其右边.这是可以计算出r的rank为k,如果正好等于i,则就返回该元素:如果k大于i,则在左边中寻找第i小的元素,否则在右边中寻

算法系列笔记3(二叉查找树)

(1)二叉查找树的性质:设x为二叉查找树的一个结点.如果y是x左子树中的一个结点,则key[y]≤key[x].如果y是x的右子树中的一个结点.则key[x]≤key[y]. (2)二叉查找树的结点中除了key域和卫星数据外,还包括left.right和p分别指向结点的左儿子.右儿子和父节点. (3)构造一棵二叉查找树最好情况下时间复杂度为O(nlgn),最坏情况为O(n^2).随机化构造一棵二叉查找树的期望时间O(nlgn).与快排和随机化快速排序算法是做相同的比较,但是顺序不一样.可以证明随