四.图论算法
文章目录
图的定义
由非空的的顶点集合和一个描述顶点之间的关系—边的集合。
图的相关术语:
① 顶点,边,弧,弧头,狐尾:数据元素vi被称为顶点,P(vi,vj)被称为顶点vi和vj之间的一条直线连接。无向图中称其为 边,有向图中称其为弧,有向图中不带箭头的一端被称为 弧尾,带箭头的一端被称为 弧头。
② 完全无向图:在无向图中,如果任意两顶点都有一条直接相连的边,则为 完全无向图,在一个含有n个顶点的完全无向图中,边的数量有 n(n-1)/2
。
③ 完全有向图:有向图中,如果任意两顶点都有方向互为相反的两条弧连接,则称为 完全有向图,在一个含有n个顶点的完全有向图中,边的数量有n(n-1)
。
④ 顶点的度,入度,出度:度是依附于某顶点的边数,表示为TD(v)。有向图中,指向该顶点的叫做 入度 ID(v),从顶点指出的叫做 出度 OD(v)。
⑤ 边的权,网图:与边有关的数据信息叫做 权重,边上带权的图称为网图或者网络。
⑥ 生成树:连通图G的生成树,指包含G的全部n个顶点的一个极小连通子图。
对于生成树,在其顶点间添加任意一条属于原图的边必定会产生回路,若生成树中减少一条边,则必然称为非连通的。
1.图的存储结构
(1).邻接矩阵
定义:
使用一维数组存储图中的顶点信息,用矩阵表示图中各顶点中的邻接关系。
矩阵表示:
A[i] [j]的值:
如果其值为1,则(vi,vj)之间有边
如果其值为0,则(vi,vj)之间无边
如果G是网图,则邻接矩阵可定义为:A[i] [j]的值:
如果其值为wij,则(vi,vj)之间有边,且其权重为wij。
如果其值为0或者∞,则(vi,vj)之间无边。
栗子:
邻接矩阵为:
邻接矩阵的特点:
①无向图的邻接矩阵一定是一个对称矩阵。所以只需要采用正三角或倒三角矩阵存储元素即可。
②对于无向图,邻接矩阵的第i行或第j列的非零元素个数正好是顶点vi的度。
③对于有向图,邻接矩阵d1第i行或者第j列非零元素的个数正好是vi的出度。
邻接矩阵的局限:
在确定图中有多少条边时,必须按行,按列对每个元素进行检索,花费时机代价很大。
抽象类型的设计:
#define MaxVertexNum 100
typedef struct
{
VertexType vexs[MaxVertexNum];//顶点表
EdgeType edges[MaxVertexNum] [MaxVertexNum];//边表
int n,e;//顶点数和边数的个数
}MGragh;
(2).邻接表
定义:
是图的一种顺序存储和链式存储相结合的存储方式。即为图中G的每个顶点vi,将所有邻接与该顶点的顶点成一个单链表,再将所有顶点的邻接表表头放到数组中,就构成了图的邻接表。
抽象类型的设计
#define MaxVerNum 100
//边表结点
typedef struct node
{
int adjvex;//存储邻接点
struct node *next;//指向下一个邻接点
int info;//边上信息(可没有)
}EdgeNode;
//顶点表结点
typedef struct vnode
{
VertexType vertex;//顶点域
EdgeNode * firstedge;//边表头指针
}VertexNode;
//创建一个存顶点的数组
typedef VertexNode AdjList[MaxVertexNum];
//图的结构
typedef struct
{
AdjList adjlist;//邻接表
int n,e;//顶点数量和边的数量
}ALGraph;
利用邻接表创建一个图:
//建立有向图的邻接表存储
void CreateALGraph(ALGraph * G){
int m,n;
EdgeNode * s;//邻接点表
scanf("%d %d",&(G->n),&(G->e));//输入图的顶点数和边数
for(int i = 0;i < G->n;i++){
scanf("%c",&(adjlist[i].vertex));//输入顶点的信息
G->adjlist[i].firstedge = NULL;
}
for(int k = 0;k < G->e;k++){
scanf("%d %d",&m,&n);//读入边vi,vj的顶点对应序号
s = (EdgeNode *)malloc(sizeof(EdgeNode));
s -> adjvex = n;
s->next = G->adjlist[m].firstedge;
G->adjlist[m].firstedge = s;
}
}
2.图的遍历算法
对上图来说:
DFS搜索:v1->v2->v4->v8->v5->v3->v6->v7
BFS搜索:v1->v2->v3->v4->v5->v6->v7->v8
(1).深度优先搜索DFS:
类比于数的先根遍历。访问到某个结点时,先对该结点进行需要的操作,再访问该结点的邻接表,进行对应结点的访问。
设置一个标志数组visited[n],其用来标记对应结点是否被访问过。
时间复杂度:当图中有e条边时,复杂度为O(e+n)
。
//主程序函数
#define TRUE 1
#define FALSE 0
void DFSTraversal(ALGragh *G){
//先对visited数组进行初始化
int visited[G->n];
for(int i = 0;i < n;i++){
visited[i] = FALSE;
}
//对图中每个结点进行DFS搜索
for(int i = 0;i < G->n;i++){
if(!visited[i]){
DFS(G,i);
}
}
}
//递归函数
//以vi为出发点对邻接表存储的图G进行DFS搜索
void DFS(ALGragh *G,int i){
EdgeNode *p;
//对当前顶点进行处理,不一定是打印操作。
printf("visit vertex:V %c\n",G->adjlist[i].vertex);
visited[i] = TRUE;//标记vi已被访问过
p = G->adjlist[i].firstedge;//取vi边表的头指针
while(p){
if(!visited[p->adjvex]){
DFS(G,p->adjvex);//如果表中元素未被访问,继续递归搜索
}
p = p->next;
}
}
(2).广度优先搜索BFS:
类比于数的层序遍历。
**先被访问的结点的邻接点 **先于 后被访问的结点的邻接点被访问。
//主程序函数
#define TRUE 1
#define FALSE 0
void BFSTraversal(Graph *G){
//初始化visited数组
int visited[n];
for(int i = 0;i < G->n;i++){
visited[i] = FALSE;
}
for(int i = 0;i < G->n;i++){
if(!visited[i]){
BFS(G,i);
}
}
}
//递归函数
//以vi为出发点,对邻接表存储的图G进行BFS搜索
void BFS(Graph *G,int k){
int *i ;
EdgeNode *p;
Queue *Q;
InitQueue(Q);
//对当前顶点进行相关操作,不一定是打印
printf("visit vertex:V %c\n",G->adjlist[k].vertex);
visited[k] = TRUE;//标记为已被搜索
EnQueue(Q,k);//顶点vk入队列
while(!Empty_Queue(Q)){
DeQueue(Q,i);//从队列中弹出一个元素,不断更新i的值
p = G->adjlist[i].firstedge;//获得当前顶点邻接表的头指针
while(!p){
if(!visited[p->adjvex]){
visited[p->adjvex] = TRUE;
//对当前顶点做处理,不一定是输出
printf("%c ",G->adjlist[p->adjvex].adjvex);
EnQueue(Q,p->adjvex);
}
p = p->next;
}
}
}
(3).DFS应用实例
Ⅰ.火力网问题
问题描述:有一个 n x n 的的方形城市,其中红色块是障碍物,白色块是空地,黑色圆圈表示炮台安放的位置。布防规则是 炮台可排放在空地上,但任意两个炮台若中间没有障碍物分隔就不能在同一行或同一列。
EXP:
解题思路:地图的设计可使用一个二维char数组存储。如果map[i] [j] = ‘X’,则该处为围墙,map[i] [j] = '.'表示该处为空地,而map[i] [j] = ‘o’,则表示该处为炮台。
放置炮台的条件:
①该处必须为空地。
②该处的同行同列不能有其他炮台,除非有间隔墙。
算法设计:
#include <stdio.h>
int n;//城市的尺寸是n x n的
char map[n] [n];//城市的地图
int count;//计数炮台的个数
//判断炮台是否能放
/*
@Param row:地图的第row+1行
@Param list:地图的第list+1列
@Return:如果返回一个1代表可以放置,反之,则不能放置
*/
int canPut(int row,int list){
//列扫描
for(int i = row-1;i>=0;i--){
//如果被扫描位置的上方有墙,直接跳出循环,再去判断行
if(map[i][list] == 'X'){
break;
}
//如果被扫描位置的上方存在炮台,直接判断不能摆放
if(map[i][list] == 'o'){
return 0;
}
}
//行扫描
for(int i = list-1;i>=0;i--){
//如果被扫描位置的左方有墙,直接跳出循环
if(map[row][i] == 'X'){
break;
}
//如果被扫描位置的左方有炮台,直接判断不能摆放
if(map[row][i] == 'o'){
return 0;
}
}
//当扫描行和列的左上方都没有炮台时,或者有墙阻隔时,返回1
return 1;
}
/*
@Param k:放置炮台的位置
@Param current:放置炮台的数目
*/
void dfs(int k,int current){
int x,y;//放置炮台的x,y坐标
if(k > n*n){//如果扫描到最后一个
if(current > count){
count = current;
}
return;
}
x = k/n;//争议点!!!
y = k%n;
//当某处是空地并且周围的条件满足时,放置炮台
if(map[x][y] == '.'&&canPut(x,y)){
map[x][y] = 'o';
//dfs操作,进入下一位置
dfs(k+1,current+1);
map[x][y] = '.';//回溯之后将地图复原。
}
dfs(k+1,current);//???
}
(4).最小生成树算法
栗子:
对于一个无向图G6:
其深度优先生成森林为:
最小生成树:如果一个无向连通图是一个网络,那么它的所有生成树中必有一棵边的权值总和最小的生成树。
Ⅰ.最小生成树的Prim算法
/*
Prim算法的理解:个人理解prim算法算作暴力生成生成树的一种,即每一次选择都选择对应结点最短的路径,合起来的路径即为最短。Prim以结点为单位选择路径。
*/
Ⅱ.最小生成树的Kruskal算法
/*
Kruskal算法的理解:个人理解Kruskal算法算作暴力生成生成树的一种,即每次选择全图边中最短的路径,合起来的路径即为最短。Kruskal以边为单位选择路径。
*/
(5).拓扑排序算法
其思想与步骤:
①.从AOV网中选择一个没有前驱的顶点(该顶点入度为0)并且输出它。
②.从网中删除该结点与其出发的所有有向边,且对该结点进行相关操作。
③.重复①②两步,直到剩余网中不再存在没有前驱的顶点为止。
个人理解,拓扑排序像是工厂流水线,必须完成某一步骤才能完成下一步。