文章目录
1.图的基本概念
图是由顶点集合及顶点间的关系组成的一种数据结构。
表示为 G = (V, E)。V代表的是顶点集合,E代表边集合
树也是一种特殊的图,这个图无环。
树关注的时节点存的值,图更关注的是顶点及边的权值。
权值:边中的附带的数据信息。
顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>.
有向图和无向图
有向图 :顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边。

无向图 :顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边。

完全图
在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图;
在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有两条边,且仅这两条边方向相反,则称此图为有向完全图。
邻接顶点
无向图中:两个顶点U、V有一条边连接。成为U、V互为临界顶点。
有向图中:两个顶点U、V。若是U指向V,则称为U邻接到V,V邻接自顶点U。并称这条边与顶点U、V相关联。
顶点的度
顶点V的度指的是与它相关联的边的条数
特殊:
- 对于有向图,顶点的度等于该顶点的入度+出度。
出度:从顶点出去的边的条数。
入度:指向这个顶点的边数。 - 对于无向图:入度=出度=顶点的度
路径
从顶点U到顶点V的一组边称为路径。路径不止一条。
路径长度:对于不带权图为路径的边个数。带权图为路径所有边权值的和
最短路径:所有路径,路径长度最小的路径。
简单路径与回路:
- 简单路径:路径上经过顶点不重复。
- 回路:路径上经过的顶点有重复。
子图、连通图、强连通图
两张图A、B,若A的顶点时B的部分顶点,A的边是B的部分边,则称为A是B的子图。
连通图:无向图中,若顶点A、B存在路径,称为A、B连通。若图中的任意两点都是连通的,则称此图为连通图。
强连通图:有向图中,若图中的任意两点都是连通的(A指向B,且B指向A),则称此图为连通图。
生成树
无向图中一个连通图的最小连通子图称为生成树。(用最少的边把所有顶点连接起来)。n个顶点的连通图的生成树有n-1条边。
最小生成树:所有生成树中,路径长度最小的生成树。
2.图的存储结构
因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边的关系即可。
节点通过数组保存,边的关系通过下面的方式保存。
图的创建
可以通过输入的方式来创建,或者是通过文件的方式来读取。
这里采用手动添加边来创建图,方便测试。
邻接矩阵
邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。

