从零开始的数据结构与算法(C)(4)

四.图论算法


图的定义

由非空的的顶点集合和一个描述顶点之间的关系—边的集合。

图的相关术语

顶点,边,弧,弧头,狐尾:数据元素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)并且输出它。

②.从网中删除该结点与其出发的所有有向边,且对该结点进行相关操作。

③.重复①②两步,直到剩余网中不再存在没有前驱的顶点为止。

个人理解,拓扑排序像是工厂流水线,必须完成某一步骤才能完成下一步。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值