这个作业属于哪个班级
这个作业的地址
这个作业的目标
学习图结构设计及相关算法
姓名
黄静
目录
图形结构属于复杂的非线性数据结构,在图形结构中,每个元素可以有零个或多个前驱元素或后继元素,也就是说元素之间的关系是多对多的。图G由两个集合V和E组成,其中V是顶点的有限集合,E是连接V中两个不同顶点的边的有限集合。记为G=(V,E);图按照边的有无方向可分为无向图和有向图
图的基本术语:
图的存储结构除了要存储图中各个顶点的信息以外,同时还要存储顶点与顶点之间的所有关系(边的信息)。常用的图的存储结构有邻接矩阵和邻接表。
图的邻接矩阵是一种采用邻接矩阵数组表示顶点之间相邻关系的存储结构。设G=(V,E)是含有n个顶点的图,则G的邻接矩阵数组是n阶方阵。使用二维数组edges[][]来保存两个顶点的信息,edges[i][j]表示从第i个顶点到第j个顶点的边信息。
其中,无向图为对称矩阵,有向图不一定为对称矩阵。在不带权值情况下,有连边赋值为1,无连边赋值为0;有权值情况下,有连边赋权值,无连边赋无穷大,本身顶点可赋0;
结构体定义
#define MAX 100
typedef struct
{
int edges[MAX][MAX];//邻接矩阵
int n, e;//n:顶点数,e:边数;
VertexType vexs[MAX];//存放顶点信息
}
也可采用二维指针数组,使用动态开辟空间
typedef struct
{
int **edges;//邻接矩阵
int n, e;//顶点数,边数
}MGraph;
建图函数
无向图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, j, a, b;
/*初始化*/
for (i = 1;i <= n;i++)
for (j = 1;j <= n;j++)
g.edges[i][j] = 0;
/*有边,赋值为1*/
for (i = 1;i <= e;i++)
{
cin >> a >> b;
//无向图,两顶点相互连边
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
g.n = n;
g.e = e;
}
有向图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, j, a, b;
/*初始化*/
for (i = 1;i <= n;i++)
for (j = 1;j <= n;j++)
g.edges[i][j] = 0;
/*有边,赋值为1*/
for (i = 1;i <= e;i++)
{
cin >> a >> b;
g.edges[a][b] = 1;
}
g.n = n;
g.e = e;
}
特点
图的邻接表是一种顺序与链式相结合的存储方法。对于含有n个结点的图,每个结点建立一个单链表,第i个单链表中的结点表示关联于顶点i的边(以顶点i为起点的边)也就是将顶点i的所有邻接点连接起来,其中每个节点表示一条边的信息。
每个单链表再设一个头节点,所有的头节点构成一个头节点数组adjlist,adjlist[i]表示顶点i的头节点,这样便可通过顶点i快速找到对应的单链表。
简而言之,即对图中每个节点i建立一个单链表,将所有顶点i的邻接点连接起来,且使用头节点数组adjlist将各条单链表也连接起来,形成邻接表。
结构体定义
typedef struct ANode //边结点;
{
int adjvex;//该边的终点编号;
struct ANode* nextarc;//指向下一条边的指针;
INfoType info;//保存该边的权值等信息;
}ArcNode;
typedef struct Vnode //头结点
{
int data;//顶点;
ArcNode* firstarc;//指向第一个邻接点;
}VNode;
typedef struct
{
VNode adjlist[MAX];//邻接表;
int n, e;//n:顶点数 e:边数
}AdjGraph;
建图函数
无向图
void CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p;
int i, j, a, b;
G = new AdjGraph;
/*邻接表初始化*/
for (i = 0;i <= n;i++)
{
G->adjlist[i].firstarc = NULL;
}
/*建立邻接表*/
for (i = 0;i < e;i++)
{
//无向图,a,b有边互连
cin >> a >> b;
p = new ArcNode;
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
}
G->n = n;G->e = e;
}
有向图
void CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p;
int i, j, a, b;
G = new AdjGraph;
/*邻接表初始化*/
for (i = 0;i <= n;i++)
{
G->adjlist[i].firstarc = NULL;
}
/*建立邻接表*/
for (i = 0;i < e;i++)
{
//有向图,仅一边连接
cin >> a >> b;
p = new ArcNode;
p->adjvex = b;
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
}
G->n = n;G->e = e;
}
特点
从给定图中任意指定的顶点出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历。在此,根据搜索方法的不同,介绍深度优先遍历DFS和广度优先遍历BFS两种遍历方法。
深度遍历:是从图中的一个顶点出发,每次遍历当前访问顶点的邻接点,一直到访问的顶点没有未被访问过的邻接点为止。然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的邻接点。访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有,以未访问的顶点为起始点,重复上述过程
深度遍历流程
从图中某个初始顶点v出发,首先访问初始顶点v
选择一个顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,依次重复1 2 两步遍历过程是一个递归的过程。
若没有下一个未被访问过的顶点,则按照路径回溯至前一个被访问过但仍有相邻点未被访问,继续访问其相邻点
若回溯至初始顶点v仍有未被访问的结点,说明此图不连通,继续以一个未被访问结点作为初始顶点,重复1 2 3步骤
直至所有顶点都已被访问
深度遍历代码
邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
int i, j;
static int flag = 0;
visited[v] = 1;//访问
//输出
if (flag == 0)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;//输出
}
for (i = 1;i <= g.n;i++)
{
/如果未访问过且二者相邻/
if (visited[i] == 0 && g.edges[i][v] == 1)
{
DFS(g, i);
}
}
}
邻接表
void DFS(AdjGraph* G, int v)//深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
适用问题
广度优先遍历:类似于树的层次遍历,从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
广度遍历流程
访问图的初始结点v
接着访问v的所有邻接点
再按照邻接点的访问顺序来访问每一个邻接点的未被访问过的邻接点
重复步骤,直至所有结点被访问
广度遍历代码
邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
int i, j;
static int flag = 0;
visited[v] = 1;//访问
//输出
if (flag == 0)
{
cout << v;
flag = 1;
}
else
{
cout << " " << v;//输出
}
for (i = 1;i <= g.n;i++)
{
/*如果未访问过且二者相邻*/
if (visited[i] == 0 && g.edges[i][v] == 1)
{
DFS(g, i);
}
}
}
邻接表
void DFS(AdjGraph* G, int v)//深度遍历
{
ArcNode* p;
p = G->adjlist[v].firstarc;
if (flag == 0)
{
cout << v;
flag = 1;
}
else
cout << " " << v;
visited[v] = 1; //标记已访问
while (p)
{
if (!visited[p->adjvex])//未被访问过
DFS(G, p->adjvex);
p = p->nextarc;
}
}
适用问题
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。
对于带权连通图G可能有多课不同树,每棵生成树的所有边的权值之和可能不同,其中权值之和最小的生成树称为图的最小生成树。
普里姆算法是一种构造性算法,用于构造最小生成树,过程如下
初始化U={v},以v到其他顶点的所有边为候选边
重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
➊从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
❷考察当前V-U中的所有顶点j,若(j, k)的权值小于原来和顶点k关联的候选边,修改候选边。
closest[]
:依附在U中顶点
lowcost[]
:候选边每个顶点到U最小边
时间复杂度:O(n^2)
适合邻接矩阵结构
实现代码
int Prim(MGraph* g)//最小生成树prim算法
{
int* lowcost;//边权重
int* clostest;//顶点编号
int min, i, j, k;
int cost = 0;
lowcost = new int[g->n + 1];
clostest = new int[g->n + 1];
/*初始化,给lowcost和clostest置初值*/
for (i = 1;i <= g->n;i++)
{
lowcost[i] = g->edges[1][i];
clostest[i] = 1;
}
lowcost[1] = 0;//从顶点1开始
for (i = 1;i <= g->n;i++)
{
min = INF;
/*寻找权值最小的点*/
for (j = 1;j <= g->n;j++)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;//k记录权值最小的编号
}
}
cost += lowcost[k];//计算花费
lowcost[k] = 0;//标记,访问过
/*修改数组lowcost和clostest*/
for (j = 1;j <= g->n;j++)
{
if (lowcost[j] != 0 && g->edges[k][j] < lowcost[j])
{
lowcost[j] = g->edges[k][j];
clostest[j] = k;
}
}
}
/*判断是否连通*/
for (i = 1;i <= g->n;i++)
{
if (lowcost[i] != 0) return -1;
}
return cost;
}
克鲁斯卡尔算法也是一种求带权无向图的最小生成树的构造性算法。按权值的递增次序选择合适的边来构造最小生成树的方法。
克鲁斯卡尔算法过程:
置U的初值等于V(即包含有G中的全部顶点)
将图G中的边按权值从小到大的顺序依次选取:
➊若选取的边未使生成树T形成回路,则选取;
❷否形成回路,则舍弃,直到所有顶点都连接在一起为止。
代码实现
typedef struct
{
int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
void Kruska1(Adj Graph* g)
{
inti, j, k, u1, v1, sn1, sn2;
UFSTree t[MAXSize]; //并查集,树结构
ArcNode* P;
Edge E[MAXSize];
k = 1; //e数组的下标从1开始计
for (i = 0;i < g.n;i++) //由g产生的边集E
{
p = g->adjlist[i].firstarc;
while (p != NULL)
{
E[k].u = i;E[k].v = p->adjvex;
E[k].w = p->weight;
k++;p = P->nextarc;
}
Heapsort(E, g.e);//采用堆排序对E数组按权值递增排序
MAKE_SET(t, g.n);//初始化并 查集树t
k = 1;//k表示当前构造生成树的第几条边,初值为1
j = 1;//E中边的下标,初值为1
while (k < g.n)//生成的边数小于n时循环
{
u1 = E[j].u;
v1 = E[j].v;//取一一条边的头尾顶点编号u1和v2
sn1 = FIND_SET(t, u1);
sn2 = FIND_SET(t, v1);//分别得到两个顶点所属的集合编号
if (sn1 != sn2)//两顶点属不同集合
{
printf(" (%d, %d) : %d\n", u1, v1, E[j].w);
k++;//生成边数增1
UNION(t, u1, v1); //将u1和v1两个顶点合并
}
j++;//下一条边
}
}
辅助数据结构是数组vest[],集合应用,用来判断该条边加入后是否会形成回路
该算法采用邻接表结构更合适
该算法适用于稀疏图
该算法时间复杂度:O(n^2)-->O(eloge)
在一个不带权图中,若从一个顶点到另一个顶点存在着一条路径,则称该路径长度为该路径所经过的边的数目。由于从一个顶点到另一个顶点可能存在多条路径,每条路径所经过的边数可能不同,即路径长度不同,所以,把路径长度最短的那条路径称为最短路径。
Dijkstra算法算是贪心思想实现的,首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。
Dijkstra算法流程
初始时只选取源点
从u中选取一个距离最近的点加入s
以选取的点为中心,查看所有未选取点的路径长度是否变化,若长度更短,则修改长度,称为路径调整
循环直至出现一条完整路径
代码实现
void Dijkstra(MGraph g, int v)
{
int dist[MAXV],path[MAXV];
int s[MAXV];
int mindis, i, j, u;
for (i = 0;i < g.n;i++)
{
dist[i] = g.edges[v][i];//距离初始化
s[i] = 0;//s[]置空
if (g.edges[v][i] < INF)//路径初始化
path[i] = v;//顶点v到i有边时
else
path[i] = -1;//顶点v到i没边时
}
s[v] = 1;//源点v放入S中
/找最小路径长度顶点u/
for (i = 0;i < g.n;i++)//循环n-1次
{
mindis = INF;
for (j = 0;j < g.n;j++)
if (s[j] == 0 && dist[j] < mindis)
{
u = j;
mindis = dist[j];
}
s[u] = 1;//顶点u加入S中
for (j = 0;j < g.n;j++) //修改不在s中的顶点的距离
if (s[j] == 0)
if (g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j])
{
dist[j] = dist[u] + g.edges[u][j];
path [j] = u;
}
}
Dispath(dist, path, s, g.n, v);//输出最短路径
}
数组dist[]:源点v到每个终点的最短路径长度
数组path[]:最短路径序列的前一顶点的序号,初值或无路径用-1表示
数组s[]:表示最短路径顶点集合
适合适用邻接矩阵存储
算法时间复杂度为O(n^2)
Floyd算法又称为插点法,是一种用于寻找给定的加权图中多源点之间最短路径的算法。可解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。**即遍历每个结点。然后以这个结点为中间结点来更新所有的结点==。
void Floyd(MatGraph g)//求每对顶点之间的最短路径
{
int A[MAXVEX][MAXVEX]; //建立A数组
int path[MAXVEX][MAXVEX]; //建立path数组
int i, j, k;
for (i = 0;i < g.n;i++)
for (j = 0;j < g.n;j++)
{
A[i][j] = g.edges[i][j];
if (i != j && g.edges[i][j] < INF)
path[i][j] = i; //i和j顶点之间有一条边时
else
path[i][i] = -1;//i和j顶点之间没有一条边时
}
for (k = 0;k < g.n;k++) // 求A[i][j]
{
for (i = 0;i < g.n;i++)
for (j = 0;j < g.n;j++)
if (A[i][j] > A[i][k] + A[k][i])//找到更短路径
{
A[i][j] = A[i][k] + A[k][j];//修改路径长度
path[i][j] = k;//修改经过顶点k
}
}
}
Floyd算法适用于APSP(All Pairs Shortest Paths,多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行V次SPFA算法。
设G=(V,E)是一个具有n个顶点的有向图,V中顶点序列v1,v2,···,v称为一个拓扑序列。
在一个有向图中找一个拓扑序列的过程称为拓扑排序。
如何进行拓扑排序?
从有向图中选取一-个没有前驱的顶点,并输出之
从有向图中删去此顶点以及所有以它为尾的弧
重复上述两步,直至图空,或者图不空但找不到无前驱的顶占为止
结构体设计
typedef struct //表头结点类型
{
Vertex data; //顶点信息
int count; //存放顶点入度
ArcNode* firstarc; //指向第一条弧
}VNode;
拓扑序列伪代码
遍历邻接表
计算每个顶点的入度,存入头结点count成员中;
遍历图顶点
找到一个入度为0的顶点,入栈 / 队列 / 数组;
while (栈不空)
{
出栈结点v,访问;
遍历v的所有邻接点
{
所有邻接点的入度 - 1;
若有邻接点入度为0,入栈st;
}
}
拓扑排序代码实现
void TopSort(AdjGraph* G)//拓扑排序算法
{
int i,j;
int St[MAXV],top = -1;//栈St的指针为top
ArcNode* p;
for (i = 0;i < G->n;i++) //入度置初值0
G->adjlist[i].count = 0;
for (i = 0;i < G->n;i++) //求所有顶点的入度
{
p = G->adjlist[i].firstarc;
while (p != NULL)
{
G->adjlist[p->adjvex].count++;
p = p->nextarc;
}
}
for (i = 0;i < G->n;i++) //将入度为0的顶点进栈
if (G->adjlist[i].count == 0)
{
top++;
St[top] = i;
}
while (top > -1)//栈不空循环
{
i = St[top];top--; //出栈一个顶点i
printf("%d ", i); //输出该顶点
p = G->adjlist[i].firstarc; //找第一个邻接点
while (p != NULL) //将顶点i的出边邻接点的入度减1
{
j = p->adjvex;
G->adjlist[j].count--;
if (G->adjlist[j].count == 0)//将入度为0的邻接点进栈
{
top++;
St[top] = j;
}
p = p->nextarc;
}
}
}
AOE网(Activity On Edge Network)是边表示活动的网,AOE网是带权有向无环图。边代表活动,顶点代表所有指向它的边所代表的活动 均已完成这一事件。由于整个工程只有一个起点和一个终点,网中只有一个入度为0的点(源点)和一个出度为0的点(汇点)。
用顶点表示事件,用有向边e表示活动,边的权表示活动时间,是一个带权的有向无环图。在AOE网中不应该出现有向环。
整个工程完成的时间为:从有向图的源点到汇点的最长路径,又叫关键路径
关键路径的边称为关键活动
关键词含义
AOE网一一带权的有向无环图
顶点--事件或状态
弧(有向边)---活动及发生的先后关系权---活动持续的时间
起点--入度为0的顶点(只有一一个)终点--出度为0的顶点( 只有一一个)
“六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论。这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够认识任何一个陌生人。”如图所示。
题目要求对每个节点计算符合“六度空间”理论的结点占结点总数的百分比,即计算每个结点中所连接的与该结点距离不超过6的结点数占结点总数的百分比。所以,最主要的就是计算每个节点所连接的距离不超过6的结点个数。
因为六度空间图可当作稀疏图来处理,所以我选择使用邻接表存储结构进行解题。又因为对每个结点距离为6以内结点进行计算,更适合使用BFS广度遍历,一层一层向外遍历,在遍历到第6层时跳出该节点的遍历,可得该距离内的结点数。
伪代码为思路总结,不是简单翻译代码。
使用邻接表存储结构,建图
for i = 1 to n 遍历每个结点
{
使用BFS遍历计算距离为6以内的结点个数BFS(G,i);
}
使用BFS遍历int BFS(AdjGraph* G, int v)
{
新建立一个队列q;
定义lastnode, node记录每一层的最后结点,visited[MAX]是否访问的标志,level记录层次,count记录结点个数;
对visited初始化为0
v结点入队q.push(v),改变visited[v]状态标记已访问;count数量++;
while 队列不为空
{
if 距离level超过6 break;
end if;
取队首元素,进入该节点单链表,遍历链表;
while p不为空
{
if 该节点未被访问
该节点入队,count++;
更新node结点;
end if;
继续遍历p = p->nextarc;
}
if 结点等于最后结点,一层遍历结束
层次增加level++;
最后结点更新为下一层最后一个lastnode = node;
end if;
}
}
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
该题求公路连通村庄所需要的最低成本,即建造最小生成树,并求最小生成树中权值的和。使用邻接矩阵更利于得到两点顶点之间的关系,因此我选择本题使用邻接矩阵和Prime算法来计算最小生成树所得最低成本。
邻接矩阵建图;
最小生成树Prime算法
int Prim(MGraph* g)
{
建立边权值lowcost,顶点编号clostest两个数组;
给lowcost和clostest初始化置初值;
从顶点1开始lowcost[1] = 0;
for i = 0 to n
{
将最小值min置初值INF;
if 顶点i未被访问过且二者最小边小于最小值min
最小值min等于该最小边;
k记录最小边对应点编号;
end if;
花费cost等于最小边结点lowcost相加;该节点置为已标记状态;
//修正数组lowcost和clostest
for i = 1 to n
{
if 未被访问且结点i与k权值小于二者最小边lowcost
修正lowcost等于edges[i][k];
end if;
}
}
//判断是否连通
for i = 1 to n
{
if lowcost[i] != 0
不连通 return-1;
end if;
}
连通 return cost;
}
g->edges = new int* [n + 1]
;lowcost
和clostest
两数组的含义与用处,不能混淆clostest
:记录最小生成树的边在U中的顶点编号lowcost
:顶点i到最小生成树中的点的边权重,取最小边权重加入最小生成树,在最小生成树中的点lowcost置为0lowcost
未完全置0,说明该图不流通手机扫一扫
移动阅读更方便
你可能感兴趣的文章