如果矩阵位置为0代表无关,位置为1代表连接。A对应的下标为0,D对应的下标为3。这里采用map映射的方式来建立顶点与顶点下标的关系。
容易得出:无向图为对称矩阵。
此外:如果这个图是带权图,则矩阵内的数据为所带权值。
邻接矩阵的存储方式的优点:
- 非常适合边比较多的情况。
- O(1)判断两个节点的连接关系。
不足:
- O(N)查找一个节点连接的所有边(N为节点个数)。
C++邻接矩阵创建图
#pragma once
#include <vector>
#include <map>
#include <iostream>
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix{
//邻接矩阵保存边关系
template<class v,class w,w max_w = INT_MAX,bool bDirection = false>
class Graph{
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
//获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
public:
// 图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for(int i = 0; i < m_matrix.size(); ++i){
m_matrix[i].resize(points.size(),max_w);
}
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDest = GetPointIndex(dest);
//区分有向图和无向图
m_matrix[posSrc][posDest] = weight;
if(!bDirection){
//无向图,添加两条边关系
m_matrix[posDest][posSrc] = weight;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
//打印边
std::cout << " ";
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
//打印矩阵
for(size_t i = 0; i < m_matrix.size(); ++i){
std::cout << m_vertexs[i] << " ";
for(size_t j = 0; j < m_matrix[i].size(); ++j){
if(m_matrix[i][j] == max_w){
std::cout << "*" << " ";
}
else{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
};
}
#include "Graph.hpp"
using namespace std;
void TestGraph(){
vector<char>points = {'A','B','C','D'};
matrix::Graph<char,int,INT_MAX,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.Print();
}
int main(){
TestGraph();
return 0;
}

邻接表
使用数组表示顶点的集合,使用链表表示边的关系。
创建指针数组,自己连接的顶点挂在下面。同样的,对节点进行编号。

邻接表的优点:
- 适合保存稀疏的边关系。
- 适合查找一个顶点连接出的边
不足:
- 不适合确定两个顶点是否相连,判断权值。
注意:如果图是有向图,则如果使用邻接表存储边的关系,需要保存出边表和入边表。
C++邻接表创建图
#pragma once
#include <vector>
#include <map>
#include <iostream>
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
// 邻接表保存边关系
namespace link_table{
template<class w>
struct Edge{
int detPos;
int srcPos;
w weight;
Edge<w> *next;
Edge(int _srcPos, int _detPos, const w& _weight)
:detPos(_detPos),srcPos(_srcPos),weight(_weight),next(nullptr)
{}
};
template<class v,class w,bool Direction = false>
class Graph{
typedef Edge<w> Edge;
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<Edge*> m_tables; // 领接表
// 获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
public:
//图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_tables.resize(points.size());
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDest = GetPointIndex(dest);
Edge* pEdge = new Edge(posSrc,posDest,weight);
//头插
pEdge->next = m_tables[posSrc];
m_tables[posSrc] = pEdge;
if(!Direction){
//无向图,添加两条边关系
Edge* pEdge = new Edge(posDest,posSrc,weight);
//头插
pEdge->next = m_tables[posDest];
m_tables[posDest] = pEdge;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
for(size_t i = 0; i < m_tables.size(); ++i){
std::cout << m_vertexs[i] << "[" << i << "]:";
Edge* pNode = m_tables[i];
while(pNode!= nullptr){
std::cout << m_vertexs[pNode->detPos]
<< "[" << pNode->detPos << "]"
<< "(" << pNode->weight << ")" << " ";
pNode = pNode->next;
}
std::cout << "nullptr" << std::endl;
}
}
};
}
void TestGraph2(){
vector<char>points = {'A','B','C','D'};
link_table::Graph<char,int,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.Print();
}
int main(){
//TestGraph();
TestGraph2();
return 0;
}

3.图的遍历
广度优先遍历 BFS
广度优先队列:以某个顶点为起点,一层一层进行遍历。需要借助队列。

具体遍历方式与二叉树的层序遍历方式类似,不同的是要通过标记的方式防止节点的重复遍历。
namespace matrix{
//邻接矩阵保存边关系
template<class v,class w,w max_w = INT_MAX,bool Direction = false>
class Graph{
public:
// 图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for(int i = 0; i < m_matrix.size(); ++i){
m_matrix[i].resize(points.size(),max_w);
}
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
//区分有向图和无向图
m_matrix[posSrc][posDet] = weight;
if(!Direction){
//无向图,添加两条边关系
m_matrix[posDet][posSrc] = weight;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
//打印边
std::cout << " ";
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
//打印矩阵
for(size_t i = 0; i < m_matrix.size(); ++i){
std::cout << m_vertexs[i] << " ";
for(size_t j = 0; j < m_matrix[i].size(); ++j){
if(m_matrix[i][j] == max_w){
std::cout << "*" << " ";
}
else{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
void BFS(const v& src){//传入起点
size_t srcPos = GetPointIndex(src);
std::queue<size_t> q;
q.push(srcPos);
//标记数组
std::vector<bool> visited(m_vertexs.size(),false);
visited[srcPos] = true; //入队列标记
int iLevelSize = 1; //每层节点个数
int iLevel = 1; //层数
while(!q.empty()){
for(int i = 0; i < iLevelSize; ++i){
int iFront = q.front();
std::cout << iFront << ":" << m_vertexs[iFront] << " ";
q.pop();
//这个节点周围的节点入队
for(size_t i = 0; i < m_vertexs.size(); i++){
if(m_matrix[iFront][i]!= max_w && !visited[i])
{
// 入队时,标记已访问节点
q.push(i);
visited[i] = true;
}
}
}
iLevelSize = q.size();
std::cout << "第" << iLevel++ << "层" << std::endl;
}
std::cout << std::endl;
}
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
//获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
};
}
// 邻接表保存边关系
namespace link_table{
template<class w>
struct Edge{
int detPos;
int srcPos;
w weight;
Edge<w> *next;
Edge(int _srcPos, int _detPos, const w& _weight)
:detPos(_detPos),srcPos(_srcPos),weight(_weight),next(nullptr)
{}
};
template<class v,class w,bool Direction = false>
class Graph{
typedef Edge<w> Edge;
public:
//图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_tables.resize(points.size());
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDest = GetPointIndex(dest);
Edge* pEdge = new Edge(posSrc,posDest,weight);
//头插
pEdge->next = m_tables[posSrc];
m_tables[posSrc] = pEdge;
if(!Direction){
//无向图,添加两条边关系
Edge* pEdge = new Edge(posDest,posSrc,weight);
//头插
pEdge->next = m_tables[posDest];
m_tables[posDest] = pEdge;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
for(size_t i = 0; i < m_tables.size(); ++i){
std::cout << m_vertexs[i] << "[" << i << "]:";
Edge* pNode = m_tables[i];
while(pNode!= nullptr){
std::cout << m_vertexs[pNode->detPos]
<< "[" << pNode->detPos << "]"
<< "(" << pNode->weight << ")" << " ";
pNode = pNode->next;
}
std::cout << "nullptr" << std::endl;
}
}
void BFS(const v& src){//传入起点
size_t srcPos = GetPointIndex(src);
std::queue<size_t> q;
q.push(srcPos);
//标记数组
std::vector<bool> visited(m_vertexs.size(),false);
visited[srcPos] = true;
int iLevelSize = 1; //每层节点数
int iLevel = 1; //层数
while(!q.empty()){
for(int i = 0;i < iLevelSize; ++i)
{
int iFront = q.front();
std::cout << iFront << ":" << m_vertexs[iFront] << " ";
q.pop();
//这个节点周围的节点入队
Edge* pEdge = m_tables[iFront];
while(pEdge)
{
if(!visited[pEdge->detPos])
{
// 入队时,标记已访问节点
q.push(pEdge->detPos);
visited[pEdge->detPos] = true;
}
pEdge = pEdge->next;
}
}
iLevelSize = q.size();
std::cout << "第" << iLevel++ << "层" << std::endl;
}
std::cout << std::endl;
}
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<Edge*> m_tables; // 领接表
// 获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
};
}
#include "Graph.hpp"
using namespace std;
void TestGraph(){
vector<char>points = {'A','B','C','D'};
matrix::Graph<char,int,INT_MAX,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.BFS('A');
}
void TestGraph2(){
vector<char>points = {'A','B','C','D'};
link_table::Graph<char,int,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.BFS('A');
}
int main(){
TestGraph();
//TestGraph2();
return 0;
}

广度优先遍历序列

1.邻接矩阵
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一;
2.邻接表
同一个图的邻接表表示方式不唯一,因此广度优先遍历序列不唯一;
复杂度分析
1.邻接矩阵
(1)访问|V|个顶点需要O(|V|)的时间;
(2)查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点;
(3)两个加起来是O(|V|)+O(|V|2),所以时间复杂度=O(|V|2);
2.邻接表
(1)访问|V|个顶点需要O(|V|)的时间;
(2)查找各个顶点的邻接点共需要O(|E|)的时间;
(3)所以时间复杂度=O(|V|+|E|);
深度优先遍历 DFS
深度优先遍历时,没遍历一个节点后,将这个节点标记已访问,防止重复访问。
-
访问顶点v;
-
从v的未被访问的邻接点中选取一个顶点w**(选取规则是找相邻编号最小的结点)**,从w出发进行深度优先遍历;
-
重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
namespace matrix{
//邻接矩阵保存边关系
template<class v,class w,w max_w = INT_MAX,bool Direction = false>
class Graph{
public:
// 图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for(int i = 0; i < m_matrix.size(); ++i){
m_matrix[i].resize(points.size(),max_w);
}
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
//区分有向图和无向图
m_matrix[posSrc][posDet] = weight;
if(!Direction){
//无向图,添加两条边关系
m_matrix[posDet][posSrc] = weight;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
//打印边
std::cout << " ";
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
//打印矩阵
for(size_t i = 0; i < m_matrix.size(); ++i){
std::cout << m_vertexs[i] << " ";
for(size_t j = 0; j < m_matrix[i].size(); ++j){
if(m_matrix[i][j] == max_w){
std::cout << "*" << " ";
}
else{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
//深度优先遍历
void DFS(const v& src){
size_t srcPos = GetPointIndex(src);
std::vector<bool> visited(m_vertexs.size(),false);
_DFS(srcPos,visited);
std::cout << std::endl;
}
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
//获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
void _DFS(size_t srcPos, std::vector<bool>& visited){
std::cout << srcPos << ":" << m_vertexs[srcPos] << " ";
visited[srcPos] = true;
for(size_t i = 0; i < m_vertexs.size(); i++)
{
if(m_matrix[srcPos][i]!= max_w && !visited[i]){
_DFS(i,visited);
}
}
}
};
}
// 邻接表保存边关系
namespace link_table{
template<class w>
struct Edge{
int detPos;
int srcPos;
w weight;
Edge<w> *next;
Edge(int _srcPos, int _detPos, const w& _weight)
:detPos(_detPos),srcPos(_srcPos),weight(_weight),next(nullptr)
{}
};
template<class v,class w,bool Direction = false>
class Graph{
typedef Edge<w> Edge;
public:
//图的创建
Graph(const std::vector<v>& points){
m_vertexs.resize(points.size());
for(size_t i = 0; i < points.size(); ++i){
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_tables.resize(points.size());
}
//添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v& src, const v& dest, const w& weight){
size_t posSrc = GetPointIndex(src);
size_t posDest = GetPointIndex(dest);
Edge* pEdge = new Edge(posSrc,posDest,weight);
//头插
pEdge->next = m_tables[posSrc];
m_tables[posSrc] = pEdge;
if(!Direction){
//无向图,添加两条边关系
Edge* pEdge = new Edge(posDest,posSrc,weight);
//头插
pEdge->next = m_tables[posDest];
m_tables[posDest] = pEdge;
}
}
void Print(){
//打印顶点对应坐标
for(size_t i = 0; i < m_vertexs.size(); ++i){
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
for(size_t i = 0; i < m_tables.size(); ++i){
std::cout << m_vertexs[i] << "[" << i << "]:";
Edge* pNode = m_tables[i];
while(pNode!= nullptr){
std::cout << m_vertexs[pNode->detPos]
<< "[" << pNode->detPos << "]"
<< "(" << pNode->weight << ")" << " ";
pNode = pNode->next;
}
std::cout << "nullptr" << std::endl;
}
}
void DFS(const v& src){
size_t srcPos = GetPointIndex(src);
std::vector<bool> visited(m_vertexs.size(),false);
_DFS(srcPos,visited);
std::cout << std::endl;
}
private:
std::vector<v> m_vertexs; // 顶点集合
std::map<v,int> m_mapIndex; // 顶点与下标的映射
std::vector<Edge*> m_tables; // 领接表
// 获取顶点下标
size_t GetPointIndex(const v& point) const{
typename std::map<v,int>::const_iterator ptr = m_mapIndex.find(point);
if(ptr!= m_mapIndex.end())
return ptr->second;
else{
throw std::invalid_argument("point not found");
return -1;
}
}
void _DFS(size_t srcPos, std::vector<bool>& visited){
std::cout << srcPos << ":" << m_vertexs[srcPos] << " ";
visited[srcPos] = true;
for(size_t i = 0; i < m_vertexs.size(); i++)
{
if(m_tables[srcPos]!= nullptr){
Edge* pEdge = m_tables[srcPos];
while(pEdge)
{
if(!visited[pEdge->detPos])
{
_DFS(pEdge->detPos,visited);
}
pEdge = pEdge->next;
}
}
}
}
};
}
#include "Graph.hpp"
using namespace std;
void TestGraph(){
vector<char>points = {'A','B','C','D'};
matrix::Graph<char,int,INT_MAX,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.DFS('C');
}
void TestGraph2(){
vector<char>points = {'A','B','C','D'};
link_table::Graph<char,int,true> graph(points);
graph.AddEdge('A','B',1);
graph.AddEdge('A','D',4);
graph.AddEdge('B','D',2);
graph.AddEdge('B','C',9);
graph.AddEdge('C','D',8);
graph.AddEdge('C','B',5);
graph.AddEdge('C','A',3);
graph.AddEdge('D','C',6);
graph.Print();
graph.DFS('A');
}
int main(){
TestGraph();
//TestGraph2();
return 0;
}

注意:
如果图是连通图,则上述的遍历方式可以将图全部遍历完毕。
如果不是连通图,最后看标记数组是否全部已经被标记。如果还有点没有标记,则说明这个图不是连通图。此时更改点继续遍历。
4. 最小生成树算法(Kruskal、 Prim算法)
定义:
无向图中一个连通图的最小连通子图称为生成树。(用最少的边把所有顶点连接起来)。n个顶点的连通图的生成树有n-1条边。
路径长度:对于不带权图为路径的边个数。带权图为路径所有边权值的和
最小生成树:所有生成树中,路径长度最小的生成树。
所以生成树一定是连通图。这个定义是在无向图的基础上开展的。
连通图:无向图中,若顶点A、B存在路径,称为A、B连通。若图中的任意两点都是连通的,则称此图为连通图。
构造最小生成树
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:
- 只能使用图中的权值最小的边来构造最小生成树
- 只能使用恰好n-1条边来连接图中的n个顶点
- 选用的n-1条边不能构成回路
构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。
Kruskal算法
克鲁斯卡尔算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。
判断图是否产生闭环,可以采用并查集的方式。如果这个边的顶点在并查集中,则说明如果添加这条边的话就构成环。
举个例子,下图是一个连通网,克鲁斯卡尔算法查找 图中 对应的最小生成树,需要经历以下几个步骤:

- 将连通网中的所有边按照权值大小做升序排序:

- 从 B-D 边开始挑选,由于尚未选择任何边组成最小生成树,且 B-D 自身不会构成环路,所以 B-D 边可以组成最小生成树。

- D-T 边不会和已选 B-D 边构成回路,可以组成最小生成树:

- A-C 边不会和已选 B-D、D-T 边构成环路,可以组成最小生成树:

- C-D 边不会和已选 A-C、B-D、D-T 边构成环路,可以组成最小生成树:

-
C-B 边会和已选 C-D、B-D 边构成环路,因此不能组成最小生成树:

-
B-T、A-B 两条边都会和已选 A-C、C-D、B-D、D-T 构成环路,都不能组成最小生成树。而 S-A 不会和已选边构成环路,可以组成最小生成树。

- 如下图所示,对于一个包含6个顶点的连通网,我们已经选择了 5 条边,这些边组成的生成树就是最小生成树。

引入并查集 #include "UnionFindSet.hpp"。
#include "UnionFindSet.hpp"
#include <vector>
#include <map>
#include <queue>
#include <iostream>
template <class w>
struct Edge
{
int detPos;
int srcPos;
w weight;
Edge<w> *next;
Edge(int _srcPos, int _detPos, const w &_weight)
: detPos(_detPos), srcPos(_srcPos), weight(_weight), next(nullptr)
{}
};
struct rules{
template <class w>
bool operator()(const Edge<w> &left, const Edge<w> &right) const{
return left.weight > right.weight; //从小到大
}
};
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix
{
// 邻接矩阵保存边关系
template <class v, class w, w max_w = INT_MAX, bool Direction = false>
class Graph
{
public:
// 图的创建
Graph() = default;
Graph(const std::vector<v> &points)
{
m_vertexs.resize(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for (int i = 0; i < m_matrix.size(); ++i)
{
m_matrix[i].resize(points.size(), max_w);
}
}
// 添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v &src, const v &dest, const w &weight)
{
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
_AddEdge(posSrc,posDet,weight);
}
void Print()
{
// 打印顶点对应坐标
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
// 打印边
std::cout << " ";
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
// 打印矩阵
for (size_t i = 0; i < m_matrix.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
for (size_t j = 0; j < m_matrix[i].size(); ++j)
{
if (m_matrix[i][j] == max_w)
{
std::cout << "*"
<< " ";
}
else
{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
// 最小生成树,返回最小生成树权值,传入一个图,这个参数是输入输出参数,函数结束后,minTree是图的最小生成树
w Kruskal(Graph<v, w, max_w, Direction> &minTree){
size_t size = m_vertexs.size();
//初始化minTree
minTree.m_vertexs = m_vertexs;
minTree.m_mapIndex = m_mapIndex;
minTree.m_matrix.resize(size);
for (int i = 0; i < size; ++i){
minTree.m_matrix[i].resize(size, max_w);
}
std::priority_queue<Edge<w>, std::vector<Edge<w>>, rules> minQueue; //从小到大
// 将所有的边添加到优先级队列中
for (size_t i = 0; i < size; ++i){
//因为最小生成树,只在无向图中成立,所以只要遍历邻接矩阵一半即可
for (size_t j = 0; j < i; ++j){
if (m_matrix[i][j]!= max_w){
minQueue.push(Edge<w>(i, j, m_matrix[i][j]));
}
}
}
// 选出n-1条边
int iDstSize = 0;
w totalW = w(); //总共的权值
// 创建并查集来标记是否成环,大小为图顶点个数
UnionFindSet ufs(size);
while (!minQueue.empty())
{
Edge<w> minEdge = minQueue.top();
minQueue.pop();
//判断这条边顶点是否在并查集中,在并查集中构成环,不符合最小生成树的定义
if(!ufs.IsInSet(minEdge.srcPos, minEdge.detPos))
{
//打印选的边测试
std::cout << m_vertexs[minEdge.srcPos] << "->" << m_vertexs[minEdge.detPos] << "权值:" << minEdge.weight << std::endl;
minTree._AddEdge(minEdge.srcPos, minEdge.detPos, minEdge.weight);
ufs.Union(minEdge.srcPos, minEdge.detPos);
iDstSize++;
totalW += minEdge.weight;
}
}
if(iDstSize == size-1)
{
// 找到最小生成树
return totalW;
}
else{
// 没有找到最小生成树,返回权值的默认值,如果权值是整数则返回0
return w();
}
}
private :
std::vector<v> m_vertexs; // 顶点集合
std::map<v, int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
// 获取顶点下标
size_t GetPointIndex(const v &point) const
{
typename std::map<v, int>::const_iterator ptr = m_mapIndex.find(point);
if (ptr != m_mapIndex.end())
return ptr->second;
else
{
throw std::invalid_argument("point not found");
return -1;
}
}
void _AddEdge(size_t srcPos, size_t detPos,const w& weight)
{
//区分有向图和无向图
m_matrix[srcPos][detPos] = weight;
if (!Direction)
{
//无向图,添加两条边关系
m_matrix[detPos][srcPos] = weight;
}
}
};
}
Prim算法
与Kruskal算法类似,Prim算法也是通用最小生成树算法的一个特例。
Prim算法的工作原理与Dijkstra最短路径算法类似,也是使用局部贪心算法。
算法说明:
-
开始指出一个起点,从起点开始找最小生成树。
-
Prim算法将所有顶点分成两部分:已选入的点,未选入的点。
-
从未选入部分顶点中选出一个点,再从选入的点钟选择一个点。
-
若两点,直接相连构成边,且这条边是所选择中权值最小的。则将选择的点添加到已选择部分顶点集合中。
例如:

-
设起点从a开始
-
开始将顶点分成两部分X、Y
X = {a} Y = {b, c, d, e, f, g, h, i}
-
X集合中选一个点a;Y中选择一个点,两个点要直接相连。
Y中只能选b、h。又要选权值最小的边,所以这个点只能选择b。
此后,X = {a,b};Y = {c,d,e,f,g,h,i}

特殊情况:如果权值相同任意选择点即可。
后续重复这个过程即可,第二步选择的顶点组如下:
X = a Y = h 权值:8
或者
X = b Y = c 权值:8
这两条权值相同,任意选择即可。

此时 X = {a,b,c} Y = {d,e,f,g,h,i}
直到把Y集合所有点选择完毕即可。这样可以避免选择成环。不需要并查集来判断成环。
但是,这里处理时使用优先级队列,因为优先级队列开始时把顶点周围的所有边添加进入,而优先级队列又不支持指定删除元素。
所以当优先级队列弹出最小权值边的时候,还需要判断这条边的两个顶点是不是在同一个集合X中,如果在集合X,说明这条边构成环。
#include <vector>
#include <map>
#include <queue>
#include <iostream>
template <class w>
struct Edge
{
int detPos;
int srcPos;
w weight;
Edge<w> *next;
Edge(int _srcPos, int _detPos, const w &_weight)
: detPos(_detPos), srcPos(_srcPos), weight(_weight), next(nullptr)
{}
};
struct rules{
template <class w>
bool operator()(const Edge<w> &left, const Edge<w> &right) const{
return left.weight > right.weight; //从小到大
}
};
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix
{
// 邻接矩阵保存边关系
template <class v, class w, w max_w = INT_MAX, bool Direction = false>
class Graph
{
public:
// 图的创建
Graph() = default;
Graph(const std::vector<v> &points)
{
m_vertexs.resize(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for (int i = 0; i < m_matrix.size(); ++i)
{
m_matrix[i].resize(points.size(), max_w);
}
}
// 添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v &src, const v &dest, const w &weight)
{
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
_AddEdge(posSrc,posDet,weight);
}
void Print()
{
// 打印顶点对应坐标
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
// 打印边
std::cout << " ";
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
// 打印矩阵
for (size_t i = 0; i < m_matrix.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
for (size_t j = 0; j < m_matrix[i].size(); ++j)
{
if (m_matrix[i][j] == max_w)
{
std::cout << "*"
<< " ";
}
else
{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
// 最小生成树,返回最小生成树权值,传入一个图,这个参数是输入输出参数,函数结束后,minTree是图的最小生成树
//Src:Prim算法传入的起始点
w Prim(Graph<v, w, max_w, Direction> &minTree, const v &Src){
size_t posSrc = GetPointIndex(Src);
//初始化minTree
size_t size = m_vertexs.size();
minTree.m_vertexs = m_vertexs;
minTree.m_mapIndex = m_mapIndex;
minTree.m_matrix.resize(size);
for(size_t i = 0; i < size; ++i){
minTree.m_matrix[i].resize(size, max_w);
}
std::vector<bool> X(size,false); //已经加入的顶点的集合
std::vector<bool> Y(size,true); //未加入的顶点的集合
X[posSrc] = true;
Y[posSrc] = false;
//从两个集合中选择两个点,构成权值最小的边
std::priority_queue<Edge<w>, std::vector<Edge<w>>, rules> minQueue;
//将这个顶点连接的边 入队列
for(size_t i = 0;i < size; i++){
if(m_matrix[posSrc][i]!= max_w){
minQueue.push(Edge<w>(posSrc, i, m_matrix[posSrc][i]));
}
}
//选择权值最小的边,添加到最小生成树中
size_t iEdgeSize = 0;
w weight = w();
while(!minQueue.empty()){
Edge<w> minEdge = minQueue.top();
minQueue.pop();
//如果这条边的两个顶点在一个集合X中,说明构成环
if(X[minEdge.detPos]){
//构成环
continue;
}
std::cout << m_vertexs[minEdge.srcPos] << "->" << m_vertexs[minEdge.detPos] << "权值:" << minEdge.weight << std::endl;
minTree._AddEdge(minEdge.srcPos, minEdge.detPos, minEdge.weight);
X[minEdge.detPos] = true;
Y[minEdge.detPos] = false;
iEdgeSize++;
weight += minEdge.weight;
//size个点,选size-1条边就可以结束循环了
if(iEdgeSize == size-1){
break;
}
//posSrc --- posDst,把posDst点所有边(除了posSrc---posDst)加入优先级队列
//所以posDst的另一个点不在集合X即可
for (size_t i = 0; i < size; i++)
{
if(m_matrix[minEdge.detPos][i]!= max_w &&!X[i]){
minQueue.push(Edge<w>(minEdge.detPos, i, m_matrix[minEdge.detPos][i]));
}
}
}
if(iEdgeSize == size-1){
return weight;
}
else{
return w();
}
}
private :
std::vector<v> m_vertexs; // 顶点集合
std::map<v, int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
// 获取顶点下标
size_t GetPointIndex(const v &point) const
{
typename std::map<v, int>::const_iterator ptr = m_mapIndex.find(point);
if (ptr != m_mapIndex.end())
return ptr->second;
else
{
throw std::invalid_argument("point not found");
return -1;
}
}
void _AddEdge(size_t srcPos, size_t detPos,const w& weight)
{
//区分有向图和无向图
m_matrix[srcPos][detPos] = weight;
if (!Direction)
{
//无向图,添加两条边关系
m_matrix[detPos][srcPos] = weight;
}
}
};
}
void TestGraphPrim()
{
std::vector<char>vet{ 'a','b','c','d','e','f','g','h','i' };
matrix::Graph<char, int> g(vet);
g.AddEdge('a', 'b', 4);
g.AddEdge('a', 'h', 8);
g.AddEdge('b', 'c', 8);
g.AddEdge('b', 'h', 11);
g.AddEdge('c', 'i', 2);
g.AddEdge('c', 'f', 4);
g.AddEdge('c', 'd', 7);
g.AddEdge('d', 'f', 14);
g.AddEdge('d', 'e', 9);
g.AddEdge('e', 'f', 10);
g.AddEdge('f', 'g', 2);
g.AddEdge('g', 'h', 1);
g.AddEdge('g', 'i', 6);
g.AddEdge('h', 'i', 7);
matrix::Graph<char, int> primTree;
int iWeight = g.Prim(primTree,'a');
std::cout << "Prim:" << iWeight << std::endl;
primTree.Print();
}
int main(){
TestGraphPrim();
return 0;
}
最后找到的最小生成树为:

5.最短路径问题(Dijkstra,Bellman-Ford,Floyd-Warshall算法 )
1. 最短路径
在网图和非网图中,最短路径的含义是不同的。由于非网图没有边上的权值,所谓最短路径,其实指的就是两个顶点之间经过的边数最少的路劲(即可以理解为把每一条边的权值看作是1)。
对于网图来说,最短路径,是指两顶点之间经过的边上的权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。
求带权有向图G的最短路径问题一般可分为两类:
-
一是单源最短路径,即求图中某一个顶点到其它顶点的最短路径,可以通过经典的 Dijkstra(迪杰斯特拉)算法求解;
-
二是求每对顶点间的最短路径,可通过Floyd(弗洛伊德)算法来求解。
2. 迪杰斯特拉(Dijkstra)算法
Dijkstra算法思路是设置一个集合S记录已求得的最短路径的顶点,初始时把源点V0(图中的某个顶点)放入S,集合S每并入一个新顶点 Vi,都要修改源点V0到集合 V-S 中顶点当前的最短路径长度值。
在构造过程中需要两个辅助数组:
-
dist[ ] :记录从源点V0到其他各顶点当前的最短路径长度,它的初态为:若从 V0 到 Vi有直接路径(即V0 和 Vi邻接),则dist[ i ]为这两个顶点边上的权值;否则置 dist[ i ] 为 ∞。
-
path[ ]:path[ i ]表示从源点到顶点 i 之间的最短路径的前驱结点。在算法结束时,可以根据其值追溯到源点 V0 到 Vi 的最短路径。
假设从顶点V0 = 0出发,邻接矩阵Edge表示带权无向图,Edge[i] [j]表示无向边(i,j)的权值,若不存在无向边(i,j),则Edge[i] []为 ∞。
Dijkstra算法步骤如下:
-
初始化:集合S初始化为{0},dist[] 的初始值dist[i] = Edge[0] [i],path[] 的初始值path[i] = -1,i = 1,2,…,n-1。
-
从顶点集合V - S 中选出Vj,满足dist[j] = Min{dist[i] | Vi ∈ V - S},Vj就是当前求的一条从V0出发的最短路径的终点,令S = S∪{j}。
-
修改从V0 出发到集合V - S上任一顶点Vk 可达的最短路径长度:
若 dist[j] + Edge[j] [k] < dist[k],则更新 dist[k] = dist[j] + Edge[j] [k] ,并修改path[k] = j(即修改顶点Vk的最短路径的前驱节点)。
-
重复 2 ~ 3操作共n-1次,直到所有的顶点都包含在S中。
解释下步骤3),每当一个顶点加入S后,可能需要修改源点V0 到集合 V-S中的可达顶点当前的最短路径长度。
下面举一个例子。如下图所示,源点为V0,初始时S = {V0},dist[1] = 6, dist[2] = 3,当V2并入集合S后,dist[1] 需要更新为 5(其比6小,即说明两点之间不是直线最短,要根据两点之间路径的权值之和来看)。

下面来讲解利用Dijkstra算法来求下图中的顶点 0 出发至其余顶点的最短路径的过程。

初始化:集合S初始化为{V0},V0可达V1和V2,其余顶点不可达,因此dist[]数组和path[]数组的设置如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 6 | 3 | ∞ | ∞ | ∞ | ∞ |
| path[i] | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
第一轮:
选出最小dist[2],将顶点V2并入集合S,此时已找到V0到V2的最短路径,S = {V0,V2}。
当V2加入到S后,从V0到集合V-S中可到达顶点的最短路径长度可能会发生变化;因此需要更新dist[]数组。
V2可达V1,因V0->V2->V1的距离5比dist[1] = 6小,更新dist[1] = 5,并修改path[1] = 2(即V1的最短路径的前驱为V2);
V2可达V3,V0->V2->V3的距离8比dist[3] = ∞ 小,更新dist[3] = 8,path[3] = 2;
V2可达V5,V0->V2->V5的距离10小于dist[5] = ∞,更新dist[5] = 10,path[5] = 2。
V2再无到达其余的顶点的路径,结束这一轮,此时dist[]数组和path[]数组如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 5 | 3 | 8 | ∞ | 10 | ∞ |
| path[i] | -1 | 2 | -1 | 2 | -1 | 2 | -1 |
第二轮:
选出最小值dist[1],将顶点V1并入集合S,此时已找到V0到V1的最短路径,S = {V0,V2,V1}。
然后更新dist[]数组和path[]数组,V1 可达V3,V0->V2->V1->V3的距离6小于dist[3] = 8,更新dist[3] = 6,path[3] = 1;
V1可达V2,但V2已经在集合S中,故不进行操作;
V1可达V4,V0->V2->V1->V4的距离9小于dist[4] = ∞,更新dist[4] = 9,path[4] = 1。
V1已无到达其余顶点的路径,结束此轮,此时dist[]数组和path[]数组如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 5 | 3 | 6 | 9 | 10 | ∞ |
| path[i] | -1 | 2 | -1 | 1 | 1 | 2 | -1 |
第三轮:
选出最小值dist[3],将顶点V3并入集合S,此时已找到V0到V3的最短路径,S = {V0,V2,V1,V3};接着更新dist[]数组和path[]数组。
V3可到达V4,V0->V2->V1->V3->V4的距离为9,等于dist[4] = 9,不做更新;
V3可到达V5,V0->V2->V1->V3->V5的距离为12,大于dist[5] = 10,不做更新。
V3再无达到其余顶点的路径,结束此轮,此时dist[]数组和path[]数组如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 5 | 3 | 6 | 9 | 10 | ∞ |
| path[i] | -1 | 2 | -1 | 1 | 1 | 2 | -1 |
第四轮:
选出最小值dist[4],将顶点V4并入集合S,此时已找到V0到V4的最短路径,S = {V0,V2,V1,V3,V4};继续更新dist[]数组和path[]数组。
V4可到V5,V0->V2->V1->V4->V5的距离11,大于dist[5] = 10,故不进行更新操作;
V4可到V6,V0->V2->V1->V4->V6的距离11,小于dist[6] = ∞,更新 dist[6] = 11,path[6] = 4。
V4再无达到其余顶点的路径,结束此轮,此时dist[]数组和path[]数组如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 5 | 3 | 6 | 9 | 10 | 11 |
| path[i] | -1 | 2 | -1 | 1 | 1 | 2 | 4 |
第五轮:
选出最小值 dist[5],将顶点 V5 并入集合S,此时已找到 V0 到 V5的最短路径,S = { V0,V2,V1,V3,V4,V5}。然后dist[]数组和path[]数组。
V5 可到 V6, V0 -> V2 -> V5 -> V6 的最短路径 13 大于 dist[6],故不进行更新操作。
V6 再无达到其余顶点的路径,结束此轮,此时dist[]数组和path[]数组如下:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dist[i] | ∞ | 5 | 3 | 6 | 9 | 10 | 11 |
| path[i] | -1 | 2 | -1 | 1 | 1 | 2 | 4 |
第六轮:
选出最小值 dist[6],将顶点 V6 并入集合,此时全部顶点都已包含在S中,结束算法。
整个算法每一轮的结果如下:

总结:Dijkstra算法就是最开始选离源点V0最近的点,然后选好点后,
再从选好的点,看其邻接点的距离dist[]是否减小,减小就修改dist[]和path[];否则就不进行修改操作。
Dijkstra算法基于贪心策略,用邻接矩阵表示图时,来使用Dijkstra算法,其时间复杂度为O(n*n)。
当边上带有负权值时,Dijkstra算法并不适用。
使用dist[]数组和path[]数组,求最短路径。 这里介绍一个例子,其他顶点依次类推。
V0 到 V6的最短路径,先利用dist[6] = 11得出V0 到 V6的距离,然后利用path[]的得出路径。
path[6] = 4,顶点V6的前驱顶点是V4,再由path[4] = 1,表示V4的前驱是V1,path[1] = 2,表示V1的前驱是V2,path[2] = -1,结束。
最后可以得到V0到V6的最短路径为V6<-V4<-V1<-V2<-V0,即V0->V2->V1->V4->V6
贪心策略也叫贪心算法(greedy algorithm)或贪婪算法,是一种强有力的穷举搜索策略,它通过一系列选择来找到问题的最优解。
在每个决策点,它都会做出当时看来是最优的选择,一旦选择后就无需回溯。
简单来说,贪心策略是一种“步步为营”的策略——只要做好眼前的每一步,就自然会在未来得到最好的结果,并且做过的决策就是是最好的决策,无需再次检查。
很多时候,贪心法并不能保证得到最优解,它能得到的是较为接近最优解的较好解,因此贪心法经常被用来解决一些对结果精度要求不高的问题。
#include <vector>
#include <map>
#include <queue>
#include <algorithm>
#include <iostream>
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix
{
// 邻接矩阵保存边关系
template <class v, class w, w max_w = INT_MAX, bool Direction = false>
class Graph
{
public:
// 图的创建
Graph() = default;
Graph(const std::vector<v> &points)
{
m_vertexs.resize(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for (int i = 0; i < m_matrix.size(); ++i)
{
m_matrix[i].resize(points.size(), max_w);
}
}
// 添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v &src, const v &dest, const w &weight)
{
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
_AddEdge(posSrc,posDet,weight);
}
void Print()
{
// 打印顶点对应坐标
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
// 打印边
std::cout << " ";
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
// 打印矩阵
for (size_t i = 0; i < m_matrix.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
for (size_t j = 0; j < m_matrix[i].size(); ++j)
{
if (m_matrix[i][j] == max_w)
{
std::cout << "*"
<< " ";
}
else
{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
// 单源最短路径
// src:源顶点,dist:保存src到各个顶点的最短距离,path:保存最短路径的节点
void Dijkstra(const v &src, std::vector<w> &dist, std::vector<int> &path)
{
size_t srcPos = GetPointIndex(src);
size_t size = m_vertexs.size();
dist.resize(size, max_w);
path.resize(size, -1);
dist[srcPos] = 0; //源顶点到自己本身最短距离为0
path[srcPos] = srcPos; //源顶点的最短路径的父节点为自己
std::vector<bool> S(size, false); //已经确定最短路径的顶点的集合
//循环判断所有的顶点
for (size_t time = 0; time < size; time++){
//选不在S集合 最短路径的顶点,更新其他路径
//选p点,p点不在s集合中
int p = 0;
w min = max_w; //最小权值
for(size_t i = 0; i < size; i++){
if(!S[i] && dist[i] < min){
p = i;
min = dist[i];
}
}
//把p点加入S集合
S[p] = true;
//松弛更新 src->p + p->p的邻接节点 与 src->p邻接节点的权值相比较,小则更新
for(size_t adP = 0;adP < size; adP++){
//找到p点所有的邻接顶点
if(!S[adP] && m_matrix[p][adP] != max_w){
if (dist[adP] > dist[p] + m_matrix[p][adP])
{
dist[adP] = dist[p] + m_matrix[p][adP];
//更新这个顶点最短路径的父节点为p点
path[adP] = p;
}
}
}
}
}
//打印最短路径
void PrintShortPath(const v& src, const std::vector<w>& dist, const std::vector<int>& path)
{
size_t srcPos = GetPointIndex(src);
size_t size = m_vertexs.size();
//计算src点到其他点的最短路径长度,src到src=0不用计算
for(size_t i = 0; i < size; i++)
{
if(i == srcPos)
continue;
//src 到顶点i的最短路径长度
std::vector<int> minPath;
size_t pos = i;
std::cout << "最短路径为:";
while(pos!= srcPos)
{
minPath.push_back(pos);
pos = path[pos];
}
minPath.push_back(srcPos);
std::reverse(minPath.begin(), minPath.end());
for (auto index : minPath)
{
std::cout << m_vertexs[index] << "->";
}
std::cout << "长度:" << dist[i] << std::endl;
}
}
private :
std::vector<v> m_vertexs; // 顶点集合
std::map<v, int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
// 获取顶点下标
size_t GetPointIndex(const v &point) const
{
typename std::map<v, int>::const_iterator ptr = m_mapIndex.find(point);
if (ptr != m_mapIndex.end())
return ptr->second;
else
{
throw std::invalid_argument("point not found");
return -1;
}
}
void _AddEdge(size_t srcPos, size_t detPos,const w& weight)
{
//区分有向图和无向图
m_matrix[srcPos][detPos] = weight;
if (!Direction)
{
//无向图,添加两条边关系
m_matrix[detPos][srcPos] = weight;
}
}
};
}
#include "Graph.hpp"
using namespace std;
void TestGraphDijkstra(){
std::vector<char> vet{ '0','1','2','3','4','5','6' };
matrix::Graph<char, int> g(vet);
g.AddEdge('0', '1', 6);
g.AddEdge('0', '2', 3);
g.AddEdge('1', '2', 2);
g.AddEdge('1', '3', 1);
g.AddEdge('1', '4', 4);
g.AddEdge('2', '3', 5);
g.AddEdge('2', '5', 7);
g.AddEdge('3', '4', 3);
g.AddEdge('3', '5', 6);
g.AddEdge('4', '5', 2);
g.AddEdge('4', '6', 2);
g.AddEdge('5', '6', 3);
vector<int> dist;
vector<int> parentPath;
g.Dijkstra('0', dist, parentPath);
g.PrintShortPath('0',dist, parentPath);
}
int main(){
TestGraphDijkstra();
return 0;
}
算法的空间复杂度:开辟了二个大小等于顶点个数的数组,O(N)
时间复杂度:N个顶点,每个顶点都需要遍历一遍这个顶点的邻接顶点。时间复杂度O(N^2);
3.贝尔曼-福特(Bellman-Ford)单源最短路径算法
Bellman-Ford 算法是一种用于计算带权有向图中单源最短路径(SSSP:Single-Source Shortest Path)的算法。该算法由 Richard Bellman 和 Lester Ford 分别发表于 1958 年和 1956 年,而实际上Edward F. Moore 也在 1957 年发布了相同的算法,因此,此算法也常被称为 Bellman-Ford-Moore 算法。
Bellman-Ford 算法 和 Dijkstra 算法 同为解决单源最短路径的算法。对于带权有向图 G = (V, E),其中 V 为顶点数量,E 为边的数量,Dijkstra 算法要求图 G 中边的权值均为非负,而 Bellman-Ford 算法 能适应一般的情况(即存在负权边的情况)。一个实现的很好的 Dijkstra 算法比 Bellman-Ford 算法的运行时间要低。
Bellman-Ford 算法采用 动态规划(Dynamic Programming)进行设计,实现的时间复杂度为 O(VE),由于使用邻接矩阵存储图关系,因此E = V*V。
Dijkstra 算法采用 贪心算法(Greedy Algorithm)范式进行设计,普通实现的时间复杂度为 O(V2),若基于 斐波那契堆(Fibonacci heap) 的最小优先队列实现版本则时间复杂度为 O(E + VlogV)。
Bellman-Ford 算法描述:

根据上图可知,起点s到图中的其他顶点。要么直接相连,要么通过其他的顶点间接相连。
简述为:s->j(代表图中的所有边) 或者 s->i+i->j
- 以s->i这条边为起始边,开始找最终边,如果找到 (s->i->j)< s->j 时,进行松弛变化,将j的最小权值 修改为 s->i->j 边的权值和。
- 所有边都需要 进行与其他边的 松弛判断。
注意:
这里可能出现权值与路径不匹配的问题。
例如:
开始计算 s->t 的最小值为6,此时计算s->t->z的值为2。(图c)
之后s->t的最小值变成了s->y->x->t的权值和2(图d);此时必须修改上一步的结果(图e);如果没有修改就会出现路径和最小权值不匹配的情况。
只要更新出最短路径,就可能影响其他路径。解决这个问题的方式就是再整体更新一次。一旦更新有可能会影响其他路径,但是最多更新顶点次数,所以直接暴力更新顶点次数次即可。
如果这次遍历没有更新边,则后面的重复暴力就可以停止了。
负权回路问题
Bellman-Ford算法可以解决负权图的单源最短路径问题。但是解决不了负权回路问题。
负权回路:s->s最短路径权值计算为负数。
例如:

如上图s->s最短路径为s->t->y->s 为-1,那么只要重读次数足够多,s->s的最短路径可以一直变小。
重复两次为-2,重读三次为-3,有根据上文可知更新最短路径,就可能影响其他路径。所以此时最短路径会一直更新,Bellman-Ford算法没办法解决。
#include <vector>
#include <map>
#include <queue>
#include <algorithm>
#include <iostream>
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix
{
// 邻接矩阵保存边关系
template <class v, class w, w max_w = INT_MAX, bool Direction = false>
class Graph
{
public:
// 图的创建
Graph() = default;
Graph(const std::vector<v> &points)
{
m_vertexs.resize(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for (int i = 0; i < m_matrix.size(); ++i)
{
m_matrix[i].resize(points.size(), max_w);
}
}
// 添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v &src, const v &dest, const w &weight)
{
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
_AddEdge(posSrc,posDet,weight);
}
void Print()
{
// 打印顶点对应坐标
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
// 打印边
std::cout << " ";
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
// 打印矩阵
for (size_t i = 0; i < m_matrix.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
for (size_t j = 0; j < m_matrix[i].size(); ++j)
{
if (m_matrix[i][j] == max_w)
{
std::cout << "*"
<< " ";
}
else
{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
// 单源最短路径
// src:源顶点,dist:保存src到各个顶点的最短距离,path:保存最短路径的节点
// 如果出现负权回路。函数返回false值
bool BellmanFord(const v &src, std::vector<w> &dist, std::vector<int> &path)
{
size_t size = m_vertexs.size();
size_t srcPos = GetPointIndex(src);
// vertor<w> dist,记录srcPos-其他顶点最短路径权值数组
dist.resize(size, max_w);
// vector<w> path,记录srcPos-其他顶点最短路径的父节点数组
path.resize(size, -1);
// 先更新srci->srci为缺省值0
dist[srcPos] = w();
//总体最多更新size轮
for (size_t time = 0; time < size; time++)
{
// i->j 更新松弛
bool bUpdate = false;
for (size_t i = 0; i < size; i++){
for(size_t j = 0; j < size; j++){
//srcPos -> i + i -> j 松弛关系
if(m_matrix[i][j] != max_w && (dist[i] + m_matrix[i][j] < dist[j])){
dist[j] = dist[i] + m_matrix[i][j];
path[j] = i;
bUpdate = true;
}
}
}
if(!bUpdate){
break; //这次没有更新出最短路径,可以退出循环了
}
}
//判断负权回路,如果退出循环还可以更新,就是负权回路返回false
//还能更新就是带负权回路
for (size_t i = 0; i < size; i++){
for(size_t j = 0; j < size; j++){
if(m_matrix[i][j]!= max_w && (dist[i] + m_matrix[i][j] < dist[j])){
return false;
}
}
}
return true;
}
//打印最短路径
void PrintShortPath(const v& src, const std::vector<w>& dist, const std::vector<int>& path)
{
size_t srcPos = GetPointIndex(src);
size_t size = m_vertexs.size();
//计算src点到其他点的最短路径长度,src到src=0不用计算
for(size_t i = 0; i < size; i++)
{
if(i == srcPos)
continue;
//src 到顶点i的最短路径长度
std::vector<int> minPath;
size_t pos = i;
std::cout << "最短路径为:";
while(pos!= srcPos)
{
minPath.push_back(pos);
pos = path[pos];
}
minPath.push_back(srcPos);
std::reverse(minPath.begin(), minPath.end());
for (auto index : minPath)
{
std::cout << m_vertexs[index] << "->";
}
std::cout << "长度:" << dist[i] << std::endl;
}
}
private :
std::vector<v> m_vertexs; // 顶点集合
std::map<v, int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
// 获取顶点下标
size_t GetPointIndex(const v &point) const
{
typename std::map<v, int>::const_iterator ptr = m_mapIndex.find(point);
if (ptr != m_mapIndex.end())
return ptr->second;
else
{
throw std::invalid_argument("point not found");
return -1;
}
}
void _AddEdge(size_t srcPos, size_t detPos,const w& weight)
{
//区分有向图和无向图
m_matrix[srcPos][detPos] = weight;
if (!Direction)
{
//无向图,添加两条边关系
m_matrix[detPos][srcPos] = weight;
}
}
};
}
#include "Graph.hpp"
using namespace std;
void TestBellmanFord()
{
vector<char> str = { 's','y','z','t','x' };
matrix::Graph<char, int, INT_MAX, true> g(str);
g.AddEdge('s', 't', 6);
g.AddEdge('s', 'y', 7);
g.AddEdge('y', 'z', 9);
g.AddEdge('y', 'x', -3);
g.AddEdge('z', 's', 2);
g.AddEdge('z', 'x', 7);
g.AddEdge('t', 'x', 5);
g.AddEdge('t', 'y', 8); //测试负权回路 8 改为 -8
g.AddEdge('t', 'z', -4);
g.AddEdge('x', 't', -2);
//测试负权回路
//g.AddEdge('y', 's', 1);
vector<int> dist;
vector<int> parentPath;
if(g.BellmanFord('s', dist, parentPath))
g.PrintShortPath('s', dist, parentPath);
else
std::cout << "有负权回路" << std::endl;
}
int main(){
TestBellmanFord();
return 0;
}
4.弗洛伊德(Floyd-Warshall)多源最短路径算法
多源最短路径:源顶点是图中的所有顶点,求图中任意两点的最短路径。
注意:
- Floyd-Warshall可以解决负数权值问题。
- 如果以所有点为源点,使用 Dijkstra算法 也可以算出图中任意两点的最短路径。但是 Dijkstra算法 不能带负数权值,Bellman-Ford算法 效率太低。
因为Floyd-Warshall算法要以图中任意顶点为源顶点。
根据上面分析可知,dist(记录源顶点到其他顶点的最短路径)数组应该是二维数组。
path(通过双亲表示法记录最短路径的节点)也应该是二维数组。
算法思路
Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。
设k是p的一个中间节点,那么从i到j最短路径p就被分成 i->k 和 k->j 的两段最短路径p1,p2;
p1是从 i->k 且 中间节点属于{1,2,…,k-1} 而取得的一条最短路径,
p2是从 k->j 且 中间节点属于{1,2,…,k-1} 而取得的一条最短路径。

简单来讲,设图中有n个顶点,
任意两点之间可能没有顶点,也可能有顶点。任意两点之间最多有(n-2)个 k 顶点。
Floyd-Warshall 算法的原理是动态规划;
设Di,j,k为从 i 到 j 的只以(1…k)集合中的节点为为中间节点 的 最短路径 的长度。
-
若最短路径经过点k,则Di,j,k = Di,k,k-1 + Dk,j,k-1;
-
若最短路径不经过点k,则Di,j,k = Di,j,k-1。
因此,Di,j,k = min(Di,j,k-1,Di,k,k-1 + Dk,j,k-1)
简单来讲:如果 k 是 i->j 之间的点 且 i->k->j < i->j 说明经过中点k会使路径更短,更新 i->j 的路径长度,并且修改Path数组,记录更新后的父顶点。
例如:

左边的是权值矩阵,右边是父节点矩阵。与其他算法类似,只不过 Floyd-Warshall 算法要不图中的每个顶点都做一遍源节点,所以是二维数组。
#include <vector>
#include <map>
#include <queue>
#include <algorithm>
#include <iostream>
// v:顶点 w:权值 max_w:最大权值 Direction:判断图是有向图还是无向图
namespace matrix
{
// 邻接矩阵保存边关系
template <class v, class w, w max_w = INT_MAX, bool Direction = false>
class Graph
{
public:
// 图的创建
Graph() = default;
Graph(const std::vector<v> &points)
{
m_vertexs.resize(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
m_vertexs[i] = points[i];
m_mapIndex[points[i]] = i;
}
m_matrix.resize(points.size());
// 邻接矩阵
for (int i = 0; i < m_matrix.size(); ++i)
{
m_matrix[i].resize(points.size(), max_w);
}
}
// 添加边关系,输入两个点,以及这两个点连线边的权值
void AddEdge(const v &src, const v &dest, const w &weight)
{
size_t posSrc = GetPointIndex(src);
size_t posDet = GetPointIndex(dest);
_AddEdge(posSrc,posDet,weight);
}
void Print()
{
// 打印顶点对应坐标
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << "[" << i << "]->" << m_vertexs[i] << std::endl;
std::cout << std::endl;
}
// 打印边
std::cout << " ";
for (size_t i = 0; i < m_vertexs.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
}
std::cout << std::endl;
// 打印矩阵
for (size_t i = 0; i < m_matrix.size(); ++i)
{
std::cout << m_vertexs[i] << " ";
for (size_t j = 0; j < m_matrix[i].size(); ++j)
{
if (m_matrix[i][j] == max_w)
{
std::cout << "*"
<< " ";
}
else
{
std::cout << m_matrix[i][j] << " ";
}
}
std::cout << std::endl;
}
}
// 求多源最短路径
void FloydWarShall(std::vector<std::vector<w>> &dist, std::vector<std::vector<int>> &path)
{
size_t size = m_vertexs.size();
//初始化顶点矩阵 与 路径矩阵
dist.resize(size);
path.resize(size);
for (size_t i = 0; i < size; i++)
{
dist[i].resize(size, max_w);
path[i].resize(size, -1);
}
//直接相连的边更新初始化
for (size_t i = 0; i < size; i++)
{
for (size_t j = 0; j < size; j++)
{
if (m_matrix[i][j]!= max_w)
{
dist[i][j] = m_matrix[i][j];
path[i][j] = i; //i->j起点是i点
}
if(i == j){
//i->i时路径长度为0
dist[i][i] = 0;
}
}
}
//最短路径的更新i->{其他顶点}->j
//k作为中间点,尝试更新i->j的路径
for (size_t k = 0; k < size; k++)
{
for (size_t i = 0; i < size; i++)
{
for (size_t j = 0; j < size; j++)
{
//经过了k点
if (dist[i][k]!= max_w && dist[k][j]!= max_w)
{
if(dist[i][k] + dist[k][j] < dist[i][j])
{
//经过k更小,则更新长度
dist[i][j] = dist[i][k] + dist[k][j];
//找上一个与j邻接的节点
//k->j入过k与j直接相连,则path[i][j] = k
//但是k->j不一定直接相连 k->...->x->j,则path[i][j]=x,就是path[k][j]
path[i][j] = path[k][j];
}
}
}
}
}
}
//打印最短路径
void PrintShortPath(const v& src, const std::vector<w>& dist, const std::vector<int>& path)
{
size_t srcPos = GetPointIndex(src);
size_t size = m_vertexs.size();
//计算src点到其他点的最短路径长度,src到src=0不用计算
for(size_t i = 0; i < size; i++)
{
if(i == srcPos)
continue;
//src 到顶点i的最短路径长度
std::vector<int> minPath;
size_t pos = i;
std::cout << "最短路径为:";
while(pos!= srcPos)
{
minPath.push_back(pos);
pos = path[pos];
}
minPath.push_back(srcPos);
std::reverse(minPath.begin(), minPath.end());
for (auto index : minPath)
{
std::cout << m_vertexs[index] << "->";
}
std::cout << "长度:" << dist[i] << std::endl;
}
}
private :
std::vector<v> m_vertexs; // 顶点集合
std::map<v, int> m_mapIndex; // 顶点与下标的映射
std::vector<std::vector<w>> m_matrix; // 邻接矩阵
// 获取顶点下标
size_t GetPointIndex(const v &point) const
{
typename std::map<v, int>::const_iterator ptr = m_mapIndex.find(point);
if (ptr != m_mapIndex.end())
return ptr->second;
else
{
throw std::invalid_argument("point not found");
return -1;
}
}
void _AddEdge(size_t srcPos, size_t detPos,const w& weight)
{
//区分有向图和无向图
m_matrix[srcPos][detPos] = weight;
if (!Direction)
{
//无向图,添加两条边关系
m_matrix[detPos][srcPos] = weight;
}
}
};
}
void TestFloydWarShall(){
vector<char> str = { '1','2','3','4','5' };
matrix::Graph<char, int, INT_MAX, true> g(str);
g.AddEdge('1', '2', 3);
g.AddEdge('1', '3', 8);
g.AddEdge('1', '5', -4);
g.AddEdge('2', '4', 1);
g.AddEdge('2', '5', 7);
g.AddEdge('3', '2', 4);
g.AddEdge('4', '1', 2);
g.AddEdge('4', '3', -5);
g.AddEdge('5', '4', 6);
vector<vector<int>> vDist; vector<vector<int>> vPath;
g.FloydWarShall(vDist, vPath);
// 打印任意两点之间的最短路径
for (size_t i = 0; i < str.size(); ++i)
{
g.PrintShortPath(str[i], vDist[i], vPath[i]);
cout << endl;
}
}
int main(){
TestFloydWarShall();
return 0;
}
426

被折叠的 条评论
为什么被折叠?



