目录
图论
一、 图的基本概念和术语
网状结构,逻辑关系多对多
- 图:图G由集合V和E组成,记为G=(V,E)。图中的结点称为顶点,V(G)是顶点的非空有穷集;相关的顶点偶对称为边,E(G)是边的有穷集。
图分为有向图、无向图
- 有向图(Digraph)——V(G)是顶点的非空有限集;E(G)是有向边(即弧)的有限集合,弧是顶点的有序对,记<v, w>,v为弧尾(Tail),w为弧头(Head)
- 无向图(Undigraph)——V(G)是顶点的非空有限集;E(G)是边的有限集合,边是顶点的无序对,记(v, w)或(w, v),并且(v, w)=(w, v).
-
顶点:表示数据元素
-
边:表示数据元素之间的逻辑关系,分为有向边(顶点的有序对)和无向边(顶点的无序对)
-
网:边带权的无向图称作无向网;弧带权的有向图称作有向网。
-
完全图:n个顶点的含有 n(n-1)/2 条边的无向图称作完全图;n个顶点的含有 e=n(n-1) 条弧的有向图称作 有向完全图.
若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图。 -
邻接点、关联:假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点,边(v,w)和顶点v 和w相关联
-
度:无向图中和顶点v 关联的边的数目定义为v的度,记为TD(v)
有向图顶点的度分为入度和出度
■ 入度:有向图中以顶点v为弧头的弧的数目称为顶点v的入度,记为ID(v)
■ 出度:有向图中以顶点v为弧尾的弧的数目称为顶点v的出
度,记为OD(v) -
路径、路径长度:设无向图G=(V,E)中的一个顶点序列 u = v i , 0 , v i , 1 , … , v i , m = w { u=v_i,₀,v_i,₁, …, v_i, m=w} u=vi,0,vi,1,…,vi,m=w中,若 ( v i , j − 1 , v i , j ) ∈ E , 1 ≤ j ≤ m (v_{i, j-1},v_{i, j})∈E,1 ≤ j ≤ m (vi,j−1,vi,j)∈E,1≤j≤m,则称从顶点u 到顶点w 之间存在一条路径;路径上边的数目称作路径长度
■ 简单路径:序列中顶点不重复出现的路径
■ 简单回路:序列中第一个顶点和最后一个顶点相同的路径 -
连通图:若无向图G中任意两个顶点之间都有路径相通,则称此图为连通图。
-
连通分量:若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。
-
强连通图:有向图中若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。否则,其各个极大强连通子图称作它的强连通分量。
-
生成树:假设一个连通图有 n 个顶点和 e 条边,其中 n-1 条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树。
-
生成森林:对非连通图,则称由各个连通分量的生成树的集合为此非连通图的生成森林。
-
有向树:如果一个有向图恰有1个顶点的入度为0,其余的顶点入度均为1,则称该图为一棵有向树
- 对于非强连通图的一个强连通分量:包含其全部n个顶点、n-1条弧、且只有1个顶点的入度为0、其余的顶点入度均为1的子图称为该连通分量的有生成向树.
- 对于非强连通图的所有强连通分量的有向生成树构成该有向图的生成森林.
- 一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧.
二、图的存储结构
■ 图的存储表示(非顺序存储映像):
- 图的数组(邻接矩阵)存储表示
- 图的邻接表存储表示
- 有向图的十字链表存储表示
- 无向图的邻接多重表存储表示
■ 设计图的存储表示,应考虑方便以下操作:
• 求入度,出度
• 求邻接顶点
• 判断顶点之间是否有边相连
1. 数组(邻接矩阵)存储表示
无向图的数组(邻接矩阵)存储表示
■ n个顶点的图用2个数组存放:二维数组(
n
∗
n
n*n
n∗n的矩阵)存放顶点之间的逻辑关系(图中的边、弧),一维数组存放顶点信息(数据元素的值)
■ 设无向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),A[i][j]
表示顶点
v
i
v_i
vi 和
v
j
v_j
vj之间是否存在连边
可以利用对称阵的压缩存储方法存储
无向图顶点
v
i
v_i
vi的度
有向图的数组(邻接矩阵)存储表示
设有向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E), A[i][j]
表示是否存在顶点
v
i
v_i
vi流向顶点
v
j
v_j
vj的弧.
有向图邻接矩阵不一定是对称阵
无向网的邻接矩阵 w i j w_{ij} wij表示在顶点 v i v_i vi 和 v j v_j vj的连边上的权值。
有时:也用 ∞ ∞ ∞代表没有边有向网的邻接矩阵 w i j w_{ij} wij表示在顶点 v i v_i vi流向顶点 v j v_j vj的弧上的权值.
#define MAXSIZE 100
typedef struct {
VertexType vexs[MAXSIZE]; //一维数组存放顶点信息
int arcs[MAXSIZE][MAXSIZE];//邻接矩阵
int vexnum, arcnum; //顶点数和边数
int kind; //图的类型:有向图还是无向图
} MGraph;
MGraph T;
邻接表存储表示
无向图的邻接表:为顶点vi所建的单链表是将与顶点vi相关联 的边建成一个单链表;或者说:将顶点vi的邻接点建成一个单链表。
■ 图的一种链式存储结构,对图中每个顶点建立一个单链表,为顶
点
v
i
v_i
vi所建的单链表是将与顶点
v
i
v_i
vi相关联的边或弧建成一个单链表。
■ 用一维数组存放每个顶点的信息和相应单链表的头指针。
■ 每个数组元素存放图中一个顶点:顶点的数据(data
)和为其所建单链表的头指针(firstarc
)。
为便于维护数据的一致性,通常单链表中只要给出邻接点的存放位置----在一维数组中对应数组元素的下标即可
存的边数是2e个 ,也就是每个弧存了2次
有向图的邻接表:第i个单链表(为
v
i
v_i
vi所建单链表)中的结点是从顶点
v
i
v_i
vi流出的弧流向的顶点
顶点vi的出度是第i个单链表中含有的数据元素的个数,即为vi所建单链表的长度。
在有向图的邻接表中不易找到指向该顶点的弧。
typedef struct ArcNode {
int vex; // 该弧所指向的顶点的位置
struct ArcNode *link; // 指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
}ArcNode;//单链表节点类型
typedef struct VNode {
int data;
// 顶点信息 int 可换位别的对于的数据类型 可能是字符型 或 自己定义的结构体类型 等
ArcNode *firstarc; // 指向 相当于链表的头指针 指向该节点的出度 有向图 (度 无向图)
}VNode;//数组元素类型
typedef struct {
VNode arc[MAXSIZE]; // 顶点数组
int vexnum, arcnum;//顶点的个数 以及 边的个数
int kind; //表示有向图还是无向图
}Graphs;
逆向(逆邻接表):
有向图的逆邻接表中,为顶点vi建的单链表示流向该顶点的弧
网的邻接表表示
有向图的十字链表存储表示
每个顶点建2个单链表:流出去的弧建一个单链表,流入的弧建一个单链表。
typedef structArcBox { // 弧的结构表示
int tailvex, headvex;
InfoType *info;//注意定义的类型
struct ArcBox *hlink,*tlink;
}ArcBox;
typedef structVexNode { // 顶点的结构表示
int data;
ArcBox *firstin,*firstout;
}VexNode;
typedef struct {
VexNode xlist[MAXSIZE]; // 顶点信息
int vexnum, arcnum; //有向图的当前顶点数和弧数
}OLGraph;
无向图的邻接多重表存储表示
边的结构表示
typedef struct Ebox {
int mark;
int ivex, jvex;
struct EBox *ilink, *jlink;
} EBox;
三、图的遍历算法
图的遍历:从图中某个顶点出发访遍图中其余顶
点,并且使图中的每个顶点仅被访问一次的过程
遍历应用举例:
- 判断图的连通性、
- 求等价类等
- 求连通分量
- 求路径相通
深度优先搜索和广度优先搜索
图的遍历——深度优先搜索(DFS)
- 图的存储——邻接矩阵和邻接表均可以
- 判别图中的顶点v的邻接点是否被访问的方法:
⮚ 解决的办法:为每个顶点设立一个“访问标志”,设一维数组visited[ ]
,
① visited[v]=1表示顶点v已经被访问
② visited[v]=0表示顶点v尚未被访问
③ 初始化时,所有顶点均为未被访问
- 深度优先搜索生成树:访问时经过的顶点和边构成 的子图
- 深度优先搜索生成森林:若选用多个出发点做深度优先搜索,会产生多棵深度优先搜索生成树—构成深度优先搜索生成森林.
深度优先搜索遍历连通图的过程类似于树的先根遍历`
图的遍历序列不唯一
深度优先搜索应用:
判断无向图是否连通?
若从无向图中任一点出发能访问到图中所
有顶点,则该图为连通图
判断有向图是否强连通?
若从有向图中每一点出发能访问到图中所有顶点,则该图为强连通图.
实现方法:
以邻接表为例实现图的深度优先搜索
存储结构为:
// 这几个结构体需要结合邻接矩阵来理解 邻接矩阵中存放的是两个顶点之间是否有弧存在 存在的话是 或者是这个弧的权重
//使用邻接表的话 我们可以表示出每个顶点的信息 但是我们缺乏的是 弧的信息 所以 第一个结构体就是 弧 ArcNode结构体就是来存放弧的信息
typedef struct ArcNode { //这个结构体是为了表示 弧的 表示的是 有位置vex顶点 指向 link位置的 “位置” 的顶点
int vex; // 该弧所指向的顶点的位置
struct ArcNode *link; // 指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
} ArcNode;//单链表节点类型
typedef struct VNode {
int data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
} VNode;//数组元素类型
typedef struct {
VNode arc[MAXSIZE]; //创建存放 定点信息的数组
int vexnum, arcnum;//顶点的个数 以及 弧的个数 弧的个数
int kind; //表示有向图还是无向图
} Graphs;
DFS实现流程图以及代码:
深度优先搜索算法代码示例:
//结构体定义
//第一个结构体存放 弧 的信息 弧 结构体对象
typedef struct ArcNode {
int vex; // 该弧所指向的顶点的位置
struct ArcNode *link; // 指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
} ArcNode;//单链表节点类型
//存放顶点信息 顶点结构体 对象
typedef struct VNode {
int data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
} VNode;//数组元素类型
//图 结构体 对象
typedef struct {
VNode arc[MAXSIZE];
int vexnum, arcnum;
int kind; //表示有向图还是无向图
} Graphs;
//深度优先搜索
void DFSTraverse(Graphs G){
for(int v=0;v<G.vexnum;++v){
visited[v]=0;//标记为都未搜索过
//visited
for(v=0;v<G.vexnum;++v){
if(!visited[v])//确保每个都被访问到
DFS(G,v); //
}
}
}
//深度搜索优先算法
void DFS(Graphs G,int v){
printf("%d\t",v);//访问
visited[v]=1;//标记该节点被访问到
p=G.arc[v].firstarc;//往后搜索
while(p){
w=p->vex;//该节点的信息存放的位置
if(visited[w]==0)//判断是否被访问过
DFS(G,w);//开始深度优先搜索
p=p->link;//接着往后搜索
}
}
图的遍历——广度优先搜索(BFS)
■ 广度优先搜索生成树:访问时经过的顶点和边构成的子图。
■ 广度优先搜索生成森林:选用多个出发点做广度优先搜索,会产生多棵广度优先搜索生成树—构成广度优先搜索生成森林。
■ 对连通图,从起始点v到其余各顶点必定存在路径。按此路径长度递增次序访问。
int visited[Max]; // 是否访问过
//
void BFSTraverse(Graphs G){
for(v=0; v<G.vexnum; ++v)
visited[v] = 0;//初始化 标记都没有访问过
for( v=0; v<G.vexnum; ++v )
if(!visited[v]) //为了保证每个节点都被访问
BFS(G, v);//广度优先搜索
}
//广度有限搜索 算法
void BFS(Graphs G,int v){
int Q[MAX],f=0,r=0;
//初始化队列
printf("%d\t",w);//访问
visited[v]=1;//标记为访问过
Q[r++]=v; //入队 v是位置 这个位置上是第一个节点 在数组中的位置
while(f<r){//队列是否为空
x=Q[f++];//出队
p=G.arc[x].firstarc; //往后接着搜索
while(p){ //理解为 x 的一圈访问一下
w=p->vex;//下一个的位置
if(visited[w]==0){//是否访问过
visited[w]=1;//开始访问 标记为访问过
printf("%d\t",w);//访问
//如果队列满了 提示
if(r==MAXSIZE)//队列满了
exit(-1);
Q[r++]=w;//入队
}
p=p->link;//向后访问
}
}
}
//将此算法改成循环队列 如果使用 循环队列如何实现
//跟 层次遍历二叉树的算法 比较 (层次遍历 树 的算法 进行 比较 观察有什么异同点)
最小生成树
四、图的遍历算法的应用(无向图)
(待补充)
求两个点之间的最短路径
求两个顶点之间的简单路径
直接使用深度优先搜索还不太行 需要先处理一下
连通分量和生成树
遍历应用举例:
- 判断图的连通性、
- 求等价类等
- 求连通分量
- 求路径相通
五、图的连通性问题
生成树的存放方式:
- 孩子兄弟表示法
- 双亲表示法
⮚ 若从无向图中任一点出发采用深度优先搜索或广度优先搜索能访问到图中所有顶点,则该图为连通图,否则为非连通图
对连通图:深度优先搜索或广度优先搜索访问时经过的顶点和边构成的子图称为深度优先搜索生成树或广度优先搜索生成树
对非连通图:深度优先搜索或广度优先搜索访问时经过的顶点和边构成的子图称为深度优先搜索生成森林或广度优先搜索生成森林
连通性:
void DFSForest(Graph G, CSTree &T)
{
T=NULL;
for(v=0; v<G.vexnum; ++v)
visited[v] = 0; //标记所有节点都未访问过
for(v=0; v<G.vexnum; ++v)
if (!visited[v]){ //开始对每个节点进行访问
s=(CSTree)malloc(sizeof(CSNode));
//
s->data=G.arc[v].data;
//
s->fc=NULL;
s->nb=NULL;
if(!T)
T=s;
else
q->nb=s;
q=s;
DFSTree(G, v, s);
}
}
void DFSTree(Graph G, int v, CSTree&T){
visited[v] = 1; //标记该节点 访问过
first=TRUE;//
for(p=G.arc[v].firstarc; p!=NULL; p=p->link){
w=p->vex;
if(!visited[w]){
s=(CSTree)malloc (sizeof(CSNode));
s->data=G.arc[w].data;
s->fc=NULL;
s->nb=NULL;
if (first){
T->fc=s;
first=FALSE:
}
else
T->nb=s;
T=s;
DFSTree(G, w, T);}
}
}
DFSForest
函数
-
初始化:
T = NULL; for (v = 0; v < G.vexnum; ++v) visited[v] = 0; // 标记所有节点都未访问过
这里初始化了森林
T
,并将所有节点的访问标记visited
设置为 0,表示它们都未被访问过。 -
遍历每个顶点:
for (v = 0; v < G.vexnum; ++v) if (!visited[v]) { // ...创建新树根节点... DFSTree(G, v, s); }
对于每个顶点,如果它未被访问过,则创建一个新的树根节点
s
并调用DFSTree
函数从该顶点开始构建 DFS 树。 -
创建新树根节点:
s = (CSTree)malloc(sizeof(CSNode)); s->data = G.arc[v].data; s->fc = NULL; s->nb = NULL; if (!T) T = s; else q->nb = s; q = s;
访问顶点v后,依次从v的每一个未被访问的邻接点,第一个邻接点是它的长子(fc),其它的每一个邻接点依次是前一个邻接点的右邻兄弟(nb)
为当前未访问的顶点创建一个新的树节点s
。如果T
为空(表示这是第一棵树),则将T
设置为s
。否则,将前一个树节点的nb
指针指向s
,并更新q
为s
。
DFSTree
函数
-
标记顶点已访问:
visited[v] = 1; // 标记该节点 访问过 first = TRUE;
-
遍历邻接表:
for (p = G.arc[v].firstarc; p != NULL; p = p->link) { w = p->vex; if (!visited[w]) { s = (CSTree)malloc(sizeof(CSNode)); s->data = G.arc[w].data; s->fc = NULL; s->nb = NULL; if (first) { T->fc = s; first = FALSE; } else { T->nb = s; } T = s; DFSTree(G, w, T); } }
遍历顶点
v
的所有邻接顶点w
。对于每一个未访问的邻接顶点w
,创建一个新的树节点s
。如果这是第一个孩子节点,则将T
的fc
指针指向s
,并设置first
为FALSE
。否则,将T
的nb
指针指向s
。然后递归调用DFSTree
函数以继续构建树。
DFSForest
函数:遍历每个顶点,若顶点未访问过,则创建一个新的树节点并调用DFSTree
函数来构建树。DFSTree
函数:从给定顶点开始,以深度优先搜索的方式递归构建树节点,并连接其所有未访问的邻接顶点为树的孩子节点。
这个过程最终会构建一个由多个 DFS 树组成的森林,每棵树对应图的一个连通分量。
生成树
■ 生成树:假设一个连通图有 n n n 个顶点和 e e e条边,其中 n − 1 n-1 n−1 条边和 n n n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树。
生成森林:对非连通图,则称由各个连通分量的生成树的集合为此非连通图的生成森林。
最小生成树 带权图的生成树上的各边权值之和 称为这棵树的代价。最小代价生成树是各边权值总和最小的生成树
最小生成树的性质(MST性质) 采用深度优先搜索 或 广度优先搜索 求? 不太行 (为什么 ??? )
贪心策略 不一定是最优解 但在生成最小生成树的时候 是适用的
Prim普利姆算法
Kruscal算法具有MST性质 就是最小生成树
MST性质(最小生成树性质): 令G=(V, E,
W)为一个带权连通图,T为G的一生成树。对任一不在T中的边uv,如果将uv加入T中会产生一回路,使得uv是回路中权值最大的边。那么树T具有MST性质。
Prim算法的原理
- 初始化:从图中的一个起始顶点开始,逐步将权值最小的边添加到生成树中。
- 扩展生成树:在每一步中,选择当前生成树中所有顶点到生成树外所有顶点中权值最小的一条边,并将该边和对应的顶点加入生成树。
- 重复上述步骤:直到所有顶点都包含在生成树中。
数据结构
图采用邻接矩阵和邻接表存放均可。下面以邻接矩阵为例实现Prim算法:
- 图的表示:用一个二维数组
arcs
表示图的邻接矩阵,arcs[i][j]
表示顶点i
到顶点j
之间的边的权值。vexnum
表示图的顶点数,arcnum
表示边的数量。 - 边的类型:
EdgeType
结构体用于存放每个顶点到生成树中顶点的最小权值边的信息。adjvex
表示这条边连接的另一个顶点,lowcost
表示这条边的权值。
#define MAX 100
#define MAXEDGE 1000000
typedef struct {
int arcs[MAX][MAX];
int vexnum, arcnum;
} AGraphs;
typedef struct {
int adjvex; // 与生成树中的点连接的最好情况,记录的是与生成树中的哪个点(记录的是点的位置)
int lowcost; // 记录的是该最好情况的弧的权重
} EdgeType;
EdgeType closedge[MAX]; // 采用一维数组closedge[MAX]存放图中每个顶点与生成树中顶点相连的最好情况
算法步骤解释
- 初始化
closedge
数组:
void prim(AGraphs G, int u) {
int i, j, k;
EdgeType closedge[MAX];
for (j = 0; j < G.vexnum; j++) {
closedge[j].adjvex = u; // 此次选中的与生成树中连接情况最好的点的位置
closedge[j].lowcost = G.arcs[u][j]; // 记录下来这个数值的大小
}
closedge[u].lowcost = 0; // 如果顶点已经包含在生成树中,则lowcost设为0。
- 逐步扩展生成树:
for (i = 1; i < G.vexnum; i++) {
k = minclosedge(closedge);
printf("(%d,%d)\n", closedge[k].adjvex, k);
closedge[k].lowcost = 0;
for (j = 0; j < G.vexnum; j++) {
if (G.arcs[k][j] < closedge[j].lowcost) {
closedge[j].lowcost = G.arcs[k][j];//选中
closedge[j].adjvex = k; //选中
}
}
}
}
- 找最小权值边:
int minclosedge(EdgeType closedge[]) {
int min = MAXEDGE, j, k = -1;
for (j = 0; j < G.vexnum; j++) {
if (closedge[j].lowcost != 0 && closedge[j].lowcost < min) {
min = closedge[j].lowcost; //选中
k = j;
}
}
return k;
}
算法实现
每一步只保留不在生成树中的点和生成树相连的最好情况
实现算法时,在每一步我们只保留不在生成树中的点和生成树相连的最好情况,而不是考察不在生成树中的点和生成树相连的所有情况。
每次加入一个顶点和一条边进入生成树后,我们都考察一下不在生成树中的点和生成树中的点相连的最好情况是否被新加入的点更新。
代码示例
#define MAX 100
#define MAXEDGE 1000000
typedef struct {
int arcs[MAX][MAX];
int vexnum, arcnum;
} AGraphs;
typedef struct {
int adjvex;
int lowcost;
} EdgeType;
// 采用一维数组closedge[MAX]存放图中每个顶点与生成树中顶点相连的最好情况
/*
当顶点v尚未加入生成树时,closedge[v]存放的是v与生成树中的顶点相连的最好情况:
v与生成树中的顶点的所有连边中权值最小的那条边;
closedge[v].adjvex存放的这条权值最小的边的另一个顶点,
closedge[v].lowcost存放的这条权值最小的边的权值。
如何区分生成树中的顶点和不在生成树中的顶点呢?
closedge[w].lowcost==0表示w已经加入生成树
*/
void prim(AGraphs G, int u) {
int i, j, k;
EdgeType closedge[MAX];
for (j = 0; j < G.vexnum; j++) {
closedge[j].adjvex = u; // 此次选中的与生成树中连接情况最好的点的位置
closedge[j].lowcost = G.arcs[u][j]; // 记录下来这个数值的大小
}
closedge[u].lowcost = 0; // 如果顶点已经包含在生成树中,则lowcost设为0。
for (i = 1; i < G.vexnum; i++) {
k = minclosedge(closedge);
printf("(%d,%d)\n", closedge[k].adjvex, k);
closedge[k].lowcost = 0;
for (j = 0; j < G.vexnum; j++) {
if (G.arcs[k][j] < closedge[j].lowcost) {
closedge[j].lowcost = G.arcs[k][j];
closedge[j].adjvex = k;
}
}
}
}
int minclosedge(EdgeType closedge[]) {
int min = MAXEDGE, j, k = -1;
for (j = 0; j < G.vexnum; j++) {
if (closedge[j].lowcost != 0 && closedge[j].lowcost < min) {
min = closedge[j].lowcost;
k = j;
}
}
return k;
}
// 时间复杂度:O(n^2) Prim算法适合于稠密图
画图求解:
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
Prim算法适合于稠密图
问法
最小生成树的求解过程
文字叙述 画表 求解过程
伪码描述(上述程序)
Kruskal 算法
先排序,对所有边按照权值升序排序
从小开始加,只要不产生回路,最后加到
n
−
1
n-1
n−1
需要 排序(适合于吸收图)
Kruscal算法适合稀疏图,时间复杂度为O(eloge),e为图的边数,因为该算法要对边按照权值排序,(堆、快速。归并)排序算法的平均时间复杂度O(eloge)
Kruskal算法是一种用于查找加权无向图的最小生成树(MST)的贪心算法。它通过逐步选择权值最小的边并确保不会形成环来构建最小生成树。下面详细描述Kruskal算法的实现过程:
Kruskal算法的原理和步骤
-
初始化:
- 创建一个包含所有图中顶点的集合,每个顶点初始时在不同的集合中。
- 初始化最小生成树为空。
-
排序:
- 将图中的所有边按权值从小到大排序。
-
逐步构建生成树:
- 依次检查排序后的每一条边,如果该边连接的两个顶点位于不同的集合中,则将这条边加入最小生成树,并合并这两个顶点所在的集合。
- 如果该边连接的两个顶点已经在同一集合中,则跳过这条边,以避免形成环。
-
终止条件:
- 当最小生成树包含的边数等于图中顶点数减一时,算法终止。
Kruskal算法的实现
为了实现Kruskal算法,需要使用**并查集(Disjoint Set Union, DSU)**数据结构来管理和合并顶点集合,并检查是否会形成环。
以下是Kruskal算法的详细实现步骤和代码示例:
数据结构
#define MAX 100
#define MAXEDGE 1000000
typedef struct {
int u, v; // 边的两个顶点
int weight; // 边的权值
} Edge;
typedef struct {
Edge edges[MAXEDGE]; // 图中的所有边
int vexnum, edgenum; // 顶点数和边数
} Graph;
int parent[MAX]; // 并查集数组
int rank[MAX]; // 并查集的秩数组(用于路径压缩优化)
并查集操作
// 查找操作,带路径压缩
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并操作,带按秩合并
void unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
Kruskal算法
// 边的比较函数,用于排序
int cmp(const void* a, const void* b) {
Edge* edgeA = (Edge*)a;
Edge* edgeB = (Edge*)b;
return edgeA->weight - edgeB->weight;
}
// Kruskal算法
void kruskal(Graph G) {
int i;
Edge result[MAX]; // 用于存储最小生成树中的边
int e = 0; // 最小生成树中的边数
// 初始化并查集
for (i = 0; i < G.vexnum; i++) {
parent[i] = i;
rank[i] = 0;
}
// 按边的权值排序
qsort(G.edges, G.edgenum, sizeof(Edge), cmp);
for (i = 0; i < G.edgenum; i++) {
Edge nextEdge = G.edges[i];
int x = find(nextEdge.u);
int y = find(nextEdge.v);
// 如果这条边不会形成环
if (x != y) {
result[e++] = nextEdge; // 将边加入结果中
unionSet(x, y); // 合并两个顶点的集合
}
}
// 打印最小生成树
printf("Following are the edges in the constructed MST:\n");
for (i = 0; i < e; i++) {
printf("%d -- %d == %d\n", result[i].u, result[i].v, result[i].weight);
}
}
- 数据结构:定义了图结构体和边结构体,用于存储图中的所有边。并查集用于管理顶点集合。
- 并查集操作:定义了并查集的查找和合并操作,用于判断是否会形成环。
- 排序:对所有边按权值进行排序。
- 构建最小生成树:依次检查每条边,使用并查集判断是否会形成环。如果不会,则将边加入最小生成树,并合并顶点集合。
- 输出结果:打印最小生成树中的所有边。
Kruskal算法通过边的排序和并查集的使用,实现了高效的最小生成树构建过程。该算法适用于稀疏图,因为它主要操作的是边而不是顶点。