求解单源最短路径问题

一、图的广度优先遍历BFS

  • 概念

图的广度优先遍历类似于树的层序遍历,基本的思想是首先访问起始顶点 v 0 v_0 v0,接着访问 v 0 v_0 v0未被访问过的邻接顶点 v 1 v_1 v1 v 2 v_2 v2、…、 v i v_i vi,之后对 v 1 v_1 v1 v 2 v_2 v2、…、 v i v_i vi重复对 v 0 v_0 v0的操作,直至遍历完图中的所有顶点。
当然,如果这个图是非连通的,那么从 v 0 v_0 v0出发,就只能访问 v 0 v_0 v0所在的连通分量中的全部顶点,至于图中其余未被访问的顶点,只能从中重新选择一个起始顶点,重复之前的操作,直至所有的顶点都被访问。
需要注意的是,在访问的过程中,我们需要一个辅助队列,每访问一个顶点,我们都需要将对应顶点的未被访问过的邻接顶点入队。
而且和树的层序遍历不同的是,图中的每一个顶点可能和图中其他任意顶点邻接,所以在遍历的过程中,我们就要防止已经访问过的顶点,再次以其他顶点的邻接顶点的身份被访问。我们需要增加一个布尔类型的数组来达到这个目的,每个顶点对应数组中的一个元素,值为true,表示已经被访问过,否则就是未被访问。

  • 例子
    如图:
    请添加图片描述
    假设起始顶点是 v 1 v_1 v1,我们先访问 v 1 v_1 v1,之后访问 v 1 v_1 v1的邻接顶点 v 2 v_2 v2 v 5 v_5 v5,然后访问 v 2 v_2 v2的邻接顶点 v 6 v_6 v6,再然后访问 v 6 v_6 v6的邻接顶点 v 3 v_3 v3 v 7 v_7 v7,接着是 v 3 v_3 v3的邻接顶点 v 4 v_4 v4,以及 v 7 v_7 v7的邻接顶点 v 8 v_8 v8
    最终的遍历序列就是 v 1 v_1 v1 v 2 v_2 v2 v 5 v_5 v5 v 6 v_6 v6 v 3 v_3 v3 v 7 v_7 v7 v 4 v_4 v4 v 8 v_8 v8
  • 代码
#include<iostream>
using namespace std;

#define MaxVertexNum 10                      //顶点的数目为8
//定义图
struct MGraph {
	char Vex[MaxVertexNum];                 //顶点表
	int Edge[MaxVertexNum][MaxVertexNum];   //邻接矩阵
	int vexnum, arcnum;                     //当前定点数和弧数
};

//访问标记数组
bool visited[MaxVertexNum];

#pragma region  图的初始化操作

//常用图
void InitMGraph2(MGraph& G)
{
	G.arcnum = 10;
	G.vexnum = 8;
	for (int i = 0; i < 8; i++)
	{
		G.Vex[i] = i + 1 + 48;
	}
	G.Edge[0][4] = G.Edge[4][0] = 1;
	G.Edge[0][1] = G.Edge[1][0] = 1;
	G.Edge[1][5] = G.Edge[5][1] = 1;
	G.Edge[5][2] = G.Edge[2][5] = 1;
	G.Edge[5][6] = G.Edge[6][5] = 1;
	G.Edge[2][6] = G.Edge[6][2] = 1;
	G.Edge[2][3] = G.Edge[3][2] = 1;
	G.Edge[6][3] = G.Edge[3][6] = 1;
	G.Edge[6][7] = G.Edge[7][6] = 1;
	G.Edge[3][7] = G.Edge[7][3] = 1;
}


#pragma endregion

//图的基本操作:寻找某一个节点的第一个邻居结点
int FirstNeighbor(MGraph G, int v)
{
	//遍历第v行
	for (int i = 0; i < G.vexnum; i++)
	{
		//有邻接顶点
		if (G.Edge[v - 1][i] != 0)
		{
			return i + 1;
		}
	}
	//没有邻接顶点
	return -1;
}

//图的基本操作:返回除指定邻居节点的下一个邻接结点
int NextNeighbor(MGraph G, int v, int firstNeighbor)
{
	for (int i = firstNeighbor; i < G.vexnum; i++)
	{
		//找到了下一个邻接结点
		if (G.Edge[v - 1][i] != 0)
			return i + 1;
	}
	//没有下一个邻接结点了
	return -1;
}

//定义全局变量图:
MGraph G;

//访问结点
void visit(int w)
{
	cout << G.Vex[w - 1] << endl;
}


//辅助队列
//定义队列
struct SqQueue {
	int data[MaxVertexNum];   //使用静态数组存储队列
	int front, rear;     //队头指针,队尾指针
};

