DS博客作业04--图
| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业04--图 |
| 这个作业的目标 | 学习图结构设计及相关算法 |
| 姓名 | 雷正伟 |
0.PTA得分截图
1.本周学习总结
1.1 图的存储结构
1.1.1 邻接矩阵
-
无向图
-
邻接矩阵的结构体定义
#define MAXV <最大顶点个数>
typedef struct
{
int no;//顶点编号
InfoType info;//顶点其他信息
}VertexType;
typedef struct//图的定义
{
int edges[MAXV][MAXV];//邻接矩阵
int e, n;//顶点数和边数
VertexType vexs[MAXV];//存放顶点信息
}MatGraph;
- 建图函数
void CreateAdj(AdjGraph*& G, int A[MAXV][MAXV], int n, int e)
{
int i, j;ArcNode* p;
G = (AdjGraph*)malloc(sizeof(AdjGraph));
for (i = 0;i < n;i++)
g->adjlist[i].firstarc = NULL;
for (i = 0;i < n;i++)
for(j=n-1;j>=0;j--)
if (A[i][j] != 0 && A[i][j] != INF)
{
p = (ArcNode*)malloc(sizeof(ArcNode));
p->adjvex = j;
p->weight = A[i][j];
p->nextarc = G->adjlist[i].firstarc;
G->adjlist[i].firstarc = p;
}
G->n = n; G->e = e;
}
1.1.2 邻接表
-
有向图
-
邻接表的结构体定义
#define MAXV <最大顶点个数>
typedef struct ANode
{
int adjvex;//该边的邻接点编号
struct ANode* nextarc;//指向下一条边的指针
int weight;//该边的相关信息,如权值(整型表示)
}ArcNode;//边结点的类型
typedef struct Vnode
{
InfoType info;//顶点的其他信息
ArcNode* firstarc;//指向第一个边结点
}VNode;//邻接表的头结点类型
typedef struct
{
VNode adjlist[MAXV];//邻接表的头结点数组
int n, e;//图中的顶点数n和边数e
}AdjGraph;//完整的图邻接表类型
- 建图函数
void CreateAdj(AdjGraph*& G, int n, int e)
{
int i, j, a, b;
ArcNode* p;
G = new AdjGraph;
for (i = 0;i < n;i++)
G->adjlist[i].firstarc = NULL;
for (i = 1;i <= e;i++)//根据输入边建图//生成(a,b)
{
cin >> a >> b;
p = new ArcNode;
p->adjvex = b;
p->nextarc = NULL;//头插法插入结点p
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc = p;
}
G->n = n; G->e = e;
}
1.1.3 邻接矩阵和邻接表表示图的区别
当图为稀疏图、顶点较多,即图结构比较大时,更适宜选择邻接表作为存储结构。当图为稠密图、顶点较少时,或者不需要记录图中边的权值时,使用邻接矩阵作为存储结构较为合适。使用邻接矩阵的时间复杂度为O(n^2),使用邻接表的时间复杂度为O(n+e)。
1.2 图遍历
1.2.1 深度优先遍历
- 调用DFS算法,假设初始点v=1,调用DFS(G,1)的执行过程如下
1. DFS(G,1):访问顶点1,找顶点1的相邻顶点5,它未被访问过,转2.
2. DFS(G,5):访问顶点5,找顶点5的相邻顶点4,它未被访问过,转3.
3. DFS(G,4):访问顶点4,找顶点4的相邻顶点2,它未被访问过,转4.
4. DFS(G,2):访问顶点2,找顶点2的相邻顶点3,他未被访问过,转5.
5. DFS(G,3):访问顶点3,找顶点3的相邻顶点,所有相邻顶点均已被访问,退出DFS(G,3),转6.
6. 继续DFS(G,2):顶点2的所有后继相邻顶点均已被访问,退出DFS(G,2),转7.
7. 继续DFS(G,4):顶点4的所有后继相邻顶点均已被访问,退出DFS(G,4),转8.
8. 继续DFS(G,5):顶点5的所有后继相邻顶点均已被访问,退出DFS(G,5),转9.
9. 继续DFS(G,1):顶点1的所有后继相邻顶点均已被访问,退出DFS(G,1),转10.
10. 结束
11. 从顶点1出发的深度优先访问序列是1 5 4 2 3 。
- 深度优先遍历代码
// 深度优先遍历——DFS遍历
void DFSTraverse(MGraph G)
{
int v;
for (v = 0; v < G.vexnum; v++)
visited[v] = FALSE;
for (v = 0; v < G.vexnum; v++)
if (!visited[v])
{
DFS(G, v);
}
}
void DFS(MGraph G, int v)
{
printf("%c ", G.Vex[v]); // 访问顶点v
visited[v] = TRUE; // 设已访问标记
int w;
for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
{
if (!visited[w]) {
DFS(G, w);
}
}
}
- 深度遍历适用哪些问题的求解
1. 判断顶点u到v是否有简单路径
2. 输出简单路径
3. 输出所有路径
4. 输出一些简单回路
5. 输出通过一个节点的所有简单回路
1.2.2 广度优先遍历
- 调用BFS算法,假设初始点v=1,调用BFS(G,1)的执行过程如下
1. 访问顶点1,1进队,转2.
2. 第一次循环:顶点1出队,找其第一个相邻顶点2,它未被访问过,访问之并将2进队;找顶点1的下一个相邻顶点5,它未被访问过,访问之并将5进队,转3.
3. 第二次循环:顶点2出队,找其第一个相邻顶点3,它未被访问过,访问之并将3进队;找顶点2的下一个相邻顶点4,它未被访问过,访问之并将4进队;找顶点2的下一个相邻顶点5,它被访问过,转4.
4. 第三次循环:顶点5出队,依次找其相邻顶点1,2,4,均已被访问过,转5.
5. 第四次循环:顶点3出队,依次找其相邻顶点2,4,均已被访问过,转6.
6. 第五次循环:顶点4出队,依次找其相邻顶点2,3,5,均已被访问过,转7.
7. 此时队列为空,遍历结束,即从顶点1出发的广度优先访问序列是1 2 5 3 4 。
- 广度优先遍历代码
// 广度优先遍历——BFS算法
void BFSTraverse(MGraph G)
{
int i;
for (i = 0; i < G.vexnum; ++i)
visited[i] = FALSE; // 访问标记数组初始化
InitQueue(Q); // 初始化辅助队列
for (i = 0; i < G.vexnum; ++i) // 从0号顶点开始遍历
if (!visited[i]) // 对每个连通分量调用一次BFS
BFS(G, i); // vi为访问过,从vi开始访问BFS
}
void BFS(MGraph G, int v)
{
printf("%c ", G.Vex[v]); // 访问初始顶点v
visited[v] = TRUE; // 对v做已访问标记
EnQueue(Q, v);
int w;
while (!IsEmpty(Q))
{
DeQueue(Q, v);
for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
{
if (!visited[w])
{
printf("%c ", G.Vex[w]);
visited[w] = TRUE;
EnQueue(Q, w);
}
}
}
}
- 广度遍历适用哪些问题的求解
1. 最短路径
2. 最远顶点
1.3 最小生成树
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
下面有一个带权图:
它的最小生成树是什么样子呢?下图绿色加粗的边可以把所有顶点连接起来,又保证了边的权值之和最小:
1.3.1 Prim算法求最小生成树
-
基于上述图结构求Kruskal算法生成的最小生成树的边序列为U={0,2,4,1,3}
-
实现Prim算法的2个辅助数组是closest[],lowcost[]。其中closest[j]保存U中的这个顶点,lowcost[j]存储最小的边的权。
-
Prim算法代码
void Prim(MatGraph g, int v)
{
int lowcost[MAXV];
int MIN;
int closest[MAXV], i, j, k;
for (i = 0;i < g.n;i++)//给lowcost[]和closest[]置初值
{
lowcost[i] = g.edges[v][i];
closest[i] = v;
}
for (i = 1;i < g.n;i++)//找出(n-1)个顶点
{
MIN = INF;
for(j=0;j<g.n;j++)//在(V-U)中找出离U最近的顶点k
if (lowcost[j] != 0 && lowcost[j] < MIN)
{
MIN = lowcost[j];
k = j;//k记录最近的顶点的编号
}
printf("边(%d,%d)权为:%d\n", closest[k], k, MIN);//输出最小生成树的一条边
lowcost[k] = 0;//标记k已经加入U
for(j=0;j<g.n;j++)//对(V-U)中的顶点j进行调整
if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];
closest[j] = k;//修改数组lowcost和closest
}
}
}
- 时间复杂度:O(n^2);Prim算法特别适合用稠密图求最小生成树,因为其算法的执行时间与图中的边数e无关。
1.3.2 Kruskal算法求解最小生成树
-
基于上述图结构求Kruskal算法生成的最小生成树的边序列为T={2,4,0,1,3}
-
实现Kruskal算法的辅助数据结构是vset[]。vset[]用于判断顶点i,j是否属于同一个连通分量。
-
Kruskal算法代码
typedef struct
{
int u;//边的起始顶点
int v;//边的终止顶点
int w;//边的权值
}Edge;
void Kruskal(MatGraph g)//Kruskal算法
{
int i, j, u1, v1, sn1, sn2, k;
int vest[MAXV];
Edge E[MaxSize];//存放图中的所有边
k = 0;//e数组的下标从0开始计
for(i=0;i<g.n;i++)//由g产生边集E,不重复选取同一条边
for(j=0;j<=i;j++)
if (g.edges[i][j] != 0 && g.edges[i][j] != INF)
{
E[k].u = i; E[k].v = j;
E[k].w = g.edgess[i][j];
k++;
}
InsertSort(E, g, e);//采用直接插入排序对E数组按权值递增排序
for (i = 0;i < g.n;i++)//初始化辅助数组
vest[i] = i;
k = 1; j = 0;//k表示当前构造生成树的第几条边,初值为1;E中边的下标,初值为0
while (k < g.n)//生成的边数小于n时循环
{
u1 = E[j].u; v1 = E[j].v;//取一条边的两个顶点
sn1 = vest[u1];
sn2 = vest[v1];//分别得到两个顶点所属的集合编号
if (sn1 != sn2)//两个顶点属于不同的集合,该边是最小生成树的一条边
{
printf("(%d,%d):%d\n", u1, v1, E[j].w);//输出最小生成树的一条边
k++;//生成边数增1
for (i = 0;i < g.n;i++)//两个集合统一编号
if (vest[i] == sn2)//集合编号为sn2的改为sn1
vest[i] = sn1;
}
j++;//扫面下一条边
}
}
- 时间复杂度:O(elog2e);Prim算法特别适合用稀疏图求最小生成树,因为Kruskal算法的执行时间仅与图中的边数有关,与顶点个数无关。
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
-
Dijkstra算法需要path[],dist[]。其中path[j]存放源点v->j的最短路径上顶点j的前一个顶点编号,其中源点v是默认的;dist[j]表示源点v->j得到最短路径长度,其中源点v是默认的。
-
Dijkstra算法代码
void Dijkstra(MatGraph g, int v)//Dijkstra算法
{
int dist[MAXV], path[MAXV];
int S[MAXV];//S[i]=1表示顶点i在S中,S[i]=0表示顶点i在U中
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有边时,置顶点i的前一个顶点为v
else path[i] = -1;//顶点v到顶点i无边时,置顶点i的前一个顶点为-1
}
S[v] = 1; path[v] = 0;//源点编号v放入S中
for (i = 0;i < g.n - 1;i++)//循环直到所有顶点的最短路径都求出
{
MINdis = INF;//MINdis置最大长度初值
for(j=0;j<g.n;j++)//选取不在S中(即在U中)且具有最小最短路径长度的顶点u
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中(即在U中)的顶点的最短路径
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(g, dist, path, S, v);//输出最短路径
}
void Dispath(MatGraph g, int dist[], int path[], int S[], int v)
{//输出从顶点v出发的所有最短路径
int i, j, k;
int apath[MAXV], d;//存放一条最短路径(逆向)及其顶点个数
for(i=0;i<g.n;i++)//循环输出从顶点v到i的路径
if (S[i] == 1 && i != v)
{
printf("从顶点%d到顶点%d的路径长度为:%d\t路径为:", v, i, dist[i]);
d = 0; apath[d] = i;//添加路径上的终点
k = path[i];
if (k == -1)//没有路径的情况
printf("无路径\n");
else//存在路径时输出该路径
{
while (k != v)
{
d++; apath[d] = k;
k = path[k];
}
d++; apath[d] = v;//添加路径上的起点
printf("%d", apath[d]);//先输出起点
for (j = d - 1;j >= 0;j--)//在输出其他顶点
printf(",%d", apath[j]);
printf("\n");
}
}
}
- 时间复杂度为O(n^2),Dijkstra适用于有向无环图。
1.4.2 Floyd算法求解最短路径
- Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似,用于解决多源最短路径、无负权回路即可、边权可正可负、运行一次算法即可求得任意两点间最短路。
- Floyd算法需要二维数组path[][]保存最短路径,同时它与当前迭代的次数有关。path[i][j]存放着考查顶点0、1、...、k之后得到的i->j的最短路径中顶点j的前一个顶点编号。
- Floyd算法优势:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单,时间复杂度为O(n^2)。
1.5 拓扑排序
- 从有向无环图图中选择一个没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复1. 和2. 直到当前的有向无环图为空或当前图中不存在无前驱的顶点为止,后一种情况说明有向图中必然存在环。
- 所以得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。
(一个有向无环图可以有一个或多个拓扑排序序列)
- 实现拓扑排序代码,结构体类型定义
typedef struct
{
Vertex data;//顶点信息
int count;//增加数据域:存放顶点入度
ArcNode* firstarc;//指向第一个邻接点
}VNode;//头结点类型
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;//找到下一个邻接点
}
}
}
- 拓扑排序伪代码
栈S初始化;
累加器count初始化;
扫描顶点表,将没有前驱(即入度为0)的顶点进栈;
当栈S非空时循环
{
v = 退出栈顶元素;输出v;
累加器加1;
将顶点v的各个邻接点的入度减1;
将新的入度为0的顶点入栈;
}
if (count < vertexNum)
输出有回路信息;
- 从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它,再从图中删去该顶点,并且删去从该顶点发出的全部有向边,重复此操作直到剩余的图中不再存在没有前驱的顶点,通过此方法即可删除入度为0的结点。
- 对于无环的有向图,对其进行拓扑排序可输出其所有顶点,而有环的图则无法输出其所有顶点。
1.6 关键路径
- AOE网
有向图中,用顶点表示活动,用有向边表示活动之间开始的先后顺序,则称这种有向图为AOV(Activity On Vertex)网络;AOV网络可以反应任务完成的先后顺序(拓扑排序)。 - 关键路径和关键活动
在AOE网中,所有活动都完成才能到达终点,因此完成整个工程所必须花费的时间(即最短工期)应该为源点到终点的最大路径长度。具有最大路径长度的路径称为关键路径,关键路径上的活动称为关键活动。
2.PTA实验作业
2.1 六度空间
2.1.1 伪代码
定义结点数n和边数e;
定义顶点访问标记数组visited[MAXV];
定义一个充当邻接矩阵的二维数组edgex[MAXV][MAXV];
int main()
输入n和e
for i=1 to e do
输入边的关系;
将edgex[][]对应的点的值改为1;
end for
for i=1 to n do
初始化visited数组 ;
调用BFS函数,返回距离不超过5的结点数;
计算并输出百分比;
end for
int BFS(int v)
定义一个队列qu;
将v入队;
visited[v]=1;
while 队不空且距离小于6 do
取队首做临时调用点temp;
循环遍历与该结点相连接的点
if 结点未遍历 then
结点数++;
入队;
visited[i]=1;
记录位置tail=i;
end if
if temp==last then
记录当前层数的最后一个元素的位置 ;
结点层数加一;
end if
end while
return count;
2.1.2 提交列表
2.1.3 本题知识点
- 创建图函数运用了头插法
- 运用BFS遍历(引入队列)
- 不仅要用广度遍历BFS,还要和递归相结合,分层运算。
2.2 村村通
2.2.1 伪代码
int main()
{
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
{
用二维数组代表邻接矩阵,并进行初始化。
}
for (i = 1; i <= e; i++)
{
给邻接矩阵赋值
}
调用普里姆算法;
}
void Prim()
{
for (i = 1; i <= n; i++)
{
给lowcost[]和closest[]置初值;
}
for (i = 1; i <n; i++)
{
给min赋初值(表示无穷);
for (j = 1; j <= n; j++)
{
在(V-U)中找出离U最近的顶点k
k记录最近顶点的编号
}
for (j = 1; j <= n; j++)
{
对(V-U)中的顶点j 进行调整;
修改数组lowcost[]和closest[];
}
}
输出num;
}
2.2.2 提交列表
2.2.3 本题知识点
- 运用二维数组代表邻接矩阵
- 采用Prim()算法