🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
文章目录
💎一、概念
🏆1.图概念
图用G表示,顶点集合用V表示,边用E表示
有向图:<x, y>和<y, x>是不同的,带有方向,G3和G4
无向图:<x, y>和<y, x>是一样的,没有方向,G1和G2
无向完全图:有n个顶点的无向图中,若有n * (n-1)/2条边 (参考等差数列求和),任意两个顶点间有且仅有一条边,如G1
有向完全图:有n个顶点的无向图若有n * (n-1)条边 (参考等差数列求和),任意两个顶点间有且仅有二条边,如G4
邻接顶点:x和y是通过<x,y>连接的,就是邻接顶点
顶点的度:等于入度和出度的和,入度是从外面连接到此顶点,出度是从这个顶点连接到外面,如G3的0顶点入度是1出度是1,度是2
径的路径长度是指该路径上各个边权值的总和
权值:边附带的数据信息
连通图:在无向图中任意一对顶点都是连通的
强连通图:在有向图中任意一对顶点都是连通的
生成树:一个连通图的最小连通子图作为该图的生成树,n个顶点的连通图的生成树有n个顶点和n-1条边
图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可。其边关系一般采用邻接矩阵或邻接表的方式保存。
🏆2.邻接矩阵
因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
- 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
- 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替。
- 邻接矩阵的优势是快速找到两个顶点之间的边,适合于边比较多的图;其劣势为要找一个顶点连出去的边,需要遍历其他顶点,因此时间复杂度为O(N),并且如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间。
顶点定义
①在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。
②当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。
③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
④邻接矩阵表示法的空间复杂度为O ( n 2 ),其中n为图的顶点数∣V |。
⑤用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
⑥ 稠密图适合使用邻接矩阵的存储表示。
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和弧数
}MGraph;
🏆邻接矩阵创建和打印
邻接矩阵创建
(1)输入总顶点数和总边数。
(2)依次输入点的信息存入顶点表中。
(3)初始化邻接矩阵,使每个权值初始化为极大值。
(4)构造邻接矩阵。
Status CreateUDN(AMGraph &G){
//采用邻接矩阵表示法,创建无向网G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(int i = 0; i<G.vexnum; ++i)
cin>>G.vexs[i]; //依次输入点的信息
for(int i = 0; i<G.vexnum;++i) //初始化邻接矩阵
for(int j = 0; j<G.vexnum;++j)
G.arcs[i][j] = INT_MAX; //边的权值均置为极大值
for(int k = 0; k<G.arcnum;++k){ //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边所依附的顶点及边的权值
int i = LocateVex(G, v1);
int j = LocateVex(G, v2); //确定v1和v2在G中的位置
G.arcs[i][j] = w; //边<v1, v2>的权值置为w
G.arcs[j][i]= G.arcs[i][j]; //置<v1, v2>的对称边<v2,v1>的权值为w
}
return true;
}
int LocateVex(AMGraph G, VertexType u) {
//图G中查找顶点u,存在则返回顶点表中的下标;否则返回-1
for(int i=0;i<G.vexnum;++i)
if(u==G.vexs[i])
return i;
return -1;
}
//打印邻接矩阵
void Print(MGraph g){
for(int i=0;i<g.vn;i++){
for(int j=0;j<g.vn;j++){
cout<<g.arc[i][j]<<" ";
}
cout<<endl;
}
}
🏆邻接矩阵删除点和边
//删除点
int DeleteVex(AMGraph& G, VerTexType v) {
for (int i = 0; i < G.vexnum; i++) {
if (G.vexs[i] == v) { //找到v
swap(G.vexs[i], G.vexs[G.vexnum - 1]); //V和最后一个交换
for (int j = 0; j < G.vexnum; j++) {
G.arcs[i][j] = G.arcs[G.vexnum - 1][j];
G.arcs[j][i] = G.arcs[j][G.vexnum - 1];
}
G.vexnum--;
break;
}
}
return OK;
}
//删除边
int DeleteArc(AMGraph& G, VerTexType v, VerTexType w) {
int i = located(G, v);
int j = located(G, w);
if (i != j && i != -1 && j != -1) {
G.arcs[i][j] = G.arcs[j][i] = 0;
G.arcnum--;
}
return OK;
}
🏆3.邻接表
无向图邻接表存储
有向图邻接表存储
邻接表的优势为:
找一个点相连的顶点的边很快。
适合边比较少,比较稀疏的图。
劣势为:
要确认两个顶点是否相连为O(N),因为要把所有的边都找一遍。
顶点定义
#define MAXVEX 100 //图中顶点数目的最大值
type char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
/*顶点结点*/
typedef struct VNode{
VertexType data; //存储顶点信息
EdgeNode *firstarc //边表头指针
}VNode, AdjList[MAXVEX]; //AdjList表示邻接表类型
// AdjList v 相当于 VNode v[MAXVEX]
/*边结点*/
typedef struct ArcNode{
int adjvex; //该边指向顶点位置
EdgeType weight; //权值,对于非网图可以不需要
struct ArcNode* nextarc; //指向下一个邻接点
}ArcNode
;
/*邻接表*/
typedef struct{
AdjList adjList;
int vexnum, arcnum; //图中当前顶点数和边数
}
邻接表创建
(1)输入总顶点数和总边数。
(2)建立顶点表
依次输入点的信息存入顶点表中
使每个表头结点的指针域初始化为NULL
(3)创建邻接表
依次输入每条边依附的两个顶点
确定两个顶点的序号i和j,建立边结点
将此边结点分别插入到v,和v;对应的两个边链表的头部
Status CreateUDG(ALGraph &G){ //采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(int i = 0; i<G.vexnum; ++i){ //输入各点,构造表头结点表
cin>> G.adjList[i].data; //输入顶点值
G.adjList[i].firstarc=NULL; //初始化表头结点的指针域
}
for(int k = 0; k<G.arcnum;++k){ //输入各边,构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
int i = LocateVex(G, v1);
int j = LocateVex(G, v2);
ArcNode* p1=new ArcNode; //生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc= G.vertices[i].firstarc;
G.adjList[i].firstarc=p1; //将新结点*p1插入顶点vi的边表头部
ArcNode* p2=new ArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex=i; //邻接点序号为i
p2->nextarc= G.adjList[j].firstarc;
G.adjList[j].firstarc=p2; //将新结点*p2插入顶点vj的边表头部
}
return true;
}
//打印邻接表
void print(void)
{
for (int k = 0; k < v; k++)
{
cout << adjList[k].data << ":";
ArcNode*p = G.adjList[i].first;
while(p){
cout<<p->adjvex<<" "<<p->weight<<"\t";
p=p->next;
}
}
}
🏆4.代码
邻接矩阵
//V表示中节点的属性,W表示边的权值,Direction表示是否为有向图,默认是无向图,默认值权值是最大 template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: Graph(const V* vertexs, size_t n) { //将顶点全部插入集合 _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(vertexs[i]); _indexMap[vertexs[i]] = i; } //初始化邻接矩阵 _matrix.resize(n); for (auto& e : _matrix) { e.resize(n, MAX_W); } } //获取顶点的下标 size_t GetVertexIndex(const V& v) { //遍历map auto it = _indexMap.find(v); if (it != _indexMap.end()) { return it->second; } else { //throw invalid_argument("不存在的顶点"); assert(false); return -1; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srcindex = GetVertexIndex(src); size_t dstindex = GetVertexIndex(dst); _matrix[srcindex][dstindex] = w; if (Direction == false) { //无向图,双向的 _matrix[dstindex][srcindex] = w; } } void Print() { // 打印顶点和下标映射关系 for (size_t i = 0; i < _vertexs.size(); ++i) { cout << _vertexs[i] << "-" << i << " "; } cout << endl << endl; cout << " "; for (size_t i = 0; i < _vertexs.size(); ++i) { cout << i << " "; } cout << endl; // 打印矩阵 for (size_t i = 0; i < _matrix.size(); ++i) { cout << i << " "; for (size_t j = 0; j < _matrix[i].size(); ++j) { if (_matrix[i][j] != MAX_W) cout << _matrix[i][j] << " "; else cout << "#" << " "; } cout << endl; } cout << endl << endl; // 打印所有的边 for (size_t i = 0; i < _matrix.size(); ++i) { for (size_t j = 0; j < _matrix[i].size(); ++j) { if (i < j && _matrix[i][j] != MAX_W) { cout << _vertexs[i] << "-" << _vertexs[j] << ":" << _matrix[i][j] << endl; } } } } private: vector<V> _vertexs; //顶点集合 map<V, int> _indexMap; //顶点和下标的映射关系 vector<vector<W>> _matrix; //存储边集合的矩阵,邻接矩阵 }; void TestGraph() { string a[] = { "张三","李四","王五","赵六","周七" }; Graph<string, int>g1(a, sizeof(a) / sizeof(a[0])); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 10); g1.AddEdge("李四", "周七", 50); g1.Print(); }
邻接表
template<class W> struct Edge { int _srcIndex; int _dstIndex;//目标点下标 W _w; //权值 Edge<W>* _next; Edge(const W& w) :_srcIndex(-1) , _dstIndex(-1) , _w(w) , _next(nullptr) { } }; //V表示中节点的属性,W表示边的权值,Direction表示是否为有向图,默认是无向图,默认值权值是最大 template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { typedef Edge<W> Edge; public: Graph(const V* vertexs, size_t n) { //将顶点全部插入集合 _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(vertexs[i]); _indexMap[vertexs[i]] = i; } //初始化邻接矩阵 _tables.resize(n); } //获取顶点的下标 size_t GetVertexIndex(const V& v) { //遍历map auto it = _indexMap.find(v); if (it != _indexMap.end()) { return it->second; } else { //throw invalid_argument("不存在的顶点"); assert(false); return -1; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srcindex = GetVertexIndex(src); size_t dstindex = GetVertexIndex(dst); Edge* eg = new Edge(w); eg->_srcIndex = srcindex; eg->_dstIndex = dstindex; eg->_next = _tables[srcindex]; _tables[srcindex] = eg; if (Direction == false) { //无向图的起点和终点相互颠倒 Edge* eg = new Edge(w); eg->_srcIndex = dstindex; eg->_dstIndex = srcindex; eg->_next = _tables[dstindex]; _tables[dstindex] = eg; } } void Print() { // 打印顶点和下标映射关系 for (size_t i = 0; i < _vertexs.size(); ++i) { cout << _vertexs[i] << "-" << i << " "; } cout << endl << endl; cout << " "; for (size_t i = 0; i < _vertexs.size(); ++i) { cout << i << " "; } cout << endl; // 打印矩阵 for (size_t i = 0; i < _vertexs.size(); ++i) { cout << "[" << i << "]" << "->" << _vertexs[i] << endl; } cout << endl; for (size_t i = 0; i < _tables.size(); ++i) { cout << _vertexs[i] << "[" << i << "]->"; Edge* cur = _tables[i]; while (cur) { cout << "[" << _vertexs[cur->_dstIndex] << ":" << cur->_dstIndex << ":" << cur->_w << "]->"; cur = cur->_next; } cout << "nullptr" << endl; } } private: vector<V> _vertexs; //顶点集合 map<V, int> _indexMap; //顶点和下标的映射关系 vector<Edge*> _tables; //邻接表 }; void TestGraph() { string a[] = { "张三","李四","王五","赵六","周七" }; Graph<string, int>g1(a, sizeof(a) / sizeof(a[0])); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 10); g1.AddEdge("李四", "周七", 50); g1.Print(); }
🏆邻接表化邻接矩阵
void table_convert_matrix(MGraph &G1, ALGraph G2) { // 邻接表转化为邻接矩阵
G1.arcnum = G2.arcnum;
G1.vexnum = G2.vexnum;
for(int i = 1; i <= G1.vexnum; i++) {
for(int j = 1; j <= G1.vexnum; j++) {
G1.Edge[i][j] = 0; // 初始化邻接矩阵
}
}
ArcNode *p;
for(int i = 1; i <= G2.vexnum; i++) { //依次遍历各顶点表结点为头的边链表
p = G2.vertices[i].first; // 取出顶点 i 的第一条出边
while(p) { //遍历边链表
G1.Edge[i][p->adjvex] = 1;
p = p -> next; // 取出下一条出边
}
}
}
🏆邻接矩阵化邻接表
void matrix_convert_table(ALGraph &G1, MGraph G2) { // 邻接矩阵转化为邻接表
G1.arcnum = G2.arcnum;
G1.vexnum = G2.vexnum;
for(int i = 1; i <= G1.vexnum; i++) {
G1.vertices[i].first = NULL; // 初始化指向第一条依附该顶点的弧的指针
}
ArcNode *p;
for(int i = 1; i <= G2.vexnum; i++) { // 依次遍历整个邻接矩阵
for(int j = 1; j <= G2.vexnum; j++) {
if(G2.Edge[i][j] == 1) {
p = (ArcNode *) malloc (sizeof(ArcNode));
p -> adjvex = j;
p -> next = G1.vertices[i].first;
G1.vertices[i].first = p;
}
}
}
}
🏆邻接表化逆邻接表
void adjacency_to_inverse_adjacency(ALGraph GOut, ALGraph &GIn) {
/*将图的邻接表转化为逆邻接表*/
GIn.arcnum = GOut.arcnum; //初始化逆邻接表的边数目
GIn.vexnum = GOut.vexnum; //初始化逆邻接表的顶点数目
for (int i = 1; i <= GIn.vexnum; i++) {
GIn.vertices[i].data = GOut.vertices[i].data; // 初始化逆邻接表的顶点信息
GIn.vertices[i].first = NULL; // 初始化指向第一条依附该顶点的弧的指针
}
for(int i = 1; i <= GOut.vexnum; i++) {
ArcNode *p = GOut.vertices[i].first; // 取得指向第一条依附该顶点的弧的指针
ArcNode *s;
while(p != NULL) { // 遍历邻接表中第i个顶点所有邻接边
s = (ArcNode *) malloc (sizeof(ArcNode)); // or s = new ArcNode;
int temp = p -> adjvex;
s -> adjvex = i;
s -> next = GIn.vertices[temp].first; //头插法将顶点i挂到GIn.vertices[temp]的边表中
GIn.vertices[temp].first = s;
p = p -> next; // 继续往后遍历i所指向的顶点
}
}
}
💎二、图遍历
🏆1.广度优先遍历-BFS
用邻接矩阵为例子,相当于二叉树中的层序遍历
//广度优先搜索,起点 void BFS(const V& src) { //找到其下标 size_t srcindex = GetVertexIndex(src); //visited表示没有被访问过的顶点,用于标记 vector<bool> visited(_vertexs.size(), false); //使用队列完成广度遍历 queue<int>q; q.push(srcindex); visited[srcindex] = true; size_t d = 1;//记录起点的度 //dSize保证队列中的数据走完 size_t leveSize = 1; while (!q.empty()) { printf("%s的%d度好友:", src.c_str(), d); while (leveSize--) { size_t front = q.front(); q.pop(); //把front顶点的邻接顶点入队列 for (size_t i = 0; i < _vertexs.size(); ++i) { if (visited[i] == false && _matrix[front][i] != INT_MAX) { printf("[%d:%s]", i, _vertexs[i].c_str()); q.push(i); visited[i] = true; } } } cout << endl; leveSize = q.size(); ++d; } } void TestGraph() { string a[] = { "张三","李四","王五","赵六","周七" }; Graph<string, int>g1(a, sizeof(a) / sizeof(a[0])); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 10); g1.AddEdge("李四", "周七", 50); g1.Print(); g1.BFS("张三"); g1.DFS("张三"); }
//辅助数组visited中存放所有节点
//初始化为0,访问过后设置为1
void BFS (Graph G, int v){ //按广度优先非递归遍历连通图G
cout<<v; visited[v] = 1; //访问第v个顶点
queue<int> q; //辅助队列Q初始化,置空
q.push(v); //v进队
while(!q.empty()){ //队列非空
q.pop(); //队头元素出队并置为u
for (int i = 0; i < G.vexnum; i++) {
int w = G.vex[i];
if (G.edge[k][i] && !visited[i]) {//v和i有边且没有被访问
cout << w;
q.push(w);
visited[i] = 1;
}
}
}
}
//调用
void BFStraverse(graph) {
for (int v = 0; v < G.vexnum; ++v)
visited[v] = 0;
for (int v = 0; v < G.vexnum; ++v)
if (!visited[v])
BFS(G, G.vex[v]);
}
🏆BFS算法求单元最短路径问题
每条边的权值都是1,参考点击链接
//求顶点u到其他顶点的最短路径
void BFS_Distance(Graph G, int v) {
for (i = 0; i < G.vexnum; ++i) {
dis[i] = INT_MAX;//初始化路径长度
path[i] = -1;//最短路径的前驱
}
dis[v] = 0;
visited[v] = 1;
q.push(v);
while (!q.empty()) {
int tmp = q.front();
q.pop();
for (int i = 0; i < G.vexnum; i++) {
int w = G.vex[i];
if (G.edge[tmp][i] && !visited[i]) {
dis[w] = dis[tmp] + 1;
path[w] = tmp;
visited[w] = 1;
q.push(w);
}
}
}
}
🏆2.深度优先遍历-DFS
用邻接矩阵为例子,相当于而擦函数中的前中后序遍历
//起点和标记数组 void _DFS(size_t srcIndex, vector<bool>& visited) { printf("[%d:%s]", srcIndex, _vertexs[srcIndex].c_str()); visited[srcIndex] = true;//表示该点被标记 for (size_t i = 0; i < _vertexs.size(); ++i) { if (visited[i] == false && _matrix[srcIndex][i] != INT_MAX) { _DFS(i, visited); } } } //深度优先搜索,起点 void DFS(const V& src) { //找到其下标 size_t srcindex = GetVertexIndex(src); //visited表示没有被访问过的顶点 vector<bool> visited(_vertexs.size(), false); _DFS(srcindex, visited); } void TestGraph() { string a[] = { "张三","李四","王五","赵六","周七" }; Graph<string, int>g1(a, sizeof(a) / sizeof(a[0])); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 10); g1.AddEdge("李四", "周七", 50); g1.Print(); g1.BFS("张三"); g1.DFS("张三"); }
//辅助数组visited中存放所有节点
//初始化为0,访问过后设置为1
void DFS(AMGraph G, int v){ //图G为邻接矩阵类型
cout<<v; visited[v] = 1; //访问第v个顶点
for(int w = 0; w<G.vexnum; w++)//依次检查邻接矩阵v所在的行
if((G.arcs[v][w]!=0)&&(!visited[w]))
DFS(G, w);
//w是v的邻接点,如果w未访问,则递归调用DFS
}
//调用
void BFStraverse(graph) {
for (int v = 0; v < G.vexnum; ++v)
visited[v] = 0;
for (int v = 0; v < G.vexnum; ++v)
if (!visited[v])
BFS(G, G.vex[v]);
}
💎二、生成最小树
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:
- 只能使用图中的权值最小的边来构造最小生成树,这几条边加起来的权值是最小的。
- 只能使用恰好n-1条边来连接图中的n个顶点。
- 选用的n-1条边不能构成回路。构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。
🏆1.Kruskal算法
任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树,直到选出N-1条边。
- 先判断加入边的两个顶点在不在一个集合,如果在一个集合,加入就会构成环。
- 加入的边,就把它的两个顶点合并。
- 并查集传送门
void _AddEdge(size_t srci, size_t dsti, const W& w) { _matrix[srci][dsti] = w; // 无向图 if (Direction == false) { _matrix[dsti][srci] = w; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); _AddEdge(srci, dsti, w); } struct Edge { size_t _srci;//原点 size_t _dsti;//目标点 W _w;//权值 Edge(size_t srci, size_t dsti, const W& w) :_srci(srci) ,_dsti(dsti) ,_w(w) {} //比较权值的大小,重载我们的>运算符 bool operator>(const Edge& e) const { return _w > e._w; } }; //最小生成树Kruskal W kruskalMST(Graph<V, W, MAX_W, Direction>& g) { size_t n = _vertexs.size(); //创建最小生成树 minTree._vertexs = _vertexs; minTree._indexMap = _indexMap; minTree._matrix.resize(n); for (size_t i = 0; i < n; ++i) { minTree._matrix[i].resize(n, MAX_W); } //升序的优先级队列,每次获取权值最小的边 priority_queue<Edge, vector<Edge>, greater<Edge>> minque; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { if (i < j && _matrix[i][j] != MAX_W) { minque.push(Edge(i, j, _matrix[i][j])); } } } //每次从优先级队列中获取权值最小的边,然后判断它们的起点和终点在不在一个集合 //若不在则选中这条边,并将起点和终点放入集合 //若在则说明如果选中则会形成环,此时忽略掉这条边 //重复上述操作,直到优先级队列为空 // 选出n-1条边 int size = 0; W totalW = W(); UnionFindSet ufs(n);//利用并查集 while (!minque.empty()) { Edge min = minque.top(); minque.pop(); if (!ufs.InSet(min._srci, min._dsti)) { cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; //往我们的最小生成树中添加这条边 minTree._AddEdge(min._srci, min._dsti, min._w); //将这条边添加到我们的并查集当中 ufs.Union(min._srci, min._dsti); ++size; totalW += min._w; } else { cout << "构成环:"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } } //找到了最小生成树 if (size == n - 1) { return totalW; } //如果没有找到就返回权值 else { return W(); } }
🏆2.Prim算法
Prim算法随便选一个点,然后选该点权值最小的那条边,再选这条边相连的节点的所有边中权值最小的边,不断重复。
这种方式不会构成环,因为每次选边,都是从两个集合里面分别选一个节点构成的边:已经相连顶点是一个集合,没有相连顶点是一个集合。因此只需要用set记录选过的节点即可。//最小生成树Prim W Prim(Graph<V, W, MAX_W, Direction>& minTree, const W& src) { //获取这个起点的下标 size_t srci = GetVertexIndex(src); size_t n = _vertexs.size(); minTree._vertexs = _vertexs; minTree._indexMap = _indexMap; minTree._matrix.resize(n); for (size_t i = 0; i < n; ++i) { minTree._matrix[i].resize(n, MAX_W); } //同两个数组,分别表示已经被最小生成树选中的结点和没有被最小生成树选中的结点 vector<bool> X(n, false); vector<bool> Y(n, true); X[srci] = true; Y[srci] = false; // 从X->Y集合中连接的边里面选出最小的边 priority_queue<Edge, vector<Edge>, greater<Edge>> minq; // 先把srci连接的边添加到队列中 for (size_t i = 0; i < n; ++i) { if (_matrix[srci][i] != MAX_W) { //分别将起始点,指向的最终点和权值构成的边放入队列中 minq.push(Edge(srci, i, _matrix[srci][i])); } } cout << "Prim开始选边" << endl; size_t size = 0; W totalW = MAX_W; while (!minq.empty()) { Edge min = minq.top(); minq.pop(); // 最小边的目标点也在X集合,则构成环 if (X[min._dsti]) { cout << "构成环:"; cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; } else { //将这条边添加到我们的最小生成树当中 minTree._AddEdge(min._srci, min._dsti, min._w); cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl; //X中对应的点将其标记成true代表已经加入了x集合 X[min._dsti] = true; //Y代表的是还没有被连接的点,所以我们将我们这个已经被连接的点的位置标记成false Y[min._dsti] = false; ++size; totalW += min._w; if (size == n - 1) break; for (size_t i = 0; i < n; ++i) { //将当前边的终点作为我们挑选下一条边的起点,并且这条起点的终点不能在我们的X集合中 //然后将这些点重新放入我们的队列中 if (_matrix[min._dsti][i] != MAX_W && Y[i]) { minq.push(Edge(min._dsti, i, _matrix[min._dsti][i])); } } } } //选到了n-1条边就返回总的权重 if (size == n - 1) { return totalW; } else { return MAX_W; } }
💎三、最短路径
🏆1.Dijkstra算法
最短路径问题:从在带权图的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。
单源最短路径问题:算法要求图中所有边的权重非负,一般在求解最短路径的时候都是已知一个起点 和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。运用贪心思想,每次从 「未求出最短路径的点」中取出距离距离起点最小路径的点,以这个点为跳转点刷新「未求出最短路径的点」的距离。然后锁定该跳转点,因为该跳转点到起始位置的距离已经是最小值了。
// 传入起点 // 传入存储起点到其他各个点的最短路径的权值和容器(例:点s到syzx的路径) // 传入每个节点的父路径,也就是从我们的源节点到我们每一个节点的最短路径的前一个节点的下标,存储路径前一个顶点下标 void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath) { //源点的下标 size_t srci = GetVertexIndex(src); //节点的数量 size_t n = _vertexs.size(); //将所有的路径初始化为无穷大 dist.resize(n, MAX_W); //将路径全部都初始化成-1,也就是没有前一个结点(我们结点的下标从0开始) pPath.resize(n, -1); //自己结点到自己的距离就是0 dist[srci] = 0; //自己到自己的最短路径的前一个节点就是自己,所以前一个节点的下标也是自己 pPath[srci] = srci; // 标记已经确定最短路径的顶点集合,初始全部都是false,也就是全部都没有被确定下来 vector<bool> S(n, false); for (size_t j = 0; j < n; ++j) { // 选最短路径顶点且不在S更新其他路径 //最小的点为u,初始化为0 int u = 0; //记录到最小的点的权值 W min = MAX_W; for (size_t i = 0; i < n; ++i) { //这个点没有被选过,并且到这个点的权值比我们当前的最小值更小,我们就进行更新 if (S[i] == false && dist[i] < min) { //用u记录这个最近的点的编号 u = i; min = dist[i]; } } //标记当前点已经被选中了 S[u] = true; // 松弛更新u连接顶点v srci->u + u->v < srci->v 则更新 //确定u链接出去的所有点 for (size_t v = 0; v < n; ++v) { //如果v这个点没有被标记过,也就是我们这个点还没有被确定最短距离,并且从我们当前点u走到v的路径是存在的 //并且从u走到v的总权值的和比之前源点到v的权值更小,我们就更新我们从源点到我们的v的最小权值 if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]) { //更新从源点到v的最小权值 dist[v] = dist[u] + _matrix[u][v]; //标记我们从源点到v的最小路径要走到v这一步的前一个节点需要走u pPath[v] = u; } } } }
测试
void TestGraphDijkstra() { const char* str = "syztx"; Graph<char, int, INT_MAX, true> g(str, strlen(str)); g.AddEdge('s', 't', 10); g.AddEdge('s', 'y', 5); g.AddEdge('y', 't', 3); g.AddEdge('y', 'x', 9); g.AddEdge('y', 'z', 2); g.AddEdge('z', 's', 7); g.AddEdge('z', 'x', 6); g.AddEdge('t', 'y', 2); g.AddEdge('t', 'x', 1); g.AddEdge('x', 'z', 4); vector<int> dist; vector<int> parentPath; g.Dijkstra('s', dist, parentPath); }
时间复杂度O(N^2),空间复杂度O(N)
下面是简单版的代码
int n, e, s;//n个点,e条边,s是原点
int dis[101], visited[101];//dis是记录原点刀i的距离
int graph[101][101]
int main() {
for (int i = 0; i <= 100; i++) {
dis[i] = INT_MAX;//最开始每条边都无限大
}
cin >> n >> e;
//输入边
for (int i = 0; i <= e; i++) {
int a, b, c;
cin >> a >> b >> c;
graph[a][b] = c;
}
cin >> s;//原点
dis[s] = 0;
for (int i = 0; i <= n; i++) {
int min = INT_MAX, minx;//min是记录当前最小点,minx是记录最小点的编号
for (int j = 1; j <= n; ++j) {
if (dis[j] < min && visited[j] == 0) {//最小点且没有被访问
min = dis[j], minx = j;
}
}
visited[minx] = 1;
for (int j = 1; j <= n; j++) {
if (graph[minx][j] > 0) {//找当前点的所有连接边
if (min + graph[minx][j] < dis[j]) {//绕路
dis[j] = min + graph[minx][j];
}
}
}
for (int i = 0; i <= n; i++) {
cout << dis[i] << " ";
}
}
}
🏆2.Bellman-Ford算法
bellman—ford算法可以解决负权图的单源最短路径问题,是一种暴力算法。时间复杂度 O(N*E) (N是点数,E是边数),使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),空间复杂度:O(N)。
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath) { //获取我们点的总和数 size_t n = _vertexs.size(); //获取我们源点的索引 size_t srci = GetVertexIndex(src); // vector<W> dist,记录srci-其他顶点最短路径权值数组 dist.resize(n, MAX_W); // vector<int> pPath 记录srci-其他顶点最短路径父顶点数组 pPath.resize(n, -1); // 先更新srci->srci为缺省值 dist[srci] = MAX_W; //cout << "更新边:i->j" << endl; // 总体最多更新n轮 //从s->t最多经过n条边,否则就会变成回路。 //每一条路径的更新都可能会影响别的路径 for (size_t k = 0; k < n; ++k) { // i->j 更新一次 bool update = false; cout << "更新第:" << k << "轮" << endl; for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { // srci -> i + i ->j //如果i->j边存在的话,并且srci -> i + i ->j比原来的距离更小,就更新该路径 if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { //这一轮有发生更新 update = true; cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl; dist[j] = dist[i] + _matrix[i][j]; //记录当前点的前一个节点 pPath[j] = i; } } } // 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了 if (update == false) { break; } } // 还能更新就是带负权回路,具体的例子在下面 for (size_t i = 0; i < n; ++i) { for (size_t j = 0; j < n; ++j) { // srci -> i + i ->j if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { return false; } } } return true; }