//初始化队列1,队头指针指向表头元素,队尾指针指向表尾元素的下一位
void InitQueue1(SqQueue& S)
{
	//确定队头指针和队尾指针的指向
	S.front = S.rear = 0;
}

//判断队列是否为空,能这么判断队列为空,是因为对头元素的前一个位置不能存储元素
bool Empty1(SqQueue& S)
{
	//对头指针和队尾指针指向同一个位置
	if (S.front == S.rear)
		return true;
	else
		return false;
}

//判断队列是否已经满了
bool isFull1(SqQueue& S)
{
	//条件是 队尾指针指向对头指针的前一个位置
	if (S.rear + 1 == S.front)
		return true;
	else
		return false;
}

//入队操作
bool EnQueue1(SqQueue& S,int e)
{
	//判断队列是否已经满了
	if (isFull1(S))
		return false;
	//元素写入,指针加1
	S.data[S.rear] = e;
	S.rear++;
	return true;
}

//出队操作
bool OutQueue1(SqQueue& S, int &x)
{
	//判断队列是否已经空了
	if (Empty1(S))
		return false;
	x = S.data[S.front];
	S.front++;
	return true;
}


#pragma region  广度优先遍历

//定义全部变量队列
SqQueue S;

//BFS算法
void BFS(MGraph G, int v)
{
	visit(v);             //访问当前结点
	visited[v] = true;    //标记已经访问过
	EnQueue1(S, v);       //入队
	while (!Empty1(S))
	{
		OutQueue1(S, v);  //顶点v出队
		for (int w = FirstNeighbor(G, v); w > 0; w = NextNeighbor(G, v, w))
		{
			if (!visited[w])
			{
				visit(w);
				visited[w] = true;
				EnQueue1(S, w);
			}
		}
	}
}

void BESTraverse(MGraph G)
{
	//初始化visited数组
	for (int i = 1; i <= G.vexnum; i++)
	{
		visited[i] = false;
	}
	//初始化队列
	InitQueue1(S);
	for (int i = 1; i <= G.vexnum; i++)
	{
		if (!visited[i])
		{
			BFS(G, i);
		}
	}
}


#pragma endregion

void test()
{
	//初始化图
	//InitMGraph1(G);
	InitMGraph2(G);

	//广度优先遍历结果
	cout << "--------------------" << endl;
	BESTraverse(G);
}

int main()
{
	test();

	system("pause");
	return 0;
}
  • 时间复杂度和空间复杂度
邻接表解释邻接矩阵解释
时间复杂度O(|V| + |E|)采用邻接表存储方式,每个定点均需访问一次,时间复杂度未O(|V|),在搜索任意一个顶点的邻接点时,每条边至少访问一次,时间复杂度未O(|E|),所以总的总的时间复杂度就是O(|V| + |E|)O( ∣ V ∣ 2 |V|^2 V2)采用邻接矩阵存储时,查找每个顶点的邻接点所需的时间都是O(|V|),所以总的时间复杂度就是O( ∣ V ∣ 2 |V|^2 V2)
空间复杂度O(|V|)需要使用一个辅助队列,每个节点都需要入队一次,最坏的情况下,时间复杂度未O(|V|)O(|V|)同邻接表

二、BFS算法求解非带权图的单源最短路径

  • 概念

所谓的非带权单源最短路径,就是指边不带权值,或者是边的权值都相等的情况下,求其中一个顶点(源点)到其他顶点的最短路径。
我们的思路如下:在广度优先遍历中,我们首先访问起始顶点 v 0 v_0 v0,之后是 v 0 v_0 v0的邻接顶点 v 2 v_2 v2 v 5 v_5 v5,显然 v 2 v_2 v2 v 5 v_5 v5 v 0 v_0 v0的距离是1,即1条边的距离;之后访问顶点 v 2 v_2 v2的未被访问过的邻接顶点 v 6 v_6 v6,也很容易判断, v 6 v_6 v6 v 0 v_0 v0的距离是2,以此类推,每一个顶点到 v 0 v_0 v0的距离,都是其前面一个顶点(例如 v 6 v_6 v6前面的顶点是 v 2 v_2 v2)到 v 0 v_0 v0的距离再加1。

  • 代码

我们继续采用上面的例子,对BFS算法做一些小的改动。之前在BFS算法中,我们遍历到每一个顶点时,进行的操作是在控制台打印这个顶点,而现在我们要进行的操作是计算这个顶点到起始顶点的距离:d[w] = d[u] + 1;,w和u的关系上面已经说过。

