目录
2.聚类系数(Clustering coefficient)
零.图的存储结构
0.预备知识
Vertex 【顶点】 Vertice 【顶点】 Arc 【弧】 Edge 【边】
Matrix 【矩阵】 Adjacent 【邻接的】 Binary list 【二叉链表】
vex 【顶点的简写】 AMG(Adjacency Matrix)【邻接矩阵存储形式的图】
ALG(Adjacency List)【邻接表存储形式的图】
注意:在讨论的问题中,主要分为两大类:一类以边为核心要素,一类以顶点为核心要素
1.邻接矩阵
#define VertexType int //顶点的值的类型(可以认为是节点的属性)
#define EdgeType int //弧的值的类型
#define MAX_VEX_NUM 100 //最大顶点数
#define MAX_ARC_NUM 1000 //最大边数
#define MAX_LEN 1e6 //表示无限距离,即该边不存在
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
2.邻接表
#include <iostream>
#include <queue>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct EdgeNode //边表结点(也就是弧结点)
{
VertexType adj_vex; //邻接点域,存储该边另一个顶点对应的下标位置(有向边的终点)
EdgeType weight; //存储权值,对于非网图可以不使用
struct EdgeNode *next; //链域,用于存储该边表的顶点(有向边的起点)的下一条边的指针
char *info; //信息域,如果有其他信息的话
} EdgeNode;
typedef struct VexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点的权值
EdgeNode *first_edge; //链域,存储指向第一条边的指针,也就是边表头指针(指向首元节点捏)
} VexNode;
typedef struct ALG //邻接链表
{
VexNode adjList[MAX_VEX_NUM]; //顶点表(因为已知结点个数,所以可以用数组实现)
int VexNum; //顶点个数
int EdgeNum; //边的个数
int flag; //无向图与有向图的标志
} ALG;
3.十字链表
4.临界多重表
一.图的构造
1.邻接矩阵
//
0
10
10
1 2 3 4 5 6 7 8 9 10
1 2 1
1 3 1
1 4 1
5 3 1
6 7 1
8 9 1
4 6 1
3 2 1
4 8 1
10 1 1
邻接矩阵如下:
X 1 1 1 X X X X X 1
1 X 1 X X X X X X X
1 1 X X 1 X X X X X
1 X X X X 1 X 1 X X
X X 1 X X X X X X X
X X X 1 X X 1 X X X
X X X X X 1 X X X X
X X X 1 X X X X 1 X
X X X X X X X 1 X X
1 X X X X X X X X X
@ 1 1 1 @ @ @ @ @ 1
1 @ 1 @ @ @ @ @ @ @
1 1 @ @ 1 @ @ @ @ @
1 @ @ @ @ 1 @ 1 @ @
@ @ 1 @ @ @ @ @ @ @
@ @ @ 1 @ @ 1 @ @ @
@ @ @ @ @ 1 @ @ @ @
@ @ @ 1 @ @ @ @ 1 @
@ @ @ @ @ @ @ 1 @ @
1 @ @ @ @ @ @ @ @ @
无向图与有向图的统一化(使用flag作为区分)
图的属性——顶点个数——边的条数——顶点权值(分两类啦)——边的初始化(默认无限距离)——边的添加(包含两个点的值和边的值)——(根据图的属性再做操作:无向图多加一条边)
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++) //注意这里对顶点到自身的边更新为了长度为0的边
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
2.邻接表
无向图与有向图的统一化(使用flag作为区分)
当图中的边数相对于顶点较少时,邻接矩阵是对存储空间的极大浪费。
我们可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。
回忆树结构的孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。
应用这种思路,我们把这种数组与链表相结合的存储方法称为邻接表(Adjacency List)
(1) 图中顶点用一个一维数组存储,当然也可以用单链表来存储,不过用数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一条边的指针,以便于查找该顶点的边信息。(此处的第一个从图中点的序号来看,从小到大)
(2) 图中每个顶点vi的所有边构成一个线性表,由于边的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为以vi为弧尾(是Tail,也是起点)的出边表。
1.无向图的邻接表
顶点表的各个结点的类型为 VexNode 由 data 和 first_edge 两个域表示。
其中,data 是数据域,存储顶点的信息。
而 first_edge 是指针域,指向边表的第一个弧节点,即此顶点的第一条边。
边表结点的类型为 EdgeNode 由 adj_vex 和 next 两个域组成。
adj_vex 是邻接点的值域,存储顶点的邻接点在顶点表中的下标(不是输入时顶点的权值!!!)
next 则存储指向边表中下一个结点的指针
比如v1顶点与v0、v2互为邻接点,则在v1的边表中,adj_vex 分别为 v0的0 和 v2的2。
如果想知道某个顶点的度,就去查找这个顶点的边表中结点的个数。
若要判断顶点vi和vj是否存在边,只需要测试顶点vi的边表adj_vex中是否存在结点vj的下标。
若求顶点的所有邻接点,其实就是对此顶点的边表进行遍历,得到的adj_vex域对应的顶点就是邻接点。
如果要遍历图中的点,用DFS与BFS,然后查看有没有边(直接调用该顶点的边集adj_vex即可)
2.有向图的邻接表
顶点vi的边表是指以vi为弧尾(没有箭头的一端,也就是出发端)的弧来存储的,这样很容易就可以得到每个顶点的出度。
有时为了便于确定顶点的入度或以顶点为弧头(箭头指向处,也就是进入端)的弧,可以建立一个有向图的逆邻接表。
即对每个顶点vi都建立一个链接为vi为弧头的表。
此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息
如下图所示:
0
10
10
1 2 3 4 5 6 7 8 9 10
1 2 1
1 3 1
1 4 1
5 3 1
6 7 1
8 9 1
4 6 1
3 2 1
4 8 1
10 1 1
邻接表如下:
1: 1—10(weight: 1) 1—4(weight: 1) 1—3(weight: 1) 1—2(weight: 1)
2: 2—3(weight: 1) 2—1(weight: 1)
3: 3—2(weight: 1) 3—5(weight: 1) 3—1(weight: 1)
4: 4—8(weight: 1) 4—6(weight: 1) 4—1(weight: 1)
5: 5—3(weight: 1)
6: 6—4(weight: 1) 6—7(weight: 1)
7: 7—6(weight: 1)
8: 8—4(weight: 1) 8—9(weight: 1)
9: 9—8(weight: 1)
10: 10—1(weight: 1)
//——————————————————————————————————————————————————————————————————————————————————————
1
10
10
1 2 3 4 5 6 7 8 9 10
1 2 1
1 3 1
1 4 1
5 3 1
6 7 1
8 9 1
4 6 1
3 2 1
4 8 1
10 1 1
邻接表如下:
1: 1—4(weight: 1) 1—3(weight: 1) 1—2(weight: 1)
2:
3: 3—2(weight: 1)
4: 4—8(weight: 1) 4—6(weight: 1)
5: 5—3(weight: 1)
6: 6—7(weight: 1)
7:
8: 8—9(weight: 1)
9:
10: 10—1(weight: 1)
#include <iostream>
#include <queue>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct EdgeNode //边表结点(也就是弧结点)
{
VertexType adj_vex; //邻接点域,存储该边另一个顶点对应的下标位置(有向边的终点)
EdgeType weight; //存储权值,对于非网图可以不使用
struct EdgeNode *next; //链域,用于存储该边表的顶点(有向边的起点)的下一条边的指针
char *info; //信息域,如果有其他信息的话
} EdgeNode;
typedef struct VexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点的权值
EdgeNode *first_edge; //链域,存储指向第一条边的指针,也就是边表头指针(指向首元节点捏)
} VexNode;
typedef struct ALG //邻接链表
{
VexNode *adjList; //顶点表(因为已知结点个数,所以可以用数组实现)
int VexNum; //顶点个数
int EdgeNum; //边的个数
int flag; //无向图与有向图的标志
} ALG;
int Locate_Vex_ALG(ALG graph,VertexType v)
{
for(int i=0; i<graph.VexNum; i++)
if(graph.adjList[i].data==v)
return i;
return -1;
}
ALG Create_ALG(void)//邻接图
{
ALG graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>graph.flag;
while(graph.flag!=0 && graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>graph.VexNum;
graph.adjList = (VexNode*)malloc(sizeof(VexNode)*graph.VexNum);
cout<<"请输入边的条数"<<endl;
cin>>graph.EdgeNum;
cout<<"请输入顶点的序号"<<endl;
for(int i=0; i<graph.VexNum; i++)
{
cin>>graph.adjList[i].data; //输入顶点的信息(权值)
graph.adjList[i].first_edge = NULL; //将边表置为空表
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;
for(int k=0; k<graph.EdgeNum; k++)
{
VertexType v1,v2;
EdgeType w;
cin>>v1>>v2>>w;
int i = Locate_Vex_ALG(graph,v1); //通过权值找到结点在顶点表中的位置
int j = Locate_Vex_ALG(graph,v2);
EdgeNode *edge1;
edge1 = (EdgeNode*)malloc(sizeof(EdgeNode));//生成一个边表的结点(从i指向j)
edge1->adj_vex = j; //存入边的终点的序号(j)
edge1->weight = w;
edge1->next = graph.adjList[i].first_edge; //这里使用头插法插入新边
graph.adjList[i].first_edge = edge1; //这里是在结点表的顶点(i)后插入
//edge1(j)变成了新的首条边
if(graph.flag==0) //无向图的操作多一步
{
EdgeNode *edge2;
edge2 = (EdgeNode*)malloc(sizeof(EdgeNode));
edge2->adj_vex = i;
edge2->weight = w;
edge2->next = graph.adjList[j].first_edge;
graph.adjList[j].first_edge = edge2;
}
}
return graph;
}
void Print_ALG(ALG graph)
{
for(int i=0;i<graph.VexNum;i++)
{
EdgeNode *p = graph.adjList[i].first_edge;
cout<<graph.adjList[i].data<<": ";
while(p)
{
cout<<graph.adjList[i].data<<"—"<<graph.adjList[p->adj_vex].data<<"(weight: "<<p->weight<<") ";
p = p->next;
}
cout<<endl;
}
}
3.邻接矩阵 —> 邻接表
ALG AMG_To_ALG(AMG graph_AMG)
{
ALG graph;
graph.flag = graph_AMG.flag;
graph.VexNum = graph_AMG.VexNum;
graph.adjList = (VexNode*)malloc(sizeof(VexNode)*graph.VexNum);
graph.EdgeNum = graph_AMG.ArcNum;
for(int i=0; i<graph.VexNum; i++)
{
graph.adjList[i].data = graph_AMG.vexs[i];//输入顶点的信息(权值)
graph.adjList[i].first_edge = NULL; //将边表置为空表
}
for(int i=0; i<graph.VexNum; i++)
for(int j=i; j<graph.VexNum; j++)
{
if(i==j || graph_AMG.Edges[i][j]==MAX_LEN)
continue;
EdgeNode *edge1;
edge1 = (EdgeNode*)malloc(sizeof(EdgeNode));//生成一个边表的结点(从i指向j)
edge1->adj_vex = j; //存入边的终点的序号(j)
edge1->weight = graph_AMG.Edges[i][j];
edge1->next = graph.adjList[i].first_edge; //这里使用头插法插入新边
graph.adjList[i].first_edge = edge1; //这里是在结点表的顶点(i)后插入
//edge1(j)变成了新的首条边
if(graph.flag==0) //无向图的操作多一步
{
EdgeNode *edge2;
edge2 = (EdgeNode*)malloc(sizeof(EdgeNode));
edge2->adj_vex = i;
edge2->weight = graph_AMG.Edges[j][i];
edge2->next = graph.adjList[j].first_edge;
graph.adjList[j].first_edge = edge2;
}
}
return graph;
}
4.邻接表 —> 邻接矩阵
AMG ALG_To_AMG(ALG graph_ALG) //创造一个用邻接矩阵实现的图
{
AMG Graph;
Graph.flag = graph_ALG.flag;
Graph.VexNum = graph_ALG.VexNum;
Graph.ArcNum = graph_ALG.EdgeNum;
for(int i=0; i<Graph.VexNum; i++)
Graph.vexs[i] = graph_ALG.adjList[i].data;
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++) //注意这里对顶点到自身的边更新为了长度为0的边
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
for(int i=0; i<graph_ALG.VexNum; i++)
{
EdgeNode *p = graph_ALG.adjList[i].first_edge;
while(p)
{
VertexType v1 = graph_ALG.adjList[i].data;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = p->adj_vex;
Graph.Edges[i][j] = p->weight; //在邻接矩阵中存储一条从点v1指向v2的边w
p = p->next;
}
}
return Graph;
}
二.图的遍历
1.DFS(深度优先搜索)
a.邻接矩阵
//
DFS的遍历序列如下:
请输入起始点:1
1 2 3 5 4 6 7 8 9 10
请输入起始点:2
2 1 3 5 4 6 7 8 9 10
请输入起始点:3
3 1 2 4 6 7 8 9 10 5
请输入起始点:4
4 1 2 3 5 10 6 7 8 9
请输入起始点:5
5 3 1 2 4 6 7 8 9 10
请输入起始点:6
6 4 1 2 3 5 10 8 9 7
请输入起始点:7
7 6 4 1 2 3 5 10 8 9
请输入起始点:8
8 4 1 2 3 5 10 6 7 9
请输入起始点:9
9 8 4 1 2 3 5 10 6 7
请输入起始点:10
10 1 2 3 5 4 6 7 8 9
bool visited[MAX_VEX_NUM];
void DFS_Unit(AMG Graph,int v)
{
cout<<Graph.vexs[v]<<" ";
visited[v] = true;
for(int j=0; j<Graph.VexNum; j++)
if(!visited[j] && Graph.Edges[v][j]!=MAX_LEN)
DFS_Unit(Graph,j);
} //直到走到底为止
void DFS(AMG Graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool)); //对visited数组进行刷新
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_AMG(Graph,v);//找到起始点的位置或者序列
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_AMG(Graph,v);
}
DFS_Unit(Graph,temp); //从该顶点出发去遍历他所属的一个连通分支
for(int i=0; i<Graph.VexNum; i++)
if(!visited[i])
DFS_Unit(Graph,i); //遍历每一个连通分支
}
b.邻接表
//
DFS的遍历序列如下:
请输入起始点:1
1 2 3 5 4 6 7 8 9 10
请输入起始点:2
2 1 3 5 4 6 7 8 9 10
请输入起始点:3
3 1 2 4 6 7 8 9 10 5
请输入起始点:4
4 1 2 3 5 10 6 7 8 9
请输入起始点:5
5 3 1 2 4 6 7 8 9 10
请输入起始点:6
6 4 1 2 3 5 10 8 9 7
请输入起始点:7
7 6 4 1 2 3 5 10 8 9
请输入起始点:8
8 4 1 2 3 5 10 6 7 9
请输入起始点:9
9 8 4 1 2 3 5 10 6 7
请输入起始点:10
10 1 2 3 5 4 6 7 8 9
bool visited[MAX_VER_NUM] = {false};
bool Judge_Edge(ALG graph,int i,int j)
{
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
if(p->adj_vex==j)
return true;
p = p->next;
}
return false;
}
void DFS_Unit(ALG graph,int i)
{
cout<<graph.adjList[i].data<<" ";
visited[i] = true;
for(int j=0; j<graph.VexNum; j++)
if(!visited[j] && Judge_Edge(graph,i,j))
DFS_Unit(graph,j);
} //直到走到底为止
void DFS(ALG graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_ALG(graph,v);
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_ALG(graph,v);
}
DFS_Unit(graph,temp);
for(int i=0; i<graph.VexNum; i++)
if(!visited[i])
DFS_Unit(graph,i);
}
2.BFS(广度优先搜索)
a.邻接矩阵
//
BFS的遍历序列如下:
请输入起始点:1
1 2 3 4 10 5 6 8 7 9
请输入起始点:2
2 1 3 4 10 5 6 8 7 9
请输入起始点:3
3 1 2 5 4 10 6 8 7 9
请输入起始点:4
4 1 6 8 2 3 10 7 9 5
请输入起始点:5
5 3 1 2 4 10 6 8 7 9
请输入起始点:6
6 4 7 1 8 2 3 10 9 5
请输入起始点:7
7 6 4 1 8 2 3 10 9 5
请输入起始点:8
8 4 9 1 6 2 3 10 7 5
请输入起始点:9
9 8 4 1 6 2 3 10 7 5
请输入起始点:10
10 1 2 3 4 5 6 8 7 9
void BFS(AMG Graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_AMG(Graph,v);
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_AMG(Graph,v);
}
BFS_Unit(Graph,temp); //从该顶点出发去遍历他所属的一个连通分支
for(int i=0; i<Graph.VexNum; i++)
if(!visited[i])
BFS_Unit(Graph,i); //遍历每一个连通分支
}
void BFS_Unit(AMG Graph,int i)
{
queue<int> qNode;
qNode.push(i);
visited[i] = true;
while(!qNode.empty())
{
int index = qNode.front();
cout<<Graph.vexs[i]<<" ";
for(int j=0; i<Graph.VexNum; i++)
{
if(!visited[j] && Graph.Edges[i][j]!=MAX_LEN)
{
visited[j] = true;
qNode.push(j);
}
}
qNode.pop();
}
}
b.邻接表
//
0
10
10
1 2 3 4 5 6 7 8 9 10
1 2 1
1 3 1
1 4 1
5 3 1
6 7 1
8 9 1
4 6 1
3 2 1
4 8 1
10 1 1
邻接表如下:
1: 1—10(weight: 1) 1—4(weight: 1) 1—3(weight: 1) 1—2(weight: 1)
2: 2—3(weight: 1) 2—1(weight: 1)
3: 3—2(weight: 1) 3—5(weight: 1) 3—1(weight: 1)
4: 4—8(weight: 1) 4—6(weight: 1) 4—1(weight: 1)
5: 5—3(weight: 1)
6: 6—4(weight: 1) 6—7(weight: 1)
7: 7—6(weight: 1)
8: 8—4(weight: 1) 8—9(weight: 1)
9: 9—8(weight: 1)
10: 10—1(weight: 1)
BFS的遍历序列如下:
请输入起始点:1
1 2 3 4 10 5 6 8 7 9
请输入起始点:2
2 1 3 4 10 5 6 8 7 9
请输入起始点:3
3 1 2 5 4 10 6 8 7 9
请输入起始点:4
4 1 6 8 2 3 10 7 9 5
请输入起始点:5
5 3 1 2 4 10 6 8 7 9
请输入起始点:6
6 4 7 1 8 2 3 10 9 5
请输入起始点:7
7 6 4 1 8 2 3 10 9 5
请输入起始点:8
8 4 9 1 6 2 3 10 7 5
请输入起始点:9
9 8 4 1 6 2 3 10 7 5
请输入起始点:10
10 1 2 3 4 5 6 8 7 9
bool visited[MAX_VER_NUM] = {false};
bool Judge_Edge(ALG graph,int i,int j)
{
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
if(p->adj_vex==j)
return true;
p = p->next;
}
return false;
}
void BFS(ALG graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_ALG(graph,v);
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_ALG(graph,v);
}
BFS_Unit(graph,temp);
for(int i=0; i<graph.VexNum; i++)
if(!visited[i])
BFS_Unit(graph,i);
}
void BFS_Unit(ALG graph,int i)
{
queue<int> qNode;
qNode.push(i);
visited[i] = true;
while(!qNode.empty())
{
int i = qNode.front();
cout<<graph.adjList[i].data<<" ";
for(int j=0; j<graph.VexNum; j++)
{
if(!visited[j] && Judge_Edge(graph,i,j))
{
visited[j] = true;
qNode.push(j);
}
}
qNode.pop();
}
}
void BFS_Unit_Pro(ALG graph,int i)
{
queue<int> qNode;
qNode.push(i);
visited[i] = true;
while(!qNode.empty())
{
int i = qNode.front();
cout<<graph.adjList[i].data<<" ";
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
if(!visited[p->adj_vex])
{
visited[p->adj_vex] = true;
qNode.push(p->adj_vex);
}
p = p->next;
}
qNode.pop();
}
}
3.有向图的搜索
一些概念
可运行总代码
#include <iostream>
#include <queue>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
//测试用数据
/*
0
10
10
1 2 3 4 5 6 7 8 9 10
1 2 1
1 3 1
1 4 1
5 3 1
6 7 1
8 9 1
4 6 1
3 2 1
4 8 1
10 1 1
*/
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
typedef struct EdgeNode //边表结点(也就是弧结点)
{
VertexType adj_vex; //邻接点域,存储该边另一个顶点对应的下标位置(有向边的终点)
EdgeType weight; //存储权值,对于非网图可以不使用
struct EdgeNode *next; //链域,用于存储该边表的顶点(有向边的起点)的下一条边的指针
char *info; //信息域,如果有其他信息的话
} EdgeNode;
typedef struct VexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点的权值
EdgeNode *first_edge; //链域,存储指向第一条边的指针,也就是边表头指针(指向首元节点捏)
} VexNode;
typedef struct ALG //邻接链表
{
VexNode *adjList; //顶点表(因为已知结点个数,所以可以用数组实现)
int VexNum; //顶点个数
int EdgeNum; //边的个数
int flag; //无向图与有向图的标志
} ALG;
int Locate_Vex_AMG(AMG Graph,VertexType v);
int Locate_Vex_ALG(ALG graph,VertexType v);
AMG Create_AMG(void) ;
ALG Create_ALG(void);
void Print_AMG(AMG Graph);
void Print_ALG(ALG graph);
ALG AMG_To_ALG(AMG graph_AMG);
AMG ALG_To_AMG(ALG graph_ALG);
void DFS(ALG graph);
void DFS_Unit(ALG graph,int v);
void BFS(ALG graph);
void BFS_Unit(ALG graph,int i);
bool Judge_Edge(ALG graph,int i,int j);
bool visited[MAX_VEX_NUM] = {false};
ALG Create_ALG(void)//邻接图
{
ALG graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>graph.flag;
while(graph.flag!=0 && graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>graph.VexNum;
graph.adjList = (VexNode*)malloc(sizeof(VexNode)*graph.VexNum);
cout<<"请输入边的条数"<<endl;
cin>>graph.EdgeNum;
cout<<"请输入顶点的序号"<<endl;
for(int i=0; i<graph.VexNum; i++)
{
cin>>graph.adjList[i].data; //输入顶点的信息(权值)
graph.adjList[i].first_edge = NULL; //将边表置为空表
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;
for(int k=0; k<graph.EdgeNum; k++)
{
VertexType v1,v2;
EdgeType w;
cin>>v1>>v2>>w;
int i = Locate_Vex_ALG(graph,v1); //通过权值找到结点在顶点表中的位置
int j = Locate_Vex_ALG(graph,v2);
EdgeNode *edge1;
edge1 = (EdgeNode*)malloc(sizeof(EdgeNode));//生成一个边表的结点(从i指向j)
edge1->adj_vex = j; //存入边的终点的序号(j)
edge1->weight = w;
edge1->next = graph.adjList[i].first_edge; //这里使用头插法插入新边
graph.adjList[i].first_edge = edge1; //这里是在结点表的顶点(i)后插入
//edge1(j)变成了新的首条边
if(graph.flag==0) //无向图的操作多一步
{
EdgeNode *edge2;
edge2 = (EdgeNode*)malloc(sizeof(EdgeNode));
edge2->adj_vex = i;
edge2->weight = w;
edge2->next = graph.adjList[j].first_edge;
graph.adjList[j].first_edge = edge2;
}
}
return graph;
}
void Print_ALG(ALG graph)
{
for(int i=0; i<graph.VexNum; i++)
{
EdgeNode *p = graph.adjList[i].first_edge;
cout<<graph.adjList[i].data<<": ";
while(p)
{
cout<<graph.adjList[i].data<<"—"<<graph.adjList[p->adj_vex].data<<"(weight: "<<p->weight<<") ";
p = p->next;
}
cout<<endl;
}
}
int Locate_Vex_ALG(ALG graph,VertexType v)
{
for(int i=0; i<graph.VexNum; i++)
if(graph.adjList[i].data==v)
return i;
return -1;
}
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++) //注意这里对顶点到自身的边更新为了长度为0的边
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
ALG AMG_To_ALG(AMG graph_AMG)
{
ALG graph;
graph.flag = graph_AMG.flag;
graph.VexNum = graph_AMG.VexNum;
graph.adjList = (VexNode*)malloc(sizeof(VexNode)*graph.VexNum);
graph.EdgeNum = graph_AMG.ArcNum;
for(int i=0; i<graph.VexNum; i++)
{
graph.adjList[i].data = graph_AMG.vexs[i];//输入顶点的信息(权值)
graph.adjList[i].first_edge = NULL; //将边表置为空表
}
for(int i=0; i<graph.VexNum; i++)
for(int j=i; j<graph.VexNum; j++)
{
if(i==j || graph_AMG.Edges[i][j]==MAX_LEN)
continue;
EdgeNode *edge1;
edge1 = (EdgeNode*)malloc(sizeof(EdgeNode));//生成一个边表的结点(从i指向j)
edge1->adj_vex = j; //存入边的终点的序号(j)
edge1->weight = graph_AMG.Edges[i][j];
edge1->next = graph.adjList[i].first_edge; //这里使用头插法插入新边
graph.adjList[i].first_edge = edge1; //这里是在结点表的顶点(i)后插入
//edge1(j)变成了新的首条边
if(graph.flag==0) //无向图的操作多一步
{
EdgeNode *edge2;
edge2 = (EdgeNode*)malloc(sizeof(EdgeNode));
edge2->adj_vex = i;
edge2->weight = graph_AMG.Edges[j][i];
edge2->next = graph.adjList[j].first_edge;
graph.adjList[j].first_edge = edge2;
}
}
return graph;
}
AMG ALG_To_AMG(ALG graph_ALG) //创造一个用邻接矩阵实现的图
{
AMG Graph;
Graph.flag = graph_ALG.flag;
Graph.VexNum = graph_ALG.VexNum;
Graph.ArcNum = graph_ALG.EdgeNum;
for(int i=0; i<Graph.VexNum; i++)
Graph.vexs[i] = graph_ALG.adjList[i].data;
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++) //注意这里对顶点到自身的边更新为了长度为0的边
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
for(int i=0; i<graph_ALG.VexNum; i++)
{
EdgeNode *p = graph_ALG.adjList[i].first_edge;
while(p)
{
VertexType v1 = graph_ALG.adjList[i].data;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = p->adj_vex;
Graph.Edges[i][j] = p->weight; //在邻接矩阵中存储一条从点v1指向v2的边w
p = p->next;
}
}
return Graph;
}
bool Judge_Edge(ALG graph,int i,int j)
{
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
if(p->adj_vex==j)
return true;
p = p->next;
}
return false;
}
void DFS_Unit(ALG graph,int i)
{
cout<<graph.adjList[i].data<<" ";
visited[i] = true;
for(int j=0; j<graph.VexNum; j++)
if(!visited[j] && Judge_Edge(graph,i,j))
DFS_Unit(graph,j);
} //直到走到底为止
void DFS(ALG graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_ALG(graph,v);
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_ALG(graph,v);
}
DFS_Unit(graph,temp);
for(int i=0; i<graph.VexNum; i++)
if(!visited[i])
DFS_Unit(graph,i);
}
void BFS(ALG graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
cout<<"请输入起始点:";
VertexType v;
cin>>v;
int temp = Locate_Vex_ALG(graph,v);
while(temp==-1)
{
cout<<"请输入正确的起始点!"<<endl;
cin>>v;
temp = Locate_Vex_ALG(graph,v);
}
BFS_Unit(graph,temp);
for(int i=0; i<graph.VexNum; i++)
if(!visited[i])
BFS_Unit(graph,i);
}
void BFS_Unit(ALG graph,int i)
{
queue<int> qNode;
qNode.push(i);
visited[i] = true;
while(!qNode.empty())
{
int i = qNode.front();
cout<<graph.adjList[i].data<<" ";
for(int j=0; j<graph.VexNum; j++)
{
if(!visited[j] && Judge_Edge(graph,i,j))
{
visited[j] = true;
qNode.push(j);
}
}
qNode.pop();
}
}
int main()
{
ALG graph = Create_ALG();
Print_ALG(graph);
AMG graph_ = ALG_To_AMG(graph);
Print_AMG(graph_);
ALG graph__ = AMG_To_ALG(graph_);
Print_ALG(graph__);
return 0;
}
三.图的基本操作
1.计算各顶点的出度与入读
a.邻接矩阵
第一列对应的是起点,第一行对应的是终点。
Edges[i][j] 指从 i 点到 j 点的一条边(其中 i 为起点,j 为终点)。
入度的计算:
计数以该顶点为终点(进入该点)的边。
外层循环以点为单位,先遍历列(不同的终点)。
内层循环以边(顶点对)为单位,再遍历以该点为终点的所有边。
因此这样计算的是 j 点的入度( 从 1-n 到 j )。
出度的计算:
计数以该顶点为起点(离开该点)的边。
外层循环以点为单位,先遍历行(不同的起点)。
内层循环以边(顶点对)为单位,再遍历以该点为起点的所有边。
因此这样计算的是 i 点的出度( 从 i 到 1-n )。
其实只用看 Edges【i】【j】的含义就好了( i 为起点,j 为终点)。
然后注意一下内外循环和是 degree【i】还是 degree【j】就好了。
注意无向图:入度 = 出度 = 度 。
//无向图
0
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
0 2 3 4 @ @ @ @ @ 1
2 0 9 @ @ @ @ @ 5 @
3 9 0 @ 3 @ @ @ @ @
4 @ @ 0 @ 2 @ 3 @ @
@ @ 3 @ 0 @ 2 @ 4 @
@ @ @ 2 @ 0 8 1 @ @
@ @ @ @ 2 8 0 @ @ 7
@ @ @ 3 @ 1 @ 0 4 @
@ 5 @ @ 4 @ @ 4 0 @
1 @ @ @ @ @ 7 @ @ 0
顶点:1 2 3 4 5 6 7 8 9 10
入度:4 3 3 3 3 3 3 3 3 2
出度:4 3 3 3 3 3 3 3 3 2
*****************************************************************************************
//有向图
1
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
0 2 3 4 @ @ @ @ @ @
@ 0 @ @ @ @ @ @ @ @
@ 9 0 @ @ @ @ @ @ @
@ @ @ 0 @ 2 @ 3 @ @
@ @ 3 @ 0 @ @ @ 4 @
@ @ @ @ @ 0 8 1 @ @
@ @ @ @ 2 @ 0 @ @ 7
@ @ @ @ @ @ @ 0 4 @
@ 5 @ @ @ @ @ @ 0 @
1 @ @ @ @ @ @ @ @ 0
顶点:1 2 3 4 5 6 7 8 9 10
入度:1 3 2 1 1 1 1 2 2 1
出度:3 0 1 2 2 2 2 1 1 1
度数:4 3 3 3 3 3 3 3 3 2
int *indegree;
int *outdegree;
//第一列对应的是起点,第一行对应的是终点
//Edges[i][j]指从i点到j点的一条边(其中i为起点,j为终点)
int *indegree_AMG(AMG graph)//入度是计数以该顶点为终点(进入该点)的边
{
int *indegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(indegree,0,sizeof(int)*graph.VexNum);
for(int j=0; j<graph.VexNum; j++) //外层循环以点为单位,先遍历列(不同的终点)
for(int i=0; i<graph.VexNum; i++) //内层循环以边(顶点对)为单位,再遍历以该点为终点的所有边
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //因此这样计算的是j点的入度(从1-n到j)
indegree[j]++;
return indegree;
}
int *outdegree_AMG(AMG graph)//出度是计数以该顶点为起点(离开该点)的边
{
int *outdegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(outdegree,0,sizeof(int)*graph.VexNum);
for(int i=0; i<graph.VexNum; i++) //外层循环以点为单位,先遍历列(不同的起点)
for(int j=0; j<graph.VexNum; j++) //内层循环以边(顶点对)为单位,再遍历以该点为起点的所有边
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //因此这样计算的是出度(从i到1-n)
outdegree[i]++;
return outdegree;
}
b.邻接表
int *indegree_ALG(ALG graph)//入度是计数以该顶点为终点(进入该点)的边
{
int *indegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(indegree,0,sizeof(int)*graph.VexNum);
for(int i=0; i<graph.VexNum; i++)//这样计算的是入度
{
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
indegree[p->adj_vex]++;
p = p->next;
}
}
return indegree;
}
int *outdegree_ALG(ALG graph)//出度是计数以该顶点为起点(离开该点)的边
{
int *outdegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(outdegree,0,sizeof(int)*graph.VexNum);
for(int i=0; i<graph.VexNum; i++)//这样计算的是出度
{
EdgeNode *p = graph.adjList[i].first_edge;
while(p)
{
outdegree[i]++;
p = p->next;
}
}
return outdegree;
}
2.聚类系数(Clustering coefficient)
1.单个节点的聚类系数 (Local Cluster Coefficient) 是所有与它相连的节点之间所连的边数,除以这些节点之间可以连出的最大边数【也就是(n+1)*n / 2 】。
2.图的聚类系数是其所有节点聚类系数的均值。
3.对于度0或者1的节点,聚类系数为 0 。(这里要特别小心,直接返回了nan)
返回NaN的运算有如下三种:
4.注意要用双精度double类型!!!
//
0
7
8
0 1 2 3 4 5 6
0 6 1
1 6 2
1 2 3
2 3 4
3 4 5
4 6 6
4 5 7
1 3 8
0 @ @ @ @ @ 1
@ 0 3 8 @ @ 2
@ 3 0 4 @ @ @
@ 8 4 0 5 @ @
@ @ @ 5 0 7 6
@ @ @ @ 7 0 @
1 2 @ @ 6 @ 0
Degree: 1 3 2 3 3 1 3
LCC: 0 1 1 1 0 0 0
CC: 0.238095
部分核心代码
int *degree_AMG(AMG graph)
{
int *degree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(degree,0,sizeof(int)*graph.VexNum);
for(int j=0; j<graph.VexNum; j++)
for(int i=0; i<graph.VexNum; i++)
if(graph.Edges[i][j]!=MAX_LEN && i!=j)
degree[j]++;
return degree;
}
int *Adjacent_Vertex_Branch(AMG graph)
{
int *arr = (int*)malloc(sizeof(int)*graph.VexNum);
memset(arr,0,sizeof(int)*graph.VexNum);
for(int i=0; i<graph.VexNum; i++) //考察每个点周围的相邻点之间的关系
{
int num = 0; //记录相邻点的个数
int cnt = 0; //记录相邻点之间的边的条数
int *index = (int*)malloc(sizeof(int)*graph.VexNum); //记录相邻点的下标,以便查找
for(int j=0; j<graph.VexNum; j++) //先找一下有哪些相邻点
{
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //如果与当前考察点相邻,则将下标加入index
index[num++] = j;
}
for(int k=0; k<num; k++) //接着考察这些相邻点之间是否有边,用两层循环
for(int q=k+1; q<num; q++) //防止重复计数
if(graph.Edges[index[k]][index[q]]!=MAX_LEN)
cnt++;
arr[i] = cnt;
}
return arr;
}
double Clustering_coefficient(AMG graph,int *arr,int *degree)
{
double sum = 0.0f;
for(int i=0; i<graph.VexNum; i++)//计算当个顶点的聚类系数
{
int n = degree[i];
if(n==1 || n==0)
sum += 0.0f;
else
sum += ((double)(arr[i]))/(n*(n-1)/2);
}
return sum/graph.VexNum;
}
int main()
{
ALG graph_ = Create_ALG();
Print_ALG(graph_);
AMG graph = ALG_To_AMG(graph_);
Print_AMG(graph);
int *degree = degree_AMG(graph);
cout<<"Degree:";
for(int i=0; i<graph.VexNum; i++)
cout<<degree[i]<<" ";
cout<<endl;
int *arr = Adjacent_Vertex_Branch(graph);
cout<<"LCC: ";
for(int i=0; i<graph.VexNum; i++)
cout<<arr[i]<<" ";
cout<<endl;
printf("%.6f",Clustering_coefficient(graph,arr,degree));
return 0;
}
3.离心率+直径+半径
【首先得是个连通图!】
节点距离:两个节点间的最短路径长度。
节点离心率 (Eccentricity):该点到其他节点距离中的的最大值。
图直径 (Diameter):图中距离最远的两个节点间的距离。
图半径 (Radius):图中所有节点的最小离心率。
提示:先计算每个节点的离心率,再计算图的半径。
首先:Floyd 算所有点之间的距离
—>找出最大距离,求出图的直径
—>算每个点的离心率(一行一行取距离的最大值)
—>找出最小离心率,求出图的半径
//
0
4
5
0 1 2 3
0 1 3
0 2 8
0 3 2
1 2 4
2 3 5
0: 0—3(weight: 2) 0—2(weight: 8) 0—1(weight: 3)
1: 1—2(weight: 4) 1—0(weight: 3)
2: 2—3(weight: 5) 2—1(weight: 4) 2—0(weight: 8)
3: 3—2(weight: 5) 3—0(weight: 2)
0 3 8 2
3 0 4 @
8 4 0 5
2 @ 5 0
dp[0][0]:0 dp[0][1]:3 dp[0][2]:7 dp[0][3]:2
dp[1][0]:3 dp[1][1]:0 dp[1][2]:4 dp[1][3]:5
dp[2][0]:7 dp[2][1]:4 dp[2][2]:0 dp[2][3]:5
dp[3][0]:2 dp[3][1]:5 dp[3][2]:5 dp[3][3]:0
Eccentricity: 7 5 7 5
Diameter: 7
Radius: 5
部分核心代码
bool Judge_Connectivity(AMG graph)
{
memset(visited,false,MAX_VEX_NUM*sizeof(bool));
DFS_Unit(graph,0); //判断连通性,就是看能否从一个顶点出发,就可以遍历完整个图
bool flag = true; //初始化为true,代表图是连通的
for(int i=0; i<graph.VexNum; i++) //看看是否还有没有遍历过的顶点
if(!visited[i])
{
flag = false;
break;
}
return flag;
}
int dp[MAX_VEX_NUM][MAX_VEX_NUM];
void Floyd(AMG graph)
{
for(int i=0; i<graph.VexNum; i++)
for(int j=0; j<graph.VexNum; j++)
dp[i][j] = graph.Edges[i][j];
for(int i=0; i<graph.VexNum; i++)
for(int j=0; j<graph.VexNum; j++)
for(int k=0; k<graph.VexNum; k++)
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]);
}
int eccentricity[MAX_VEX_NUM];
void Eccentricity(AMG graph)
{
for(int i=0; i<graph.VexNum; i++)
{
int max_dist = 0;
for(int j=0; j<graph.VexNum; j++)
if(dp[i][j]!=MAX_LEN)
max_dist = max(max_dist,dp[i][j]);
eccentricity[i] = max_dist;
}
}
int Diameter(AMG graph)
{
int max_dist = 0;
for(int i=0; i<graph.VexNum; i++)
for(int j=0; j<graph.VexNum; j++)
if(dp[i][j]!=MAX_LEN)
max_dist = max(max_dist,dp[i][j]);
return max_dist;
}
int Radius(AMG graph)
{
int min_eccentricity = MAX_LEN;
for(int i=0; i<graph.VexNum; i++)
min_eccentricity = min(min_eccentricity,eccentricity[i]);
return min_eccentricity;
}
int main()
{
ALG graph_ = Create_ALG();
Print_ALG(graph_);
AMG graph = ALG_To_AMG(graph_);
Print_AMG(graph);
Floyd(graph);
if(Judge_Connectivity(graph))
cout<<1<<endl;
else
cout<<0<<endl;
for(int i=0; i<graph.VexNum; i++)
{
for(int j=0; j<graph.VexNum; j++)
cout<<"dp["<<i<<"]["<<j<<"]:"<<dp[i][j]<<" ";
cout<<endl;
}
Eccentricity(graph);
cout<<"Eccentricity:";
for(int i=0; i<graph.VexNum; i++)
cout<<eccentricity[i]<<" ";
cout<<endl;
cout<<"Diameter:"<<Diameter(graph)<<endl;
cout<<"Radius: "<<Radius(graph)<<endl;
return 0;
}
4.图的路径问题
(1)无向图 / 有向图两点之间是否有通路存在?
(2)两点之间是否有路径存在?
(3)如果有路径,路径经过哪些顶点?
通路【Access】:是图的一个点、边的交错序列 (v0e1v1e2…envn) ,v0 和 vn 分别称为通路的起点和终点, n 为通路的长度。【(n+1)个点+n-条边,长度为n】
简单通路(路径)【Path】:边各不相同的路径。
【因此需要用 visited 或者 set 来避免重复的边,注意是边!!!】
初级通路【Beginner Path】:结点、边都各不相同的路径,又称基本路径。
【其实只要满足不走相同的点就满足不走相同的边了】
PS:单独一个结点 v 也是路径,它是长度为 0 的基本路径。
(1)路径存在
1.邻接矩阵
bool flag = false;
bool Judge_Path_U_To_V_Unit(AMG graph,int u_index,int v_index)
{
visited[u_index] = true;
if(!flag)
for(int index=0; index<graph.VexNum; index++)
{
if(!visited[index])
{
if(graph.Edges[index][v_index]!=MAX_LEN)//说明有边可到,或者已经是v点了
return flag = true;
visited[index] = true;
Judge_Path_U_To_V_Unit(graph,index,v_index);//DFS算法来找路径
visited[index] = false;//回溯
}
}
return flag;
}
bool Judge_Path_U_To_V(AMG graph)
{
cout<<"请输入起点与终点的值(非下标):"<<endl;
VertexType u,v;
cin>>u>>v;
if(u==v)
return true;
int u_index = Locate_Vex_AMG(graph,u);
int v_index = Locate_Vex_AMG(graph,v);
memset(visited,false,sizeof(bool)*graph.VexNum);
flag = false;
Judge_Path_U_To_V_Unit(graph,u_index,v_index);
if(flag)
cout<<"从"<<u<<"到"<<v<<"存在路径"<<endl;
else
cout<<"从"<<u<<"到"<<v<<"不存在路径"<<endl;
return flag;
}
2.邻接表
bool flag = false;
bool Judge_Path_U_To_V_Unit(ALG graph,int u_index,int v_index)
{
visited[u_index] = true;
EdgeNode *p = graph.adjList[u_index].first_edge;
while(!flag && p)
{
int index = p->adj_vex;
if(index==v_index)
return flag = true;
if(!visited[index])
{
visited[index] = true;
Judge_Path_U_To_V_Unit(graph,index,v_index);//DFS算法来找路径
visited[index] = false;//回溯
}
p = p->next;
}
return flag;
}
bool Judge_Path_U_To_V(ALG graph)
{
cout<<"请输入起点与终点的值(非下标):"<<endl;
VertexType u,v;
cin>>u>>v;
if(u==v)
return true;
int u_index = Locate_Vex_ALG(graph,u);
int v_index = Locate_Vex_ALG(graph,v);
memset(visited,false,sizeof(bool)*graph.VexNum);
flag = false;
Judge_Path_U_To_V_Unit(graph,u_index,v_index);
if(flag)
cout<<"从"<<u<<"到"<<v<<"存在路径"<<endl;
else
cout<<"从"<<u<<"到"<<v<<"不存在路径"<<endl;
return flag;
}
(2)通路存在
参照路径即可,因为如果存在通路,那么就一定存在路径,所以可以直接按路径来判断
(3)所有路径
回溯即可,这里运用到了二维数组中的拷贝,注意 memset 和 memcpy 的区别。
然后要注意 memcpy 在二维数组中的细节(比如这里要用 path【i】而不是 path)
/*
0
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
*/
int All_Path[100][100] = {0};//用于存储路径上各个顶点的信息
int cnt = 0;//记录当前已经找到的路径数目
int num = 0;//记录当前路径上已有的点的数目
void Find_All_Path_U_To_V_Unit(ALG graph,int u_index,int v_index)
{
visited[u_index] = true;
All_Path[cnt][num++] = u_index;
if(u_index==v_index)
{
cnt++;
memcpy(All_Path[cnt],All_Path[cnt-1],sizeof(int)*num);//将上一行的复制到下一行中
return;
}
EdgeNode *p = graph.adjList[u_index].first_edge;
while(p)
{
int index = p->adj_vex;
if(!visited[index])
{
Find_All_Path_U_To_V_Unit(graph,index,v_index);//DFS算法来找路径
visited[index] = false;//回溯
num--; //直接覆盖当前元素即可
}
p = p->next;
}
}
bool Find_All_Path_U_To_V(ALG graph)
{
cout<<"请输入起点与终点的值(非下标):"<<endl;
VertexType u,v;
cin>>u>>v;
if(u==v)
return true;
int u_index = Locate_Vex_ALG(graph,u);
int v_index = Locate_Vex_ALG(graph,v);
memset(visited,false,sizeof(bool)*graph.VexNum);
memset(All_Path,-1,sizeof(int)*graph.VexNum);
Find_All_Path_U_To_V_Unit(graph,u_index,v_index);
if(cnt)
{
cout<<"从"<<u<<"到"<<v<<"存在"<<cnt<<"条路径"<<endl;
for(int i=0; i<cnt; i++)
{
cout<<"第"<<i+1<<"条路径: ";
for(int j=0; All_Path[i][j]!=v_index; j++)
cout<<graph.adjList[All_Path[i][j]].data<<"->";
cout<<v<<"\n******************************"<<endl;
}
}
else
cout<<"从"<<u<<"到"<<v<<"不存在路径"<<endl;
return cnt;
}
5.图的环路问题
(1)无向图 / 有向图是否存在环路?
(2)是否存在包含u点的环路?
(3)有几条环路?
(4)环路经过哪些点,环路轨迹是什么?
环路 / 回路:起点和终点重合的路径,又分简单回路、初级回路(圈)
可以通过找路径的方法,用DFS,如果遇到了之前走过的点,就说明有环路。
(1)图中是否有回路
void IsCycle(ALGraph G)
{
int visited[MAX_VERTEX_NUM],u, CycleFlag;
for(u=1; u<=G.vexnum; u++)
visited[u]=0;
for(u=1; u<=G.vexnum; u++)
if(!visited[u])
{
CycleFlag=Cycle(G,visited,u);
if(CycleFlag) break;
}
if(CycleFlag)
printf("图中存在回路!\n");
else
printf("图中不存在回路!\n");
}
(2)图中是否有包含顶点u的回路
直接从u点开始搜索路径即可。
int Cycle(ALGraph G, int *visited,int u)
{
ArcNode *p;
int w;
int flag =0;
visited[u]=1;
p=G.vertices[u].firstarc;
while(p&&!flag)
{
w=p->adjvex;
if(visited[w]!=1) //w未访问过(0)或访问且其邻接点已访问完(2)
{
if(!visited[w]) //w未访问过,从w开始继续dfs
flag=Cycle(G,visited,w);
}
else //顶点w已经访问过,并且其邻接点已经访问完
flag=1;
p=p->nextarc;
}
visited[u]=2;
return(flag);
}
6.图的双连通性
7.有向图的强连通性
8.有向图的中心点
9.总结与对比
四.图的应用
1.最小生成树
a.Prim 算法
Prim算法的精髓是根据点的不同,最小生成树也可能不同。
根据起始点,找到与该点相关联的其他顶点,选择其中边权值最小的纳入集合。
以此类推,直到将所有顶点都纳入集合中为止,这个就是Prim算法的最小生成树。
核心思想:每次都把最小的边挑选出来,然后固定该边以及它的起点,再将终点的所有边加入
(随机构建一个无向图)
现在我们构建两个集合S(红色的点),V(蓝色的点)。
S集合中存放的是已加入最小生成树的点,V集合中存放的是还没有加入最小生成树的点。
显然刚开始时所有的点都在V集合中。(这里通过判断Cloeset_Edge_Of_Node 中,每个元素的LowestCost是不是-1,来判断是否已经加入最小生成树)
然后们先将任意一个点加入集合S中(默认为点1),并且初始化所有点(除了点1)到集合S的距离是无穷大。
用一个变量 wpl 存放最小生成树所有边权值的和。
我们每次都选择离S集合最近的点加入S集合中,并且用新加入的点去更新dist数组,此时需要更新距离(也就是数组中的最小边)(贪心:每次都选最小的)。
更新就是观察能否通过新加入的点使到达集合的距离变小,也就是有没有更小的边存在。
(看下面dist数组的变化)。
我们开始在加入点1后开始第一次更新。
现在集合S={1},集合V={2,3,4,5,6,7},根据贪心策略,我们选择离集合S最近的点加入 ,即点2,并把这一条边的权值加到 wpl 中。
集合更新为S={1,2},V={3,4,5,6,7},并用点2去更新dist数组,我们发现点3和点7都可以都可以通过边2-3,2-7缩短到集合S得距离。
重复上面的步骤,直到将全部的点加入到最小生成树中。
这样一颗最小生成树就构建完成了,权值和是57。
0
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4 //记得再做一次!!!!
7 5 2
7 10 7
6 8 1
//图示为从顶点1出发
从顶点【1】出发:
10—1: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
从顶点【2】出发:
1—2: 2
10—1: 1
3—1: 3
5—3: 3
7—5: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
从顶点【3】出发:
1—3: 3
10—1: 1
2—1: 2
5—3: 3
7—5: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
从顶点【4】出发:
6—4: 2
8—6: 1
1—4: 4
10—1: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
9—8: 4
最小生成树的权值为:22
从顶点【5】出发:
7—5: 2
3—5: 3
1—3: 3
10—1: 1
2—1: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
从顶点【6】出发:
8—6: 1
4—6: 2
1—4: 4
10—1: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
9—8: 4
最小生成树的权值为:22
从顶点【7】出发:
5—7: 2
3—5: 3
1—3: 3
10—1: 1
2—1: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
从顶点【8】出发:
6—8: 1
4—6: 2
1—4: 4
10—1: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
9—8: 4
最小生成树的权值为:22
从顶点【9】出发:
8—9: 4
6—8: 1
4—6: 2
1—4: 4
10—1: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
最小生成树的权值为:22
从顶点【10】出发:
1—10: 1
2—1: 2
3—1: 3
5—3: 3
7—5: 2
4—1: 4
6—4: 2
8—6: 1
9—8: 4
最小生成树的权值为:22
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
typedef struct Closest_Edge_Of_Node //当前已搜索范围内,每个点(由下标确定)的最小边
{
VertexType adj_vex; //该最小边的另一个点(边的终点)的权值
EdgeType LowestCost; //当前的该点最小边的权值(用-1来表示已经确定了加入该点和所对应的边)
} Close_Edge; //下标代表顶点的序号
Closest_Edge_Of_Node arr[MAX_VEX_NUM];
void Mini_Span_Tree_Prim(AMG graph,VertexType u) //u为起始点
{
int k = Locate_Vex_AMG(graph,u); //通过权值找到u点的下标
arr[k].LowestCost = -1; //将u点的LowestCost记为为-1,标志着已经加入该点和所对应的边
for(int j=0; j<graph.VexNum; j++) //遍历除了u点之外的所有顶点
if(j!=k)
{
arr[j].adj_vex = u; //让每个顶点的当前最小边的终点都是u
arr[j].LowestCost = graph.Edges[k][j]; //先只搜索与u点相邻的边,将其权值当做当前每个节点的最小边(没有的就是无限长)
} //此时没有边的用MAX_LEN的作用就体现出来了(∞)
int wpl = 0; //wpl指最小生成树的权值之和
int Index_of_min = k; //Index_of_min指的是当前最小边的起点的下标
for(int i=1; i<graph.VexNum; i++) //这里的i都是次数!!!
{
//每次的i都是新加入搜索的顶点(x),这是错误的理解!!!
// if(i==k) //就不用再搜索起始出发点了
// continue;
int MinSpan = MAX_LEN; //MinSpan指的是当前已经搜索过的点,它们所有相邻边的集合中的最小值(除开已经添加的点)
for(int j=0; j<graph.VexNum; j++)
{
if(arr[j].LowestCost!=-1 && arr[j].LowestCost<MinSpan) //如果该点(以及对应的边)未曾添加过,且权值小于当前找到的最小值
{
MinSpan = arr[j].LowestCost; //更新最小边的权值
Index_of_min = j; //更新最小边的起点的下标
}
}
VertexType u_0,v_0;
u_0 = arr[Index_of_min].adj_vex; //u_0表示当前最小边的终点的权值
v_0 = graph.vexs[Index_of_min]; //v_0表示当前最小边的起点的权值
cout<<v_0<<"—"<<u_0<<": "<<arr[Index_of_min].LowestCost<<endl;
wpl += arr[Index_of_min].LowestCost; //将该边添加,并在wpl中加上它的权值
arr[Index_of_min].LowestCost = -1; //设置为-1,表示已经讲该点添加过了,就不需要重复考虑了
for(int j=0; j<graph.VexNum; j++) //此时再对所有的顶点的最小边进行更新,加入搜索当前新添加的顶点的所有边
{
if(graph.Edges[Index_of_min][j]<arr[j].LowestCost) //如果比该节点当前的最小边还小
{
arr[j].adj_vex = graph.vexs[Index_of_min]; //更新该节点当前的最小边的起点
arr[j].LowestCost = graph.Edges[Index_of_min][j]; //更新该节点当前的最小边的权值
}
}
}
cout<<"最小生成树的权值为:"<<wpl<<endl;
}
可运行总代码
#include <iostream>
#include <queue>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
typedef struct Closest_Edge_Of_Node //当前已搜索范围内,每个点(由下标确定)的最小边
{
VertexType adj_vex; //该最小边的另一个点(边的终点)的权值
EdgeType LowestCost; //当前的该点最小边的权值(用-1来表示已经确定了加入该点和所对应的边)
} Close_Edge; //下标代表顶点的序号
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++)
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
Closest_Edge_Of_Node arr[MAX_VEX_NUM];
void Mini_Span_Tree_Prim(AMG graph,VertexType u) //u为起始点
{
int k = Locate_Vex_AMG(graph,u); //通过权值找到u点的下标
arr[k].LowestCost = -1; //将u点的LowestCost记为为-1,标志着已经加入该点和所对应的边
for(int j=0; j<graph.VexNum; j++) //遍历除了u点之外的所有顶点
if(j!=k)
{
arr[j].adj_vex = u; //让每个顶点的当前最小边的终点都是u
arr[j].LowestCost = graph.Edges[k][j]; //先只搜索与u点相邻的边,将其权值当做当前每个节点的最小边(没有的就是无限长)
} //此时没有边的用MAX_LEN的作用就体现出来了(∞)
int wpl = 0; //wpl指最小生成树的权值之和
int Index_of_min = k; //Index_of_min指的是当前最小边的起点的下标
for(int i=1; i<graph.VexNum; i++) //这里的i都是次数!!!(就是添加了【VexNum-1】次顶点)
{
//每次的i都是新加入搜索的顶点(x),这是错误的理解!!!
// if(i==k) //就不用再搜索起始出发点了
// continue;
int MinSpan = MAX_LEN; //MinSpan指的是当前已经搜索过的点,它们所有相邻边的集合中的最小值(除开已经添加的点)
for(int j=0; j<graph.VexNum; j++)
{
if(arr[j].LowestCost!=-1 && arr[j].LowestCost<MinSpan) //如果该点(以及对应的边)未曾添加过,且权值小于当前找到的最小值
{
MinSpan = arr[j].LowestCost; //更新最小边的权值
Index_of_min = j; //更新最小边的起点的下标
}
}
VertexType u_0,v_0;
u_0 = arr[Index_of_min].adj_vex; //u_0表示当前最小边的终点的权值
v_0 = graph.vexs[Index_of_min]; //v_0表示当前最小边的起点的权值
cout<<v_0<<"—"<<u_0<<": "<<arr[Index_of_min].LowestCost<<endl;
wpl += arr[Index_of_min].LowestCost; //将该边添加,并在wpl中加上它的权值
arr[Index_of_min].LowestCost = -1; //设置为-1,表示已经讲该点添加过了,就不需要重复考虑了
for(int j=0; j<graph.VexNum; j++) //此时再对所有的顶点的最小边进行更新,加入搜索当前新添加的顶点的所有边
{
if(graph.Edges[Index_of_min][j]<arr[j].LowestCost) //如果比该节点当前的最小边还小
{
arr[j].adj_vex = graph.vexs[Index_of_min]; //更新该节点当前的最小边的起点
arr[j].LowestCost = graph.Edges[Index_of_min][j]; //更新该节点当前的最小边的权值
}
}
}
cout<<"最小生成树的权值为:"<<wpl<<endl;
}
int main()
{
AMG graph = Create_AMG();
Print_AMG(graph);
int cnt = 1;
while(cnt!=11)
{
cout<<"从顶点【"<<graph.vexs[cnt-1]<<"】出发:"<<endl;
Mini_Span_Tree_Prim(graph,cnt++);
cout<<endl;
}
return 0;
}
b.Kruskal 算法
Kruskal算法和Prim算法的不同之处在于:
这个算法以边集为单位,记录各个点是否在同一连通分量上,如果不在则纳入,实现的时候常用并查集(算法)进行实现,但在这里以书上的为标准,采用书上的算法进行实现。
用边集 Edges 数组用于存储所有的边『存完后要排序喔』
Edges 数组中的元素类型为 EdgeNode ,包含了该边的起点、终点和权值等信息。
然后 Connected_Component 数组用于存储每个结点当前所属的连通分量的序号。
『初始化为每个节点的下标捏』
设置一个循环用于遍历每一条边,(从前往后遍历,从小到大遍历)
然后检查当前最小边的起点和终点是否在一个连通分量中。
如果不在一个连通分量中,那就加入该边。
同时将终点的连通分量序号赋值为起点的,即将终点并入一个连通分支中。
值得注意的是:一般来说该算法只会出现一种最小生成树
//邻接矩阵实现
@ 2 3 4 @ @ @ @ @ 1
2 @ 9 @ @ @ @ @ 5 @
3 9 @ @ 3 @ @ @ @ @
4 @ @ @ @ 2 @ 3 @ @
@ @ 3 @ @ @ 2 4 @ @
@ @ @ 2 @ @ 8 1 @ @
@ @ @ @ 2 8 @ @ @ 7
@ @ @ 3 4 1 @ @ 4 @
@ 5 @ @ @ @ @ 4 @ @
1 @ @ @ @ @ 7 @ @ @
1 1 2 2 2 3 3 3 4 4 4 5 7 8 9 1 10 1
最小生成树的边:
6 8 1
1 2 2
4 6 2
5 7 2
1 3 3
3 5 3
1 4 4
8 9 4
最小生成树的总权值为:22
typedef struct EdgeNode //这里是运用边集,此时边的数目是已知的
{
VertexType Head; //存储该边的起点
VertexType Tail; //存储该边的终点
EdgeType weight; //存储该边的权值
} EdgeNode;
EdgeNode edge[MAX_Edge_NUM]; //边集
VertexType VexSet[MAX_VEX_NUM]; //顶点集
bool Compare(EdgeNode a,EdgeNode b) //比较的函数用于辅助sort运作
{
return a.weight<b.weight; //比较的是边的权值,如果a小于b则返回true,不需要换序
}
void Mini_Span_Tree_Kruskal(AMG graph)
{
int k = 0; //变量k用于辅助边的存储,即代表当前已经存入的边数
//这里值得注意的是有向图与无向图的区别
if(graph.flag==0) //该图为无向图
{
for(int i=0; i<graph.VexNum; i++)
for(int j=i; j<graph.VexNum; j++) //只需要遍历完右半部分
if(graph.Edges[i][j]!=MAX_LEN) //距离不为无穷大说明有边
{
edge[k].Head = graph.vexs[i]; //存入边的起点
edge[k].Tail = graph.vexs[j]; //存入边的终点
edge[k].weight = graph.Edges[i][j]; //存入边的权值
k++; //边的数目加一
}
}
else //该图为有向图
{
for(int i=0; i<graph.VexNum; i++)
for(int j=0; j<graph.VexNum; j++) //有向图必须全部遍历完
if(graph.Edges[i][j]!=MAX_LEN) //距离不为无穷大说明有边
{
edge[k].Head = graph.vexs[i]; //存入边的起点
edge[k].Tail = graph.vexs[j]; //存入边的终点
edge[k].weight = graph.Edges[i][j]; //存入边的权值
k++; //边的数目加一
}
}
sort(edge,edge+k,Compare); //对所有的边从小到大排序
for(int i=0; i<k; i++)
cout<<edge[i].weight<<" ";
for(int i=0; i<k; i++) //初始化每个顶点的连通分量,起始状况下每个点自成一个连通分支
VexSet[i] = i;
int wpl = 0; //初始化wpl
for(int i=0; i<k; i++) //依照权值,从小到大遍历边集
{
int v1 = Locate_Vex_AMG(graph,edge[i].Head); //当前最小边的起点
int v2 = Locate_Vex_AMG(graph,edge[i].Tail); //当前最小边的终点
int vs1 = VexSet[v1]; //起点的连通分支序号
int vs2 = VexSet[v2]; //终点的连通分支序号
if(vs1 != vs2) //如果两者不一样,则说明加入该边后一定不会产生环,因此添加该边
{
cout<<edge[i].Head<<" "<<edge[i].Tail<<" "<<edge[i].weight<<endl;
wpl += edge[i].weight;
for(int j=0; j<graph.VexNum; j++)//将终点原本所在的连通分支中的其他点全部纳入起点的连通分支
if(VexSet[j]==vs2)
VexSet[j] = vs1;
}
}
cout<<"最小生成树的总权值为:"<<wpl<<endl;
}
可运行总代码
#include <iostream>
#include <queue>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++)
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
typedef struct EdgeNode //这里是运用边集,此时边的数目是已知的
{
VertexType Head; //存储该边的起点
VertexType Tail; //存储该边的终点
EdgeType weight; //存储该边的权值
} EdgeNode;
EdgeNode edge[MAX_Edge_NUM]; //边集
VertexType VexSet[MAX_VEX_NUM]; //顶点集
bool Compare(EdgeNode a,EdgeNode b) //比较的函数用于辅助sort运作
{
return a.weight<b.weight; //比较的是边的权值,如果a小于b则返回true,不需要换序
}
void Mini_Span_Tree_Kruskal(AMG graph)
{
int k = 0; //变量k用于辅助边的存储,即代表当前已经存入的边数
//这里值得注意的是有向图与无向图的区别
if(graph.flag==0) //该图为无向图
{
for(int i=0; i<graph.VexNum; i++)
for(int j=i; j<graph.VexNum; j++) //只需要遍历完右半部分
if(graph.Edges[i][j]!=MAX_LEN) //距离不为无穷大说明有边
{
edge[k].Head = graph.vexs[i]; //存入边的起点
edge[k].Tail = graph.vexs[j]; //存入边的终点
edge[k].weight = graph.Edges[i][j]; //存入边的权值
k++; //边的数目加一
}
}
else //该图为有向图
{
for(int i=0; i<graph.VexNum; i++)
for(int j=0; j<graph.VexNum; j++) //有向图必须全部遍历完
if(graph.Edges[i][j]!=MAX_LEN) //距离不为无穷大说明有边
{
edge[k].Head = graph.vexs[i]; //存入边的起点
edge[k].Tail = graph.vexs[j]; //存入边的终点
edge[k].weight = graph.Edges[i][j]; //存入边的权值
k++; //边的数目加一
}
}
sort(edge,edge+k,Compare); //对所有的边从小到大排序
for(int i=0; i<k; i++)
cout<<edge[i].weight<<" ";
for(int i=0; i<k; i++) //初始化每个顶点的连通分量,起始状况下每个点自成一个连通分支
VexSet[i] = i;
int wpl = 0; //初始化wpl
for(int i=0; i<k; i++) //依照权值,从小到大遍历边集
{
int v1 = Locate_Vex_AMG(graph,edge[i].Head); //当前最小边的起点
int v2 = Locate_Vex_AMG(graph,edge[i].Tail); //当前最小边的终点
int vs1 = VexSet[v1]; //起点的连通分支序号
int vs2 = VexSet[v2]; //终点的连通分支序号
if(vs1 != vs2) //如果两者不一样,则说明加入该边后一定不会产生环,因此添加该边
{
cout<<edge[i].Head<<" "<<edge[i].Tail<<" "<<edge[i].weight<<endl;
wpl += edge[i].weight;
for(int j=0; j<graph.VexNum; j++)//将终点原本所在的连通分支中的其他点全部纳入起点的连通分支
if(VexSet[j]==vs2)
VexSet[j] = vs1;
}
}
cout<<"最小生成树的总权值为:"<<wpl<<endl;
}
int main()
{
AMG graph = Create_AMG();
Print_AMG(graph);
Mini_Span_Tree_Kruskal(graph);
cout<<endl;
return 0;
}
并查集(Disjoint_Sets)的实现
1.应用方向:
判断图的连通性; 求连通分支数; 求最大连通分量
判断两个点是否在同一连通块内; 判断增加一条边后是否会产生环
2.基本概念:
(1)
代表元『作为该集合的标志』
find 函数,用于寻找 x 所在的集合的代表元(代表元的下标就是连通分支序号!!!)
pre 数组用于存储下标为 i 的元素的上级『 pre[i] = j 』
此时如何标志出代表元?只需要让 代表元的上级为自身 即可『 pre[i] = i 』
find 此时就很好实现了,只要找到一个 pre是自身的返回 即可。
而两个集合的合并不用担心有交集,因为一个元素只可能有一个上级,注定只会属于一个集合。
join 函数的功能是把 x 所在集合与 y 所在集合合并为一个集合,这里默认将后者并入前者。
这里要和 merge 函数区分开
merge 函数的功能是把y并入x所在集合中,此时y的所有下属都会被一同移动到x所在的集合中。
但是y的所有上级都不会受影响。
直接 pre[y] = find(x) 即可。
(2)
其实并查集中的集合就是一个多叉树🌲,所有集合合起来就是一个森林:
集合的代表元就是树的根节点;
元素的上级 pre[i] 就是结点的双亲结点『这里由于不是完全二叉树,所以不能用堆的性质喔』;find 函数就是找x所在树的根节点;
join 函数就是把y所在的整颗树都并入x的树中
『而且是把y所在树的根节点直接接在x所在树的根节点下,也就是变为了它的孩子结点』
merge 就是把以y为根结点的子树并入x的树中『并且是变成了x所在树的根结点的孩子结点』
(3)find 的 pro 版本!!!(路径压缩算法)
这里并没有明确谁是谁的上级的规则,而是直接指定后面的数据作为前面数据的上级。
那么这样就导致了最终的树状结构无法预计:
即有可能是良好的 n 叉树,也有可能是单支树结构(一字长蛇形)。
试想,如果最后真的形成单支树结构,那么它的效率就会及其低下。
(树的深度过深,那么查询过程就必然耗时)
而我们最理想的情况就是所有人的直接上级都是代表元,这样一来整个树的结构就只有两级,此时查询代表元只需要查找一次。
因此,这就产生了路径压缩算法。
当从某个节点出发去寻找它的根节点时,我们会途径一系列的节点。
在这些节点中,除了根节点【代表元】外,其余所有节点都需要更改 pre[x](上级)为根节点。
可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即 pre[x] 的值)。
简单说来就是将x到根节点路径上的所有点的 pre[x](上级)都设为根节点。
该算法存在一个缺陷:
只有当查找了某个节点的代表元后,才能对该查找路径上的各节点进行路径压缩。
换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。
(4)find 的 ProX 版本!!!(加权标记法)
需要将树中所有节点都增设一个权值,用以表示该节点所在树中的高度.
(比如用rank[x]=3表示 x 节点所在树的高度为3)。
这样一来,在合并操作的时候就能通过这个权值的大小来决定谁当谁的上级。
为了使合并后的树不产生退化(即:使树中左右子树的深度差尽可能小),对每一个元素 x ,增设一个rank[x]数组,用以表达子树 x 的高度。
在合并时,如果rank[x] > rank[y],则令pre[x] = y;否则令pre[y] = x。
//并查集的实现
15
4
4
114 514 666 233
3
520 1314 999
3
51 71 101
5
123 234 345 456 567
请输入全体集合的元素个数:
15
请输入集合的个数:
4
请输入第1个集合中的元素个数:
4
请依次输入该集合中的元素
114 514 666 233
请输入第2个集合中的元素个数:
3
请依次输入该集合中的元素
520 1314 999
请输入第3个集合中的元素个数:
3
请依次输入该集合中的元素
51 71 101
请输入第4个集合中的元素个数:
5
请依次输入该集合中的元素
123 234 345 456 567
第0个元素114的连通分支序号为:0
第1个元素514的连通分支序号为:0
第2个元素666的连通分支序号为:0
第3个元素233的连通分支序号为:0
第4个元素520的连通分支序号为:4
第5个元素1314的连通分支序号为:4
第6个元素999的连通分支序号为:4
第7个元素51的连通分支序号为:7
第8个元素71的连通分支序号为:7
第9个元素101的连通分支序号为:7
第10个元素123的连通分支序号为:10
第11个元素234的连通分支序号为:10
第12个元素345的连通分支序号为:10
第13个元素456的连通分支序号为:10
第14个元素567的连通分支序号为:10
#include <iostream>
#define MAX_VEX_NUM 100
using namespace std;
int pre[MAX_VEX_NUM]; //用于存储第i个元素归属的集合的属性(不是存储数值的)
int arr[MAX_VEX_NUM]; //用于存储第i个元素的数值
void Init_Disjoint_Sets(int n) //初始化:让每一个点都独立成为一个集合
{
for(int i=0; i<n; i++)
pre[i] = i;
}
int find(int x) //查找代表元(非递归写法)
{
while(pre[x]!=x)
x = pre[x];
return x;
}
int find_(int x) //查找代表元(递归写法)
{
if(pre[x]==x)
return x;
else
return find(pre[x]);
}
void join(int x,int y) //合并x和y所属的集合
{
int rootx = find(x);
int rooty = find(y);
if(rootx != rooty)
pre[rooty] = pre[rootx];
}
void merge(int x,int y) //将y并入x的集合中
{
pre[y] = find(x);
}
int find_Pro(int x)
{
if(pre[x]==x)
return x;
else
return pre[x] = find(pre[x]);//后序遍历时找到了代表元,依次弹出递归栈并赋值结果
}
void Print(int n)
{
for(int i=0; i<n; i++)
cout<<"第"<<i<<"个元素"<<arr[i]<<"的连通分支序号为:"<<find_Pro(i)<<endl;
}
int main()
{
cout<<"请输入全体集合的元素个数:"<<endl;
int n;
cin>>n;
Init_Disjoint_Sets(n);
cout<<"请输入集合的个数:"<<endl;
int SetNum;
cin>>SetNum;
int k = 0; //用于辅助pre数据的存入,表示当前存入的数据个数
for(int i=0; i<SetNum; i++)
{
cout<<"请输入第"<<i+1<<"个集合中的元素个数:"<<endl;
int NodeNum;
cin>>NodeNum;
cout<<"请依次输入该集合中的元素"<<endl;
for(int j=0; j<NodeNum; j++)
{
int temp;
cin>>temp;
arr[k] = temp;
merge(k-j,k);
k++;
}
}
Print(n);
join(10,0);
Print(n);
return 0;
}
另一种实现方式:
//
15
114 514 666 233 520 1314 999 51 71 101 123 234 345 456 567
4
4
114 514 666 233
3
520 1314 999
3
51 71 101
5
123 234 345 456 567
第1个元素(114)的连通分支序号为:0
第2个元素(514)的连通分支序号为:0
第3个元素(666)的连通分支序号为:0
第4个元素(233)的连通分支序号为:0
第5个元素(520)的连通分支序号为:4
第6个元素(1314)的连通分支序号为:4
第7个元素(999)的连通分支序号为:4
第8个元素(51)的连通分支序号为:7
第9个元素(71)的连通分支序号为:7
第10个元素(101)的连通分支序号为:7
第11个元素(123)的连通分支序号为:10
第12个元素(234)的连通分支序号为:10
第13个元素(345)的连通分支序号为:10
第14个元素(456)的连通分支序号为:10
第15个元素(567)的连通分支序号为:10
#include <iostream>
#define MAX_VEX_NUM 100
using namespace std;
int pre[MAX_VEX_NUM]; //用于存储第i个元素归属的集合的属性(不是存储数值的)
int arr[MAX_VEX_NUM]; //用于存储第i个元素的数值
int n;
void Init_Disjoint_Sets(int n) //初始化:让每一个点都独立成为一个集合
{
for(int i=0; i<n; i++)
pre[i] = i;
}
int locate(int x)
{
for(int i=0; i<n; i++)
if(arr[i]==x)
return i;
return -1;
}
int find_by_data(int x_data) //通过数据查找代表元(非递归写法)
{
int x = locate(x_data);
while(pre[x]!=x)
x = pre[x];
return x;
}
int find(int x) //通过下标查找代表元(非递归写法)
{
while(pre[x]!=x)
x = pre[x];
return x;
}
int find_(int x_data) //查找代表元(递归写法)
{
int x = locate(x_data);
if(pre[x]==x)
return x;
else
return find(pre[x]);
}
void join(int x_data,int y_data) //合并x和y所属的集合
{
int x = locate(x_data);
int y = locate(y_data);
int rootx = find(x);
int rooty = find(y);
if(rootx != rooty)
pre[rooty] = pre[rootx];
}
void merge(int x_data,int y_data) //将y并入x的集合中
{
int x = locate(x_data);
int y = locate(y_data);
pre[y] = find(x);
}
int find_Pro(int x_data)
{
int x = locate(x_data);
if(pre[x]==x)
return x;
else
return pre[x] = find(pre[x]);//后序遍历时找到了代表元,依次弹出递归栈并赋值结果
}
void Print(int n)
{
for(int i=0; i<n; i++)
cout<<"第"<<i+1<<"个元素("<<arr[i]<<")的连通分支序号为:"<<find(i)<<endl;
}
int main()
{
cout<<"请输入全体集合的元素个数:"<<endl;
cin>>n;
Init_Disjoint_Sets(n);
cout<<"请依次输入所有元素的值"<<endl;
for(int i=0; i<n; i++)
cin>>arr[i];
cout<<"请输入集合的个数:"<<endl;
int SetNum;
cin>>SetNum;
int k = 0; //用于辅助pre数据的存入,表示当前存入pre的数据个数
for(int i=0; i<SetNum; i++)
{
cout<<"请输入第"<<i+1<<"个集合中的元素个数:"<<endl;
int NodeNum;
cin>>NodeNum;
cout<<"请依次输入集合【"<<i+1<<"】中的元素"<<endl;
for(int j=0; j<NodeNum; j++)
{
int temp;
cin>>temp;
pre[k] = k-j;//以第一个元素的下标为连通分支序号
k++;
}
}
Print(n);
join(123,51);
cout<<endl;
Print(n);
return 0;
}
2.最短路径
Floyd和Dijkstra是经典的最短路径算法,两者有相似也有不同:
复杂度上,Dijkstra算法时间复杂度是O(n2),Floyd算法时间复杂度是O(n3)。
功能上,Dijkstra是求单源最短路径,并且权值不能为负,Floyd是求多源最短路径,可有负权值。
算法实现上,Dijkstra 是一种贪心算法实现起来较复杂,Floyd基于动态规划实现简单。
a.Dijkstra 算法
【单源最短路径】
给定一个带权有向图 graph 与起始点 u ,求从 u 到 graph 中其他顶点的最短路径。
设置一个集合 visited 用于标志该点是否已经固定,或者是否已经找到了到该点的最短路径。
设置一个辅助数组 dist 用于存储u到各个顶点当前的最短路径。
从 visited 中的顶点中选择一个尚未被固定的顶点 v ,选择 dist[v] 中最小的边添加进去。
并将 v 对应的 visited 赋值为 true。
此时调整其他结点的当前的最短路径:dist[k] = min(dist[k],dist[v]+graph.Edges[v][k]);
//
0
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
Vexs: 1 2 3 4 5 6 7 8 9 10
dist: 0 2 3 4 6 6 8 7 7 1
path: -1 0 0 0 2 3 9 3 1 0
(1) 0
(1)--[2]-->(2) 2
(1)--[3]-->(3) 3
(1)--[4]-->(4) 4
(1)--[3]-->(3)--[3]-->(5) 6
(1)--[4]-->(4)--[2]-->(6) 6
(1)--[1]-->(10)--[7]-->(7) 8
(1)--[4]-->(4)--[3]-->(8) 7
(1)--[2]-->(2)--[5]-->(9) 7
(1)--[1]-->(10) 1
bool visited[MAX_VEX_NUM] = {false}; //存储结点是否被访问:visited[v]=ture(表示v结点已被访问)
int dist[MAX_VEX_NUM]; //起点到其余各顶点的最短路径值,相当于 dist【0】【i】
int path[MAX_VEX_NUM]; //最短路径的路径信息(路径上前驱结点的下标)
int sum[MAX_VEX_NUM] = {0}; //起始点到各点的最短路径的长度,初始化为0
void Dijkstra(AMG graph,int u)
{
int i = Locate_Vex_AMG(graph,u); //i表示顶点u的下标(通过传入的u的权值)
for (int j=0; j<graph.VexNum; j++)
{
dist[j] = graph.Edges[i][j]; //初始化最短距离
if (graph.Edges[i][j]!=MAX_LEN)
path[j] = i; //如果该点与起始点之间存在边,则初始化该点的前驱结点为起始点u
else
path[j] = -1; //如果该点与起始点之间不存在边,则暂且将该点的前驱结点的下标记为-1(即不存在路径)
}
dist[i] = 0; //起始点到自身的距离为0
visited[i] = true; //起始点已经走过啦
path[i] = -1; //path【i】的值是,路径上该下标表示的点的前驱结点的下标
int v; //表示当前已搜索范围内的最短边的对应顶点
VertexType Min_Len; //表示当前最短边的长度
for (int i=0; i<graph.VexNum-1; i++) //依次固定一条最短边以及对应的点
{
Min_Len = MAX_LEN;
for (int j=0; j<graph.VexNum; j++)//遍历所有的顶点
{
if (!visited[j] && dist[j]<Min_Len)//未被固定且边长更小
{
v = j; //更新顶点
Min_Len = dist[j]; //更新权值
}
}
visited[v] = true; //添加最短边的对应顶点
for (int k=0; k<graph.VexNum; ++k)//在添加完顶点后,更新最短距离(因为出现了新的路径)
{
if (!visited[k] && (dist[v]+graph.Edges[v][k])<dist[k])//只对未被固定的顶点进行更新,相当于插入一个新顶点
{
dist[k] = dist[v] + graph.Edges[v][k];//更新最短距离
path[k] = v; //更新前驱顶点
}
}
}
}
void Print_trace(AMG graph,int i,int i_) //打印u到下标为i的点的最短路径
{
int temp = path[i]; //记录起始点的前驱节点的下标
if (temp!=-1)
{
sum[i_] += graph.Edges[temp][i];
Print_trace(graph,temp,i_);//这里是采用的后序位置遍历路径上的点并打印
}//因为path中存储的是前驱结点的下标,是从i逆向到u的
if (i==0)
cout<<"("<<graph.vexs[i]<<")";//i等于0,说明为起始点
else
cout<<"--["<<graph.Edges[temp][i]<<"]-->("<<graph.vexs[i]<<")";
}
void Print_trace_Pro(AMG graph,int i) //u到下标为i的点的最短路径
{
int temp = path[i];
sum[i] += graph.Edges[i][temp];
stack<int> s;
s.push(graph.Edges[i][temp]);
while(temp!=-1)
{
sum[i] += graph.Edges[temp][path[temp]];
s.push(graph.Edges[temp][path[temp]]);
temp = path[temp];
}
cout<<"("<<graph.vexs[i]<<")";
while(!s.empty())
{
cout<<"->"<<"["<<s.top()<<"]";
s.pop();
}
}
简化版本【不要求打印路径】
int *Dijkstra(AMG graph,int i)
{
memset(visited,false,sizeof(bool)*graph.VexNum);
int *dist = (int*)malloc(sizeof(int)*graph.VexNum);
for(int j=0; j<graph.VexNum; j++)
dist[j] = graph.Edges[i][j];
visited[i] = true;
int v; //当前需要固定的点
VertexType Min_Len; //当前需要固定的边
for(int i=0; i<graph.VexNum-1; i++) //添加顶点需要n-1次
{
Min_Len = MAX_LEN;
for(int j=0; j<graph.VexNum; j++) //遍历所有的顶点
if(!visited[j] && dist[j]<Min_Len)
{
v = j;
Min_Len = dist[j];
}
visited[v] = true;
for(int k=0; k<graph.VexNum; k++) //添加顶点后要更新最短路径
if(!visited[k] && (dist[v]+graph.Edges[v][k]<dist[k]))
dist[k] = dist[v]+graph.Edges[v][k];
}
return dist;
}
另外一种打印路径的方法
/**
* 根据距离d和路径数组path输出路径,这样就不需要路径的节点数也能正确输出路径
* @param d 路径长度
* @param diameter_path 储存路径的数组
* @param g 图
*/
void printPath(int d, int *diameter_path, Graph g)
{
int k = 0;
int path_length = 0;
printf("Path: ");
do
{
printf("%s->", g.vertex[diameter_path[k]]);
path_length += g.matrix[diameter_path[k]][diameter_path[k + 1]];
k++;
}
while (path_length < d);
printf("%s\n", g.vertex[diameter_path[k]]);
}
int dijkstra(Graph g, int start, int end, int *path)
{
//TODO
int *visited = (int*)malloc(sizeof(int)*g.N);
memset(visited,0,sizeof(int)*g.N);
int sum = 0;
int *dist = (int*)malloc(sizeof(int)*g.N);
for(int i=0; i<g.N; i++)
{
dist[i] = g.matrix[start][i];
}
int k=0;
path[k++] = start; //起始点是start
visited[start] = 1;
for(int i=0; i<g.N-1; i++) //用于添加新的结点,共添加n-1次
{
int Min_len = max_dis; //当前的最短边
int v; //当前要固定的顶点
for(int q=0; q<g.N; q++) //找最短边
{
if(!visited[q] && Min_len > dist[q])
{
Min_len = dist[q];
v = q;
}
}
visited[v] = 1;
for(int j=0; j<g.N; j++) //加入v后更新最短路径
{
if(!visited[j] && dist[j] > dist[v]+g.matrix[v][j])
{
dist[j] = dist[v]+g.matrix[v][j];
if(j==end)
{
path[k++] = v;
}
}
}
}
path[k++] = end; //终止点是end
for(int i=0; i<k-1; i++)
sum += g.matrix[path[i]][path[i+1]];
return sum;
}
可运行总代码
#include <iostream>
#include <queue>
#include <stack>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++)
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
bool visited[MAX_VEX_NUM] = {false}; //存储结点是否被访问:visited[v]=ture(表示v结点已被访问)
int dist[MAX_VEX_NUM]; //起点到其余各顶点的最短路径值,相当于 dist【0】【i】
int path[MAX_VEX_NUM]; //最短路径的路径信息(路径上前驱结点的下标)
int sum[MAX_VEX_NUM] = {0}; //起始点到各点的最短路径的长度,初始化为0
void Dijkstra(AMG graph,int u)
{
int i = Locate_Vex_AMG(graph,u); //i表示顶点u的下标(通过传入的u的权值)
for (int j=0; j<graph.VexNum; j++)
{
dist[j] = graph.Edges[i][j]; //初始化最短距离
if (graph.Edges[i][j]!=MAX_LEN)
path[j] = i; //如果该点与起始点之间存在边,则初始化该点的前驱结点为起始点u
else
path[j] = -1; //如果该点与起始点之间不存在边,则暂且将该点的前驱结点的下标记为-1(即不存在路径)
}
dist[i] = 0; //起始点到自身的距离为0
visited[i] = true; //起始点已经走过啦
path[i] = -1; //path【i】的值是,路径上该下标表示的点的前驱结点的下标
int v; //表示当前已搜索范围内的最短边的对应顶点
VertexType Min_Len; //表示当前最短边的长度
for (int i=0; i<graph.VexNum-1; i++) //依次固定一条最短边以及对应的点
{
Min_Len = MAX_LEN;
for (int j=0; j<graph.VexNum; j++)//遍历所有的顶点
{
if (!visited[j] && dist[j]<Min_Len)//未被固定且边长更小
{
v = j; //更新顶点
Min_Len = dist[j]; //更新权值
}
}
visited[v] = true; //添加最短边的对应顶点
for (int k=0; k<graph.VexNum; ++k)//在添加完顶点后,更新最短距离(因为出现了新的路径)
{
if (!visited[k] && (dist[v]+graph.Edges[v][k])<dist[k])//只对未被固定的顶点进行更新,相当于插入一个新顶点
{
dist[k] = dist[v] + graph.Edges[v][k];//更新最短距离
path[k] = v; //更新前驱顶点
}
}
}
}
void Print_trace(AMG graph,int i,int i_) //打印u到下标为i的点的最短路径
{
int temp = path[i]; //记录起始点的前驱节点的下标
if (temp!=-1)
{
sum[i_] += graph.Edges[temp][i];
Print_trace(graph,temp,i_);//这里是采用的后序位置遍历路径上的点并打印
}//因为path中存储的是前驱结点的下标,是从i逆向到u的
if (i==0)
cout<<"("<<graph.vexs[i]<<")";//i等于0,说明为起始点
else
cout<<"--["<<graph.Edges[temp][i]<<"]-->("<<graph.vexs[i]<<")";
}
void Print_trace_Pro(AMG graph,int i) //u到下标为i的点的最短路径
{
int temp = path[i];
sum[i] += graph.Edges[i][temp];
stack<int> s;
s.push(graph.Edges[i][temp]);
while(temp!=-1)
{
sum[i] += graph.Edges[temp][path[temp]];
s.push(graph.Edges[temp][path[temp]]);
temp = path[temp];
}
cout<<"("<<graph.vexs[i]<<")";
while(!s.empty())
{
cout<<"->"<<"["<<s.top()<<"]";
s.pop();
}
}
int main()
{
AMG graph = Create_AMG();
Print_AMG(graph);
int cnt = 1;
Dijkstra(graph,cnt);
cout << "Vexs: ";
for (int i=0; i<graph.VexNum; i++)
cout << graph.vexs[i] << " ";
cout << endl;
cout<<"dist: ";
for (int i=0; i<graph.VexNum; i++)
cout << dist[i] << " ";
cout << endl;
cout << "path: ";
for (int i=0; i<graph.VexNum; i++)
cout << path[i] << " ";
cout << endl;
for (int i=0; i<graph.VexNum; i++)
{
Print_trace(graph,i,i);
cout<<" "<<sum[i]<<endl;
}
return 0;
}
b.Floyd 算法
又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法
具体思想为:
1 .邻接矩阵(二维数组) dp 储存路径:
数组中的值最初表示点与点之间初
始直接路径,最终是点与点之间的最短路径。
有两点需要注意的:
(1)如果没有边直接相连的两点,那么设置为一个很大的值(不要因为计算溢出成负数)。
(2)顶点到自身的距离要为0。
2 .将 n 个点依次加入松弛计算:
每个点加入后进行枚举,与当前的最短路径长度比较大小,看是否有路径长度被更改(自己能否更新路径)。
顺序加入 (k循环) 松弛的点时候,需要遍历图中每一个顶点对 ( i,j 双重循环)。
然后判断每一个点对的距离是否因为加入了新的顶点,而使得最短距离变得更小了。
如果变小,那么两点 ( i ,j ) 距离就需要更新。
3 .重复上述直到最后插点试探完成。
其中第2步的状态转移方程为: dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]);
其中 dp[i][j] 的意思可以理解为下标为 i 的顶点到下标为 j 的顶点的最短路径。
所以 dp[i][k] 为 i 到 k 的最短路径,而同理:dp[k][j] 为 k 到 j 的最短路径。
注意:最外层循环是 k 哦!!!【每次都要更新整个dp数组】
//
0
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
0 2 3 4 @ @ @ @ @ 1
2 0 9 @ @ @ @ @ 5 @
3 9 0 @ 3 @ @ @ @ @
4 @ @ 0 @ 2 @ 3 @ @
@ @ 3 @ 0 @ 2 @ 4 @
@ @ @ 2 @ 0 8 1 @ @
@ @ @ @ 2 8 0 @ @ 7
@ @ @ 3 @ 1 @ 0 4 @
@ 5 @ @ 4 @ @ 4 0 @
1 @ @ @ @ @ 7 @ @ 0
dp[0][0]:0 dp[0][1]:2 dp[0][2]:3 dp[0][3]:4 dp[0][4]:6 dp[0][5]:6 dp[0][6]:8 dp[0][7]:7 dp[0][8]:7 dp[0][9]:1
dp[1][0]:2 dp[1][1]:0 dp[1][2]:5 dp[1][3]:6 dp[1][4]:8 dp[1][5]:8 dp[1][6]:10 dp[1][7]:9 dp[1][8]:5 dp[1][9]:3
dp[2][0]:3 dp[2][1]:5 dp[2][2]:0 dp[2][3]:7 dp[2][4]:3 dp[2][5]:9 dp[2][6]:5 dp[2][7]:10 dp[2][8]:7 dp[2][9]:4
dp[3][0]:4 dp[3][1]:6 dp[3][2]:7 dp[3][3]:0 dp[3][4]:10 dp[3][5]:2 dp[3][6]:10 dp[3][7]:3 dp[3][8]:7 dp[3][9]:5
dp[4][0]:6 dp[4][1]:8 dp[4][2]:3 dp[4][3]:10 dp[4][4]:0 dp[4][5]:9 dp[4][6]:2 dp[4][7]:8 dp[4][8]:4 dp[4][9]:7
dp[5][0]:6 dp[5][1]:8 dp[5][2]:9 dp[5][3]:2 dp[5][4]:9 dp[5][5]:0 dp[5][6]:8 dp[5][7]:1 dp[5][8]:5 dp[5][9]:7
dp[6][0]:8 dp[6][1]:10 dp[6][2]:5 dp[6][3]:10 dp[6][4]:2 dp[6][5]:8 dp[6][6]:0 dp[6][7]:9 dp[6][8]:6 dp[6][9]:7
dp[7][0]:7 dp[7][1]:9 dp[7][2]:10 dp[7][3]:3 dp[7][4]:8 dp[7][5]:1 dp[7][6]:9 dp[7][7]:0 dp[7][8]:4 dp[7][9]:8
dp[8][0]:7 dp[8][1]:5 dp[8][2]:7 dp[8][3]:7 dp[8][4]:4 dp[8][5]:5 dp[8][6]:6 dp[8][7]:4 dp[8][8]:0 dp[8][9]:8
dp[9][0]:1 dp[9][1]:3 dp[9][2]:4 dp[9][3]:5 dp[9][4]:7 dp[9][5]:7 dp[9][6]:7 dp[9][7]:8 dp[9][8]:8 dp[9][9]:0
int dp[MAX_VEX_NUM][MAX_VEX_NUM] = {0};
void Floyd(AMG graph)
{
int n = graph.VexNum;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
dp[i][j] = graph.Edges[i][j];
//将已经存储好的边表复制给dp二维数组,因为不能改变graph本身的数据
for(int k=0; k<n; k++)
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]);//插入k点是否将路径距离变小了
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
printf("dp[%d][%d]:%-4d",i,j,dp[i][j]);
cout<<endl;
}
}
可运行总代码
#include <iostream>
#include <queue>
#include <stack>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
int dp[MAX_VEX_NUM][MAX_VEX_NUM] = {0};
void Floyd(AMG graph)
{
int n = graph.VexNum;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
dp[i][j] = graph.Edges[i][j];
//将已经存储好的边表复制给dp二维数组,因为不能改变graph本身的数据
for(int k=0; k<n; k++)
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]);//插入k点是否将路径距离变小了
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
printf("dp[%d][%d]:%-4d",i,j,dp[i][j]);
cout<<endl;
}
}
int main()
{
AMG graph = Create_AMG();
Print_AMG(graph);
Floyd(graph);
return 0;
}
c.Warshall算法
一般的,给定一个矩阵A(行列相等),我们对其使用Warshall算法:
//注,该矩阵上只有0或1两种元素,做加法时,1+1还是1
1、先找到该矩阵的对角线,并从对角线的左上方开始为第一个元素
2、以对角线上第一个元素为中心,按列展开,寻找中心所在的列中所有不为0的元素
3、将“ 该中心所在的行 ”加到“ 该中心所在的列 ”中所有不为0的元素所在的行上
4、加完之后,以对角线上第二个元素为中心,按列展开,寻找该列中所有不为0的元素
5、重复“ 操作3 ”
6、一直到将对角线上所有元素都展开后结束。
在此特别强调!!!一定!一定!不要对中心按行展开来求!!!
可能有一些求到的结果没有问题,但是有些情况是会求到错误的结果,原因是有些数值会被忽略
3.关键路径
a.拓扑排序
拓扑序列的特点:
(线性排列) (不唯一) (有向无环图一定存在) (有向有环图一定不存在)
应用:
判断是否存在环
在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。
先统计所有节点的入度,对于入度为0的节点就可以分离出来。
然后把这个节点指向的节点的入度减一。
循环操作操作,直到所有的节点都被分离出来。
如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是无解的情况。
关键:找入度为0的点输出,然后删除该顶点以及它所有的出边。
然后反复操作,直到全部顶点输出完为止。
或者剩余顶点入度均不为0。(存在回路,无法继续拓扑排序了)
设置一个辅助数组 indegree 用于存储各个顶点的入度
//有向图
1
10
15
1 2 3 4 5 6 7 8 9 10
1 2 2
1 3 3
1 4 4
5 3 3
6 7 8
8 9 4
4 6 2
3 2 9
4 8 3
10 1 1
9 2 5
5 9 4
7 5 2
7 10 7
6 8 1
3 0 1 2 2 2 2 1 1 1
该图中有环,不存在拓扑序列
不存在出度为0的点,所以没有TopoList.
//______________________________________________________________________________________
1
11
15
1 2 3 4 5 6 7 8 9 10 11
1 2 3
1 3 4
1 4 6
2 7 6
3 5 2
3 6 5
4 6 4
4 10 3
5 7 1
5 9 8
6 8 2
7 9 5
8 11 1
9 11 3
10 11 5
0 3 4 6 @ @ @ @ @ @ @
@ 0 @ @ @ @ 6 @ @ @ @
@ @ 0 @ 2 5 @ @ @ @ @
@ @ @ 0 @ 4 @ @ @ 3 @
@ @ @ @ 0 @ 1 @ 8 @ @
@ @ @ @ @ 0 @ 2 @ @ @
@ @ @ @ @ @ 0 @ 5 @ @
@ @ @ @ @ @ @ 0 @ @ 1
@ @ @ @ @ @ @ @ 0 @ 3
@ @ @ @ @ @ @ @ @ 0 5
@ @ @ @ @ @ @ @ @ @ 0
顶点:1 2 3 4 5 6 7 8 9 10 11
入度:0 1 1 1 1 2 2 1 2 1 3
出度:3 1 2 2 2 1 1 1 1 1 0
拓扑序列:1 2 3 4 5 6 7 8 9 10 11
不使用栈的版本
//不使用栈的版本
int *Topological_Sorting(AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
//为了不改变原来的入度序列,采用复制一个数组的方式
int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum);//用于存储拓扑序列
int cnt = 0;//表示已经存入序列的结点数目
for(int i=0; i<graph.VexNum; i++)//外层循环用于依次加入结点
{
int j;
for(j=0; j<graph.VexNum; j++)//第一个内层循环用于寻找一个入度为0的结点
{
if(indegree_Copy[j]==0) //找到了入度为0的顶点
{
TopoList[cnt++] = graph.vexs[j];//存入该结点
indegree_Copy[j]--;//对入度减一,防止j结点重复操作
break;
}
}
if(j==graph.VexNum && cnt!=graph.VexNum) //说明不存在入度为0的顶点了,且当前并未将所有结点存入
return NULL; //返回NULL,说明不存在拓扑序列
//遍历所有顶点,若存在以该结点为起点的边,则将该边删去,并边的终点的入度减一
for(int k=0; k<graph.VexNum; k++)
if(j!=k && graph.Edges[j][k]!=MAX_LEN)
indegree_Copy[k]--;
//这里也不用担心对已经加入序列的点再减一,因为变成负数后根本不会再参与后续计算了
}
return TopoList;
}
使用栈的版本
//
1
11
15
1 2 3 4 5 6 7 8 9 10 11
1 2 3
1 3 4
1 4 6
2 7 6
3 5 2
3 6 5
4 6 4
4 10 3
5 7 1
5 9 8
6 8 2
7 9 5
8 11 1
9 11 3
10 11 5
0 3 4 6 @ @ @ @ @ @ @
@ 0 @ @ @ @ 6 @ @ @ @
@ @ 0 @ 2 5 @ @ @ @ @
@ @ @ 0 @ 4 @ @ @ 3 @
@ @ @ @ 0 @ 1 @ 8 @ @
@ @ @ @ @ 0 @ 2 @ @ @
@ @ @ @ @ @ 0 @ 5 @ @
@ @ @ @ @ @ @ 0 @ @ 1
@ @ @ @ @ @ @ @ 0 @ 3
@ @ @ @ @ @ @ @ @ 0 5
@ @ @ @ @ @ @ @ @ @ 0
顶点:1 2 3 4 5 6 7 8 9 10 11
入度:0 1 1 1 1 2 2 1 2 1 3
出度:3 1 2 2 2 1 1 1 1 1 0
拓扑序列:1 4 10 3 6 8 5 2 7 9 11
//使用栈的版本
//区别就是加入序列的顺序不同了
int *Topological_Sorting_By_Stack(AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum);
int cnt = 0;
stack<int> s;
for(int i=0; i<graph.VexNum; i++)
if(indegree_Copy[i]==0)
{
s.push(i);
indegree_Copy[i]--;
}
while(!s.empty())
{
int i = s.top();
TopoList[cnt++] = graph.vexs[i];
s.pop();
for(int j=0; j<graph.VexNum; j++)
if(i!=j && graph.Edges[i][j]!=MAX_LEN)
indegree_Copy[j]--;
int k;
for(k=0; k<graph.VexNum; k++)
if(indegree_Copy[k]==0)
{
s.push(k);
indegree_Copy[k]--;
}
}
if(cnt!=graph.VexNum)
return NULL;
else
return TopoList;
}
b.关键路径(Critical Path)
(1)AOE网(Activity On Edge)
AOE网即边表示活动的『带权有向无环图』,与AOV网(顶点表示活动)相对应。
而拓扑排序恰恰就是在AOV网上进行的,这是拓扑排序与关键路径最直观的联系。
(2)AOE网是一个带权的有向无环图
顶点表示事件(Event),边表示活动(Activity),边的权值表示活动持续的时间(Weight)。
下面的就是一个AOV网:
其中 V1~V9 表示事件,a1~a11 表示活动:
活动的权值表示完成该活动所需要的时间
每一个事件 Vi 的含义都表示:在它之前的活动已经完成,在它之后的活动此时才可以开始
(3)AOE网的源点和汇点
一个工程中只有一个开始点和一个完成点
入度为 0 的点称为源点(工程的起始),出度为 0 的点称为汇点(工程的结束)。
下图中的红色顶点表示源点和汇点:
(4)关键路径
AOE网中的有些活动是可以并行进行的。
完成工程的最短时间是从源点到汇点的最长路径的长度。(注意这里的“最短”和“最长”)
其中在所有路径中长度最长的路径 就叫做 关键路径(Critical Path)。
【意思就是快的必须等慢的,类似于坐大巴旅游:到一个景点后所有人下车观览,然后必须等所有人都玩完了并且上到了大巴后,大巴才能启程才能去下一个地点】
关键活动(关系到整个工程的完成期限的活动)
关键路径上的活动都是关键活动 (权值增加,则会使『最长路径』的长度增加)
如下图中红色顶点和有向边构成的就是一条关键路径:
关键路径的长度就是完成路径上所有活动所需要的时间总和,即为 6+1+9+2 = 18。
(5)ETV(Earliest Time Of Vertex)
事件最早发生时间,就是该顶点的最早的全部人都到达的时间。
这里看 V5 ,V5 的 ETV 为 7(6 + 1),而非 5 (4+1),因为此时 a1 和 a4 都没有完成。
需要说明,事件的最早发生时间一定是从源点到该顶点进行计算的。
(6)LTV (Latest Time Of Vertex)
事件最晚发生时间,就是每个顶点对应的事件最晚需要开始的时间。
也就是该顶点的最晚的全部人都到达的时间,如果超出此时间将会延误整个工期(理解的关键)。
要计算某一个事件的最晚发生时间,需要从汇点开始从后往前倒推。
(值得注意的是,如果一个点有多条边,计算后继节点 - 边的权值所得到的结果中,要取最小的)
因为假设我们取的是最大的结果(也就是看上去最晚的时间),那么当从该时间到经过其他活动到达其他顶点时,势必会造成超时的后果,那么就会将工期不断延误。
所以这里要强调一点:最晚发生时间是指,在不延误整个工期(也就是让那些快的和最慢的一样慢)的前提下,尽可能晚进行的时间。
(7)ETE (Earliest Time Of Edge)
活动的最早开工时间,就是边的起点的最早开始时间。
活动 要最早开工时间为事件 的最早发生时间 6;同理,活动 的最早发生时间为事件 的最早发生时间 7。显然活动的最早开工时间就是活动发生前的事件的最早开始时间。
(8)LTE (Lastest Time of Edge)
活动的最晚发生时间,就是不推迟工期的最晚开工时间。
活动的最晚发生时间是基于事件的最晚发生时间来计算的。
(9)只要知道了每一个事件(顶点)的 ETV 和 LTV,就可以推断出对应的 ETE 和 LTE
此外还需要注意,关键路径是活动的集合,而不是事件的集合。
所以当我们求得 ETV 和 LTV 之后,还需要计算 ETE 和 LTE。
(10)关键路径算法
关键是这四个概念:ETV、LTV、ETE 和 LTE。
在求得了 ETE 与 LTE 之后,只需要判断两者是否相等,如果相等则为关键路径中的一条边。
事件的最早发生时间:vex_early( j )
从源点到顶点 j 的最长路径长度,一开始初始化为 0,通过拓扑排序顺序依次更新。
(其实也就是用压栈弹栈)
顺序拓扑:vex_early[j] = max(vex_early[i] + graph.Edges[i][j])【循环遍历 i 】
事件的最晚发生时间:vex_late( j ) 【在不延误整个工期的前提下】
从汇点往前算:vex_late( j ) = vex_early( n ) — 从 vj 到 vn 的『 最长 』路径长度
(如果不是减去最长的,那么其他较短的路径加上后就会超过先前的时间了)
逆序拓扑:vex_late[j] = min(vex_late[j] - graph.Edges[i][j])【循环遍历 i 】
活动的最早开始时间:edge_early( i )
活动 ai 由编号为 i 的边表示,该边由起点 vj 与终点 vk 组成。
则该活动的最早发生时间为:edge_early( i ) = vex_early( j )
活动的最晚开始时间:edge_late( i )
vk 的最迟发生时间减去该活动的持续时间:edge_late( i ) = vex_late( k ) — weight( i )
最后最晚开始时间与最早开始时间相等的活动称为关键活动
VE(j) = max{VE(i) + W(i,j)},i可以取多个值,迭代过程从原点到终点;i左j右;
VL(i) = min{VL(j) – W(i,j)},j可以取多个值,迭代过程从终点到原点;i左j右;
AE(当前活动) = VE(当前活动
的起始结点);通过结点时间求活动边时间;
AL(当前活动) = VL(当前活动的结束结点) – W(当前活动);通过结点时间求活动边时间;
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|
indegree | 0 | 1 | 1 | 1 | 1 | 2 | 2 | 1 | 2 | 1 | 3 |
outdegree | 3 | 1 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 0 |
vex_early | 0 | 3 | 4 | 6 | 6 | 10 | 9 | 12 | 14 | 9 | 17 |
vex_late | 0 | 3 | 4 | 9 | 6 | 14 | 9 | 16 | 14 | 12 | 17 |
crucial_path | V | V | V | X | V | X | V | X | V | X | V |
result | 1-> | 2 | -> | 3-> | 5 | -> | 7 | -> | 9 | -> | 11 |
//
1
11
15
1 2 3 4 5 6 7 8 9 10 11
1 2 3
1 3 4
1 4 6
2 7 6
3 5 2
3 6 5
4 6 4
4 10 3
5 7 1
5 9 8
6 8 2
7 9 5
8 11 1
9 11 3
10 11 5
****************************************************************************************
1:0 0 0 0 0 0 0 0 0 0 0
2:0 3 4 6 0 0 0 0 0 0 0
3:0 3 4 6 0 0 9 0 0 0 0
4:0 3 4 6 6 9 9 0 0 0 0
5:0 3 4 6 6 10 9 0 0 9 0
6:0 3 4 6 6 10 9 0 14 9 0
7:0 3 4 6 6 10 9 12 14 9 0
8:0 3 4 6 6 10 9 12 14 9 0
9:0 3 4 6 6 10 9 12 14 9 13
10:0 3 4 6 6 10 9 12 14 9 17
11:0 3 4 6 6 10 9 12 14 9 17
vex_early 数组:0 3 4 6 6 10 9 12 14 9 17
****************************************************************************************
1:17 17 17 17 17 17 17 17 17 17 17
2:17 17 17 17 17 17 17 16 14 12 17
3:17 17 17 9 17 17 17 16 14 12 17
4:17 17 17 9 6 17 9 16 14 12 17
5:17 17 17 9 6 14 9 16 14 12 17
6:17 3 17 9 6 14 9 16 14 12 17
7:17 3 9 9 6 14 9 16 14 12 17
8:17 3 4 9 6 14 9 16 14 12 17
9:3 3 4 9 6 14 9 16 14 12 17
10:0 3 4 9 6 14 9 16 14 12 17
11:0 3 4 9 6 14 9 16 14 12 17
vex_late 数组:0 3 4 9 6 14 9 16 14 12 17
****************************************************************************************
关键路径:0->1->2->4->6->8->10->10
void Crucial_Path (AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
int *outdegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(outdegree_Copy,outdegree,sizeof(int)*graph.VexNum);
int *vex_early = (int*)malloc(sizeof(int)*graph.VexNum);
int *vex_late = (int*)malloc(sizeof(int)*graph.VexNum);
memset(vex_early,0,sizeof(int)*graph.VexNum); //初始化数组为0
//int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum); //顺序拓扑序列
//int *Counter_TopoList = (int*)malloc(sizeof(int)*graph.VexNum); //逆序拓扑序列
for(int i=0; i < graph.VexNum; i++) //顺序拓扑序列取最大值
{
for(int i=0;i<graph.VexNum;i++)
cout<<vex_early[i]<<" ";
cout<<endl;
if(indegree_Copy[i] == 0) //说明找到了入读为0的点
{
for(int j=0; j < graph.VexNum; j++) //以该点为准,将该点加入拓扑序列中,然后更新该点所有的后继节点的最早发生时间
{
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //如果顶点j与顶点i有边,则删除这条边,并且顶点j的入度-1,同时更新最早发生时间
{
vex_early[j] = max(vex_early[j],graph.Edges[i][j]+vex_early[i]);//当前的时间比从i赶到j后的时间还长,则以慢的为准
indegree_Copy[j]--;
}
}
}
}
int k = graph.VexNum-1; //最后一个顶点的下标
for(int i=0; i < graph.VexNum; i++) //将 vex_late 数组全部初始化为 vex_early 最后一顶点的值
vex_late[i] = vex_early[k];
for(int i=k; i>=0; i--) //逆序拓扑序列取最小值,从后往前搜索
{
for(int i=0;i<graph.VexNum;i++)
cout<<vex_late[i]<<" ";
cout<<endl;
if(outdegree_Copy[i] == 0)
{
for(int j=0; j <graph.VexNum; j++)
{
if(graph.Edges[j][i]!=MAX_LEN && i!=j)
{
vex_late[j] = min(vex_late[j],vex_late[i]-graph.Edges[j][i]);//取小值
outdegree_Copy[j]--;
}
}
}
}
cout << "\n****************************\n";
cout << "vex_early 数组:";
for(int i=0; i < graph.VexNum; i++)
{
cout << vex_early[i] << " ";
}
cout << endl;
cout << "vex_late 数组:";
for(int i=0; i < graph.VexNum; i++)
{
cout << vex_late[i] << " ";
}
cout << "\n****************************\n";
cout << "关键路径:";
for(int i=0; i < graph.VexNum; i++)
{
if(vex_early[i] == vex_late[i])
{
cout<<i<<"->";
}
}
cout << k << endl;
}
可运行总代码
#include <iostream>
#include <queue>
#include <stack>
#include <memory.h>
#include <algorithm>
#define VertexType int
#define EdgeType int
#define MAX_VEX_NUM 100
#define MAX_Edge_NUM 1000
#define MAX_LEN 1e6
using namespace std;
typedef struct AMG
{
VertexType vexs[MAX_VEX_NUM]; //存储了顶点的值,下标即代表着顶点的序号
EdgeType Edges[MAX_VEX_NUM][MAX_VEX_NUM]; //各个顶点之间的边的有无与权值
int VexNum; //顶点个数
int ArcNum; //边的条数
int flag; //标志着图的属性(0代表无向图,1代表有向图)
} AMG;
int Locate_Vex_AMG(AMG Graph,VertexType v) //根据所给节点的权值,来寻找节点在图中的下标
{
for(int i=0; i<Graph.VexNum; i++)
if(Graph.vexs[i]==v)
return i;
return -1; //找不到就返回 -1
}
AMG Create_AMG(void) //创造一个用邻接矩阵实现的图
{
AMG Graph;
cout<<"请输入图的属性(0代表无向图,1代表有向图)"<<endl;
cin>>Graph.flag;
while(Graph.flag!=0 && Graph.flag!=1)
{
cout<<"输入有误!"<<endl;
cin>>Graph.flag;
}
cout<<"请输入顶点个数"<<endl;
cin>>Graph.VexNum;
cout<<"请输入边的条数"<<endl;
cin>>Graph.ArcNum;
cout<<"请输入顶点的权值"<<endl; //顶点的权值在以边为核心的情况下,顺序输入从1到n即可
for(int i=0; i<Graph.VexNum; i++)
cin>>Graph.vexs[i];
//将邻接矩阵初始化,即先默认为零图,每条边的距离都是MAX_LEN
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
Graph.Edges[i][j] = MAX_LEN;
Graph.Edges[i][i] = 0;
}
cout<<"请输入边的信息(边的两个顶点+边的权值)"<<endl;//在有向图中则存在Head和Tail的区别
for(int k=0; k<Graph.ArcNum; k++)
{
VertexType v1,v2; //临时变量用于存储输入的顶点
EdgeType w; //临时变量用于存储输入的边
cin>>v1>>v2>>w;
int i = Locate_Vex_AMG(Graph,v1); //通过输入的顶点的值来找到顶点的位置或者序列
int j = Locate_Vex_AMG(Graph,v2);
Graph.Edges[i][j] = w; //在邻接矩阵中存储一条从点v1指向v2的边w
if(Graph.flag==0)
Graph.Edges[j][i] = w; //无向图需要多一步操作
}
return Graph;
}
void Print_AMG(AMG Graph)
{
for(int i=0; i<Graph.VexNum; i++)
{
for(int j=0; j<Graph.VexNum; j++)
{
if(Graph.Edges[i][j]!=MAX_LEN)
printf("%-4d",Graph.Edges[i][j]);
else
printf("@ ");
}
cout<<endl;
}
}
int *indegree;
int *outdegree;
//第一列对应的是起点,第一行对应的是终点
//Edges[i][j]指从i点到j点的一条边(其中i为起点,j为终点)
int *indegree_AMG(AMG graph)//入度是计数以该顶点为终点(进入该点)的边
{
int *indegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(indegree,0,sizeof(int)*graph.VexNum);
for(int j=0; j<graph.VexNum; j++) //外层循环以点为单位,先遍历列(不同的终点)
for(int i=0; i<graph.VexNum; i++) //内层循环以边(顶点对)为单位,再遍历以该点为终点的所有边
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //因此这样计算的是j点的入度(从1-n到j)
indegree[j]++;
return indegree;
}
int *outdegree_AMG(AMG graph)//出度是计数以该顶点为起点(离开该点)的边
{
int *outdegree = (int*)malloc(sizeof(int)*graph.VexNum);
memset(outdegree,0,sizeof(int)*graph.VexNum);
for(int i=0; i<graph.VexNum; i++) //外层循环以点为单位,先遍历行(不同的起点)
for(int j=0; j<graph.VexNum; j++) //内层循环以边(顶点对)为单位,再遍历以该点为起点的所有边
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //因此这样计算的是i点的出度(从i到1-n)
outdegree[i]++;
return outdegree;
}
int *Topological_Sorting(AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
//为了不改变原来的入度序列,采用复制一个数组的方式
int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum);//用于存储拓扑序列
int cnt = 0;//表示已经存入序列的结点数目
for(int i=0; i<graph.VexNum; i++)//外层循环用于依次加入结点
{
int j;
for(j=0; j<graph.VexNum; j++)//第一个内层循环用于寻找一个入度为0的结点
{
if(indegree_Copy[j]==0) //找到了入度为0的顶点
{
TopoList[cnt++] = graph.vexs[j];//存入该结点
indegree_Copy[j]--;//对入度减一,防止j结点重复操作
break;
}
}
if(j==graph.VexNum && cnt!=graph.VexNum) //说明不存在入度为0的顶点了,且当前并未将所有结点存入
return NULL; //返回NULL,说明不存在拓扑序列
//遍历所有顶点,若存在以该结点为起点的边,则将该边删去,并边的终点的入度减一
for(int k=0; k<graph.VexNum; k++)
if(j!=k && graph.Edges[j][k]!=MAX_LEN)
indegree_Copy[k]--;
//这里也不用担心对已经加入序列的点再减一,因为变成负数后根本不会再参与后续计算了
}
return TopoList;
}
int *Topological_Sorting_By_Stack(AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum);
int cnt = 0;
stack<int> s;
for(int i=0; i<graph.VexNum; i++)
if(indegree_Copy[i]==0)
{
s.push(i);
indegree_Copy[i]--;
}
while(!s.empty())
{
int i = s.top();
TopoList[cnt++] = graph.vexs[i];
s.pop();
for(int j=0; j<graph.VexNum; j++)
if(i!=j && graph.Edges[i][j]!=MAX_LEN)
indegree_Copy[j]--;
int k;
for(k=0; k<graph.VexNum; k++)
if(indegree_Copy[k]==0)
{
s.push(k);
indegree_Copy[k]--;
}
}
if(cnt!=graph.VexNum)
return NULL;
else
return TopoList;
}
void Crucial_Path (AMG graph)
{
int *indegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(indegree_Copy,indegree,sizeof(int)*graph.VexNum);
int *outdegree_Copy = (int*)malloc(sizeof(int)*graph.VexNum);
memcpy(outdegree_Copy,outdegree,sizeof(int)*graph.VexNum);
int *vex_early = (int*)malloc(sizeof(int)*graph.VexNum);
int *vex_late = (int*)malloc(sizeof(int)*graph.VexNum);
memset(vex_early,0,sizeof(int)*graph.VexNum); //初始化数组为0
int *TopoList = (int*)malloc(sizeof(int)*graph.VexNum); //顺序拓扑序列
int *Counter_TopoList = (int*)malloc(sizeof(int)*graph.VexNum); //逆序拓扑序列
for(int i=0; i < graph.VexNum; i++) //顺序拓扑序列取最大值
{
for(int i=0;i<graph.VexNum;i++)
cout<<vex_early[i]<<" ";
cout<<endl;
if(indegree_Copy[i] == 0) //说明找到了入读为0的点
{
for(int j=0; j < graph.VexNum; j++) //以该点为准,将该点加入拓扑序列中,然后更新该点所有的后继节点的最早发生时间
{
if(graph.Edges[i][j]!=MAX_LEN && i!=j) //如果顶点j与顶点i有边,则删除这条边,并且顶点j的入度-1,同时更新最早发生时间
{
vex_early[j] = max(vex_early[j],graph.Edges[i][j]+vex_early[i]);//当前的时间比从i赶到j后的时间还长,则以慢的为准
indegree_Copy[j]--;
}
}
}
}
int k = graph.VexNum-1; //最后一个顶点的下标
for(int i=0; i < graph.VexNum; i++) //将 vex_late 数组全部初始化为 vex_early 最后一顶点的值
vex_late[i] = vex_early[k];
for(int i=k; i>=0; i--) //逆序拓扑序列取最小值,从后往前搜索
{
for(int i=0;i<graph.VexNum;i++)
cout<<vex_late[i]<<" ";
cout<<endl;
if(outdegree_Copy[i] == 0)
{
for(int j=0; j <graph.VexNum; j++)
{
if(graph.Edges[j][i]!=MAX_LEN && i!=j)
{
vex_late[j] = min(vex_late[j],vex_late[i]-graph.Edges[j][i]);//取小值
outdegree_Copy[j]--;
}
}
}
}
cout << "\n****************************\n";
cout << "vex_early 数组:";
for(int i=0; i < graph.VexNum; i++)
{
cout << vex_early[i] << " ";
}
cout << endl;
cout << "vex_late 数组:";
for(int i=0; i < graph.VexNum; i++)
{
cout << vex_late[i] << " ";
}
cout << "\n****************************\n";
cout << "关键路径:";
for(int i=0; i < graph.VexNum; i++)
{
if(vex_early[i] == vex_late[i])
{
cout<<i<<"->";
}
}
cout << k << endl;
}
int main()
{
AMG graph = Create_AMG();
Print_AMG(graph);
indegree = indegree_AMG(graph);
outdegree = outdegree_AMG(graph);
for(int i=0; i<graph.VexNum; i++)
cout<<i<<" ";
cout<<endl;
for(int i=0; i<graph.VexNum; i++)
cout<<indegree[i]<<" ";
cout<<endl;
for(int i=0; i<graph.VexNum; i++)
cout<<outdegree[i]<<" ";
cout<<endl;
int *TopoList = Topological_Sorting_By_Stack(graph);
if(TopoList)
for(int i=0; i<graph.VexNum; i++)
cout<<TopoList[i]<<" ";
else
cout<<"该图中有环,不存在拓扑序列";
Crucial_Path(graph);
return 0;
}
AOV 顶点活动网
DAG 有向无环图
(用顶点表示活动)
五.图的习题
1.长度为k的简单路径
采用链接表存储结构,编写一个判别无向图中任意给定的两个顶点(u,v)之间是否存在一条长度为k的简单路径
首先,简单路径要求路径上的各顶点均不互相重复,因此要设置一个辅助数组visited来避免路径上出现重复的顶点。
使用DFS算法,利用辅助变量step作为参数来记录当前路径的长度,当step的值为k时,判断该点是否为v,如果是,则说明u与v两个顶点之间存在一条长度为k的简单路径,直接跳出递归;如果不是k,则回溯路径,退回到上一个节点在去搜索,直到找到路径或者找完了所有可能为止。
这里设置一个辅助标志flag,用于存储答案,同时在找到路径快速跳出循环。
bool flag;
void func(ALG graph,int u_index,int v_index,int k,int step)
{
if(!flag && step==k && u_index==v_index)
flag = true;
if(!flag)
{
for(int i=0; i<graph.VexNum; i++)
if(!visited[i] && Judge_Edge(graph,u_index,i) && step<k)
{
visited[i] = true;
func(graph,i,v_index,k,step+1);
visited[i] = false;
}
}
}
bool Search_K_Path(ALG graph)
{
flag = false;
memset(visited,false,sizeof(bool)*graph.VexNum);
cout<<"请输入起点:";
VertexType u;
cin>>u;
int u_index = Locate_Vex_ALG(graph,u);
cout<<"请输入终点:";
VertexType v;
cin>>v;
int v_index = Locate_Vex_ALG(graph,v);
cout<<"请输入简单路径的长度:";
int k;
cin>>k;
visited[u_index] = true;
func(graph,u_index,v_index,k,0);
if(flag)
cout<<"两个顶点(u,v)之间存在一条长度为"<<k<<"的简单路径"<<endl;
else
cout<<"两个顶点(u,v)之间不存在一条长度为"<<k<<"的简单路径"<<endl;
return flag;
}