图
前言
图是计算机科学中非常常用的一类数据结构,有许许多多的计算问题都是用图来定义的。并且图涉及了许多方面的知识多,也就是说学好了图,基本等于理解了数据结构这门课的精神。
下图是我对于图这一章的总结的思维导图:
一、图是什么?
图(graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
1.图中的数据称为顶点,有别于线性表的元素,树中的结点;
2.在图结构中,不允许没有顶点,v为有穷非空;
3.在图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用 边 来表示;边集可以是空集。
二、图的定义
1.图的基本术语
无向边:若顶点v1和v2之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(v0,v1)来表示。
无向图:图中任意两个顶点之间的边都是无向边;
有向边:若顶点从v0到v1的边有方向,则称这条边为有向边,也成为弧,用有序偶<v0,v1>来表示,v0为弧尾,v1为弧头;
有向图:图中任意两个顶点之间的边都是有向边
简单图:在图中不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图
无向完全图:在无向图中,如何任意两个顶点之间都存在边,则称为无向完全图
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图
权:与图的边或弧相关的数叫做权
网:带权的图称为网
路径:图中顶点间存在路径,两个顶点存在路径则说明是连同的
环:如果路径最终回到起始点则称为环
简单路径:两个顶点间的连通不存在重复的路径
连通图:任意两个顶点都是连通的图
连通分量:无向图中的极大连通子图
强连通图:任意两个顶点都是连通的有向图
强连通分量:有向图中的极大连通子图称做有向图的强连通分量
连通图的生成树:一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
有向树:如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一颗有向树
有向图的生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若个棵不相交的有向树的弧。
2.图的存储结构
1.邻接矩阵存储方法
图的邻接矩阵存储方式是用两个数组来表示图;一个一维数组存储图中顶点信息,一个二维数组储存图中的边或弧的信息。
例如:
1.无向图
2.有向图:
3.有向网图:
图的邻接矩阵存储的结构代码如下:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
2.邻接表存储方法
当一个图为稀疏图时,使用n*n的空间存储其邻接表显然比较浪费空间。而当前这种存储方法采取了顺序存储和链式存储相结合的方式,大大减少了不必要的浪费。
邻接表法简单来讲,首先见一个顺序表,将所有的顶点存至其中(顶点表)。而对于每个顶点A来讲采用链表结点的方式进行存储,将next指针指向其相邻接点a1→a2→a3…,这个单链表称为该顶点的边表(对于有向图则称为出边表)。故在此种存储方式中存在两种结点,一种是顶点表结点,一种是边表结点。
1.无向图:
2.有向图:需要用到邻接表和逆邻接表
3.带权值的网图(在边表结点定义中增加一个weight的数据域)
图的结点定义的代码如下:
typedef char VertexType;
typedef int EdgeType;
typedef struct EdgeNode {
int adjvesx;
EdgeType weight;
struct EdgeNode* next;
}EdgeNode;
typedef struct VertexNode {
VertexType data;
EdgeNode* firstedge;
}VertexNode, AdjList[MAXVEX];
typedef struct {
AdjList adjList;
int numVertexes, numEdges;
}GraphAdjList;
3.其他存储方式
1.十字链表:它是邻接表和邻接矩阵的结合
定义顶点表结点结构:
重新定义边表结点结构
2.邻接多重表:它是无向图的另一种存储结构,宇十字链表类似。
仿照十字链表的方式,对边表结构进行改装,重新定义的边表结构如下
其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。
iLink指向依附顶点iVex的下一条边
jLink指向依附顶点jVex的下一条边。
也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。
三.图的遍历
1.深度优先遍历
他从图中的某个顶点v出发,访问此顶点,然后从v的未访问的邻接点再出发进行访问,直至所有的顶点全部被访问。
int visited[MAX] = { 0 };
void DFS(AdjGraph* G, int v) {
ArcNode* p;
visited[v] = 1;
cout << v;
p = G->adjlist[v].firstarc;
while (p != NULL) {
if (visited[p->adjvex] == 0)
DFS(G, p->adjvex);
p = p->nextarc;
}
}
2.广度优先遍历
看图理解:
1. 从任一点开始(这里是A),对应下图中的第一行只有A
2. A的下一个顶点有两个 B 、F ,对应第二行 A出 B、F进
3. B的下一个顶点有三个:C、I、G,对应第三行 B 出,C、I、G进
4. …以此类推,直到H出;
5. 综上得出遍历的顺序为:A、B、F、C、I、G、E、D、H
void BFS(AdjGraph* G, int v) {
int w, i;
ArcNode* p;
SqQueue* qu;
InitQueue(qu);
int visited[MAXV];
for (i = 0;i < G->n;i++) visited[i] = 0;
cout << v;
visited[v] = 1;
enQueue(qu, v);
while (!QueueEmpty(qu)) {
deQueue(qu, w);
p = G->adjlist[w].firstarc;
while (p != NULL) {
if (visited[p->adjvex] == 0) {
cout << p->adjvex;
visited[p->adjvex] = 1;
enQueue(qu, p->adjvex);
}
p = p->nextarc;
}
}
cout << endl;
}
四.图的应用
拓扑排序
很多问题都可转化为图, 利用图算法解决,例如早餐吃薄煎饼的过程,以动作为顶点,以先后次序为有向边。(有先后次序和依赖关系)
从工作流程图得到工作次序排列的算法,称为“拓扑排序”
拓扑排序处理一个有向无圈图(DAG), 输出顶点的线性序列:使得两个顶点v,w,如果G中有(v,w)边,在线性序列中v就出现在w之前。
拓扑排序广泛应用在依赖事件的排期上,还可以用在项目管理、 数据库查询优化和矩阵乘法的次序优化上。拓扑排序可以采用DFS很好地实现:
1.将工作流程建立为图,工作项是节点,依赖关系是有向边
2.工作流程图一定是个DAG图,否则有循环依赖
3.对DAG图调用DFS算法,以得到每个顶点的“结束时间”,按照每个顶点的“结束时间”从大到小排序输出这个次序下的顶点列表
五.最小生成树(代码过长就不附上了)
最小生成树的定义:构造联通网的最小代价生成树称为最小生成树。
Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息。
Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
1. 把图中的所有边按代价从小到大排序;
2. 把图中的n个顶点看成独立的n棵树组成的森林;
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
5.
六.最短路径(代码同上)
最短路径的定义:在一个带权图中,顶点V0到图中任意一个顶点Vi的一条路径所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径最短的那条路径称为最短路径。
Dijkstra算法
求单源最短路径,即求一个顶点到任意顶点的最短路径,其时间复杂度为O(n*n)
Floyd算法
求各顶点之间的最短路径,其时间复杂度为O(nnn)
七.关键路径
1.AOE网
若在带权的有向无环图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向无环图称为AOE网。记住AOE-网只是比AOV-网多了一个边的权重,而且AOV-网一般是设计一个庞大的工程各个子工程实施的先后顺序,而我们的AOE-网就是不仅仅关系整个工程中各个子工程的实施的先后顺序,同时也关系整个工程完成最短时间。
因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。
AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题:
1.完成整个工程需要的时间
2.哪些活动是影响工程进度的关键
2.求关键路径的步骤
输入顶点数和边数,已经各个弧的信息建立图
从源点v1出发,令ve[0]=0;按照拓扑序列往前求各个顶点的ve。如果得到的拓扑序列个数小于网的顶点数n,说明我们建立的图有环,无关键路径,直接结束程序
从终点vn出发,令vl[n-1]=ve[n-1],按逆拓扑序列,往后求其他顶点vl值
根据各个顶点的ve和vl求每个弧的e(i)和l(i),如果满足e(i)=l(i),说明是关键活动。
总结
以上就是今天要讲的内容,本文介绍了我对与图的重要的知识点的认识,在经过了几周的对图的学习之后,让我对于图这一章有了那么一点点的认识,明白了图在数据结构中的重要性。但是我认为我的学习还有着些许的不足,所以我认为我还应该要多多的去进行复习来巩固我所学习的知识。
疑难
7-3 公路村村通 (30 分)
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
输出格式:
输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。
#include <bitsdc++.h>
#define MAX 0x3f3f3f
using namespace std;
int Graph[10010][10010];
int M, N, minpri[10010];
int FindNextPt()
{
int nextpt = 0, min_ = MAX;
for(int i = 1; i <= N; ++i)
{
if(minpri[i] != 0 && minpri[i] < min_)
{
min_ = minpri[i];
nextpt = i;
}
}
return nextpt;
}
int prim()
{
int nextpt, cnt = 1, sum = 0;
for(int i = 1; i <= N; ++i)
minpri[i] = Graph[1][i];
minpri[1] = 0;
for(int i = 1; i < N; ++i)
{
nextpt = FindNextPt();
if(nextpt == 0) break;
sum += minpri[nextpt];
minpri[nextpt] = 0;
for(int j = 1; j <= N; ++j)
if(minpri[j] > Graph[nextpt][j])
minpri[j] = Graph[nextpt][j];
++cnt;
}
if(cnt != N)
return -1;
else
return sum;
}
int main()
{
int pos1, pos2, val, minpri;
scanf("%d %d", &N, &M);
for(int i = 1; i <= N; ++i)
for(int j = 1; j <= N; ++j)
Graph[i][j] = MAX;
for(int i = 1; i <= M; ++i)
{
scanf("%d %d %d", &pos1, &pos2, &val);
Graph[pos1][pos2] = Graph[pos2][pos1] = val;
}
minpri = prim();
printf("%d\n", minpri);
}