//求解非带权图的单源最短路径问题
int d[MaxVertexNum];      //记录最短路径长度
int path[MaxVertexNum];  //记录路径
void BFS_MIN_Distance(MGraph G, int u)
{
	//初始化最短路径长度
	for (int i = 1; i < G.vexnum; i++)
	{
		d[i] = INFINITY;
		path[i] = -1;
	}

	visited[u] = true;    //标记已经被访问过
	d[u] = 0;             //出发点到本身的路径长度是0
	EnQueue1(S1, u);       //入队
	while (!Empty1(S1))    //BFS算法主要过程
	{
		OutQueue1(S1, u);  //出队
		for (int w = FirstNeighbor(G, u); w > 0; w = NextNeighbor(G, u, w))
		{
			if (!visited[w])
			{
				visited[w] = true;
				d[w] = d[u] + 1;     //w到起始点的路径长度在前驱节点的基础上 + 1
				path[w] = u;         //w的前驱节点是u
				EnQueue1(S1, w);
			}
		}
	}
}

代码中的path数组,是用来记录每一个顶点的前驱顶点,例如:path[w] = u。

三、Dijkstra算法求解单源最短路径问题

在Dijkstra算法中,我们将图中的所有顶点分为两部分,一部分是已经找到到达起始顶点的最短路径,这部分顶点构成一个集合,记作S,另一部分是还没有找到,这部分顶点同样构成一个集合,记作Q。显然,顶点集V = S + Q。

除此之外,我们还需要借助两个辅助数组:
dist[]:这个数组的长度为有|V|,分别对应 v 0 v_0 v0,…, v n v_n vn这n个顶点,数组中的值dist[i]的含义是顶点 v i v_i vi到达起始顶点 v 0 v_0 v0的最短路径的长度。
path[]:这个数组的长度为有|V|,分别对应 v 0 v_0 v0,…, v n v_n vn这n个顶点,数组中值path[i]的含义是顶点 v i v_i vi到达起始顶点 v 0 v_0 v0的最短路径中,顶点 v i v_i vi的前驱节点在顶点表中的数组下。

在求解的过程中,dist[]和path[]是动态更新的,其实我们一开始也并不知道顶点 v i v_i vi到达起始顶点 v 0 v_0 v0的最短路径是那一条,但是我们会通过算法的执行,让这两个数组中存储的信息变成我们想要的结果。
过程如下:

  • 初始化
    我们遍历邻接矩阵,查看图中的顶点和起始顶点之间是否存在边,如果存在,我们在dist[]数组对应的位置上存入对应变得权值,path[]数组中对应位置的值即为起始顶点在数组中的下标;如果不存在,dist[]数组对应的位置的值设为无穷,path[]数组中对应位置的值为-1,表示没有到达起始顶点的路径。目前顶点集S中只有起始顶点 v 0 v_0 v0,顶点集Q中包含 v 1 v_1 v1 v n v_n vn
  • 算法流程
  1. 我们遍历dist[]数组,从中找到除起始顶点 v 0 v_0 v0以外的其他顶点对应的值中,最小的那个值dist[k],将对应的顶点 v k v_k vk加入到顶点集S中,dist[k]就是顶点 v k v_k vk到达起始顶点的最短路径的长度 v 0 v_0 v0
  2. 之后我们需要做一个比较,我们需要得到集合Q中的顶点到达 v k v_k vk的距离(在邻接矩阵arcs[][]中可以得到),之后再加上dist[k],即dist[k] + arcs[k][q](q属于Q);之后将dist[k] + arcs[k][q]与dist[q]做对比,如果前者大于后者,不做任何操作,反之,将dist[k] + arcs[k][q]赋值给dist[q],path[q] = k。
  3. 重复1~2,我们发现,每重复一次,都可以将Q中的一个顶点划分到S中,所以最终需要执行n - 1次,才能够找到所有的顶点到达起始顶点 v 0 v_0 v0的最短路径。
时间复杂度解释空间复杂度解释
O( ∣ V ∣ 2 |V|^2 V2)无论是采用邻接表方式存储,还是采用邻接矩阵的方式进行存储,时间复杂度都是 O( ∣ V ∣ 2 |V|^2 V2)。先说邻接矩阵,我们每一次循环只能为一个顶点找到最短路径,所以总共要循环|V| - 1次,而再每一次循环中,我们需要对dist[]数组进行遍历,找到最小的值,这个时间花费为O(|V|),我们还要对dist[]的值进行修改,修改的时候需要遍历邻接矩阵中的某一行,这个时间花销也是O(|V|),总体来算,就是(|V| - 1) * (|V| + |V|),也就是O( ∣ V ∣ 2 |V|^2 V2) 。而采用邻接表的方式存储,可以减少修改dist[]数组花费的时间,但是再dist[]数组中选择最小分量的时间不变,因此时间复杂度仍然是O( ∣ V ∣ 2 |V|^2 V2)O(|V|)这里我们只考虑了算法执行过程中需要消费的空间,而并没有考虑到存储图需要花费的空间,执行算法的过程中我们需要使用两个辅助空间dist[]和path[],空间花销都是O(|V|)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值