这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 付峻霖 |
0.PTA得分截图
1.本周学习总结
1.1 图的存储结构
1.1.1 邻接矩阵(不用PPT上的图)
- 邻接矩阵的结构体定义
typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
typedef struct
{
VertexType vexs[MAXVEX]; /* 顶点表 */
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵,可看作边表 */
int numNodes, numEdges; /* 图中当前的顶点数和边数 */
}MGraph;
- 建图函数
/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:
");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
scanf(&G->vexs[i]);
for(i = 0;i <G->numNodes;i++)
for(j = 0;j <G->numNodes;j++)
G->arc[i][j]=INFINITY; /* 邻接矩阵初始化 */
for(k = 0;k <G->numEdges;k++) /* 读入numEdges条边,建立邻接矩阵 */
{
printf("输入边(vi,vj)上的下标i,下标j和权w:
");
scanf("%d,%d,%d",&i,&j,&w); /* 输入边(vi,vj)上的权w */
G->arc[i][j]=w;
G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
}
}
1.1.2 邻接表
- 邻接表的结构体定义
typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
EdgeType info; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode* next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
VertexType data; /* 顶点域,存储顶点信息 */
EdgeNode* firstedge; /* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numNodes, numEdges;/* 图中当前顶点数和边数 */
}GraphAdjList;
- 建图函数
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:
");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i < G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
{
scanf(&G->adjList[i].data); /* 输入顶点信息 */
G->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(k = 0;k < G->numEdges;k++) /* 建立边表 */
{
printf("输入边(vi,vj)上的顶点序号:
");
scanf("%d,%d",&i,&j); /* 输入边(vi,vj)上的顶点序号 */
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex=j; /* 邻接序号为j */
e->next=G->adjList[i].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex=i; /* 邻接序号为i */
e->next=G->adjList[j].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[j].firstedge=e; /* 将当前顶点的指针指向e */
}
}
1.1.3 邻接矩阵和邻接表表示图的区别
- 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
- 邻接矩阵的时间复杂度为0(n2),而邻接表的时间复杂度为0(n+e)。
- 在邻接表上容易找到任意一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi,vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,还不及邻接矩阵方便。
- 邻接矩阵多用于稠密图的存储(e接近n(n - 1)/2),而邻接表多用于稀疏图的存储(e<<n2)。邻接矩阵是不错的图存储结构,但是,对于边数相对于顶点数量比较少的图来说,会造成集大的资源浪费,而邻接表就解决了这个问题
1.2 图遍历
1.2.1 深度优先遍历
-
选上述的图,继续介绍深度优先遍历结果
(1)从v0点开始深度遍历,遍历相邻且编号最小且未被访问的顶点v1
(2)在v1继续遍历与v1相邻的点且编号最小且未被访问的顶点
(3)若相邻顶点均已经被访问了,就回溯到上一个点继续找,直到所有顶点都遍历到为止
(4)最终深度优先遍历结果为:v0,v1,v2,v4,v3,v6,v7,v5,v8
-
邻接矩阵深度遍历代码
void DFSTraverse(MGraph* G)
{
int i = 0;
/* 把每一个定点都设为未访问过 */
for(i = 0; i < G->numVertexes; i++)
{
visited[i] = 0;
}
/* 对未访问过的定点调用DFS */
for(i = 0; i < G->numVertexes; i++)
{
if(visited[i] == 0)
{
DFS(G,i);
}
}
}
- 深度遍历适用哪些问题的求解。
图的深度遍历可以找到两点之间的全部路径,因此可以找到迷宫问题的全部可能答案
1.2.2 广度优先遍历
- 选上述的图,继续介绍广度优先遍历结果
(1)广度优先遍历(Depth First Search)的主要思想是:类似于树的层序遍历。
(2)从v0开始,有2个邻接点,“v1,v2”,这是第二层;
(3)在分别从v1,v2开始找他们的邻接点,为第三层。以此类推。
(4)最终广度优先遍历结果为:v0,v1,v2,v3,v4,v5,v6,v7,v8
- 邻接矩阵广度遍历代码
/* 邻接矩阵的广度优先遍历 */
void BFSTraverse(MGraph* G)
{
int i = 0, j = 0;
queue<int> myqueue;
/* 初始化,把每一个定点都设为未访问过 */
for(i = 0; i < G->numVertexes; i++)
{
visited[i] = 0;
}
/* 对每一个定点做循环 */
for(i = 0; i < G->numVertexes; i++)
{
/* 如果节点没有被访问过 */
if(visited[i] == 0)
{
/* 该节点设置为已经被访问 */
visited[i] = 1;
/* 打印出该节点,并把该节点入队列 */
cout << G->vexs[i] << " ";
myqueue.push(i);
/* 若当前的队列不为空 */
while(!myqueue.empty())
{
i = myqueue.front();
myqueue.pop();
for(j = 0; j < G->numVertexes; j++)
{
/* 判断其他定点若与当前的定点存在边且未访问过 */
if(G->arc[i][j] != 65536 && visited[j] == 0)
{
visited[j] = 1;
cout << G->vexs[j] << " ";
myqueue.push(j);
}
}
}
}
}
}
- 广度遍历适用哪些问题的求解。
通常用于求解无向图的最短路径问题
1.3 最小生成树
最小生成树本身是一棵生成树,所以需要时刻满足以下两点:
1.生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路;
2.对于具有 n 个顶点的连通网,其生成树中只能有 n-1 条边,这 n-1 条边连通着 n 个顶点。
简单来说,最小生成树类似于网络设计,如何设计网线,网络使得其能覆盖到整片区域,并且成本最低。
1.3.1 Prim算法求最小生成树
-
Prim算法大致思路是
(1)设图G顶点集合为U,首先任意选择图G中的一点作为起始点a,将该点加入集合V。
(2)再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小,此时将b点也加入集合V;
(3)以此类推,现在的集合V={a,b},再从集合U-V中找到另一点c使得点c到V中任意一点的权值最小,此时将c点加入集合V,直至所有顶点全部被加入V,此时就构建出了一颗最小生成树。
(4)因为有N个顶点,所以该最小生成树就有N-1条边,每一次向集合V中加入一个点,就意味着找到一条最小生成树的边。 -
Prim算法的两个辅助数组
lowcost[i]数组:表示以i为终点的边的最小权值,当lowcost[i]=0说明以i为终点的边的最小权值=0,也就是表示i点加入了MST
mst[i]数组:表示对应lowcost[i]的起点,即说明边<mst[i],i>是MST的一条边,当mst[i]=0表示起点i加入MST -
模拟一下Prim算法
我们假设v0是起始点
{(v0,v1)}
{(v0,v1),(v1,v2)}
{(v0,v1),(v1,v2),(v2,v4)}
{(v0,v1),(v1,v2),(v2,v4),(v4,v3)}
{(v0,v1),(v1,v2),(v2,v4),(v4,v3),(v4,v5)}
{(v0,v1),(v1,v2),(v2,v4),(v4,v3),(v4,v5),(v3,v6)}
{(v0,v1),(v1,v2),(v2,v4),(v4,v3),(v4,v5),(v3,v6),(v6,v7)}
{(v0,v1),(v1,v2),(v2,v4),(v4,v3),(v4,v5),(v3,v6),(v6,v7),(v7,v8)}
展开后得到的二叉树则为Prim算法生成的最小生成树
- 根据上面的过程,可以容易的写出具体实现代码如下
#include<iostream>
#include<fstream>
using namespace std;
#define MAX 100
#define MAXCOST 0x7fffffff
int graph[MAX][MAX];
int prim(int graph[][MAX], int n)
{
int lowcost[MAX];
int mst[MAX];
int i, j, min, minid, sum = 0;
for (i = 0; i < n; i++)
{
lowcost[i] = graph[0][i];
mst[i] = 0;
}
mst[1] = 0;
for (i = 0; i < n; i++)
{
min = MAXCOST;
minid = 0;
for (j = 0; j < n; j++)
{
if (lowcost[j] < min && lowcost[j] != 0)
{
min = lowcost[j];
minid = j;
}
}
if(!(mst[minid]==1&& minid==0))
cout << "V" << mst[minid] << "-V" << minid << "=" << min << endl;
sum += min;
lowcost[minid] = 0;
for (j = 0; j < n; j++)
{
if (graph[minid][j] < lowcost[j])
{
lowcost[j] = graph[minid][j];
mst[j] = minid;
}
}
}
return sum;
}
int main()
{
int i, j, k, m, n;
int x, y, cost;
cin >> m >> n;//m=顶点的个数,n=边的个数
//初始化图G
for (i = 0; i < m; i++)
{
for (j = 0; j < m; j++)
{
graph[i][j] = MAXCOST;
}
}
//构建图G
for (k = 0; k < n; k++)
{
cin >> i >> j >> cost;
graph[i][j] = cost;
graph[j][i] = cost;
}
//求解最小生成树
cost = prim(graph, m);
//输出最小权值和
cout << "最小权值和=" << cost << endl;
system("pause");
return 0;
}
- Prim算法代码
/* Prim算法生成最小生成树 */
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /* 保存相关顶点下标 */
int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
lowcost[0] = 0; /* 初始化第一个权值为0,即v0加入生成树 */
/* lowcost的值为0,在这里就是此下标的顶点已经加入生成树 */
adjvex[0] = 0; /* 初始化第一个顶点下标为0 */
for(i = 1; i < G.numVertexes; i++) /* 循环除下标为0外的全部顶点 */
{
lowcost[i] = G.arc[0][i]; /* 将v0顶点与之有边的权值存入数组 */
adjvex[i] = 0; /* 初始化都为v0的下标 */
}
for(i = 1; i < G.numVertexes; i++)
{
min = GRAPH_INFINITY; /* 初始化最小权值为∞, */
/* 通常设置为不可能的大数字如32767、65535等 */
j = 1;k = 0;
while(j < G.numVertexes) /* 循环全部顶点 */
{
if(lowcost[j]!=0 && lowcost[j] < min)/* 如果权值不为0且权值小于min */
{
min = lowcost[j];/* 则让当前权值成为最小值 */
k = j; /* 将当前最小值的下标存入k */
}
j++;
}
printf("(%d, %d)
", adjvex[k], k); /* 打印当前顶点边中权值最小的边 */
lowcost[k] = 0; /* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */
for(j = 1; j < G.numVertexes; j++) /* 循环所有顶点 */
{
if(lowcost[j]!=0 && G.arc[k][j] < lowcost[j])
{/* 如果下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
lowcost[j] = G.arc[k][j]; /* 将较小的权值存入lowcost相应位置 */
adjvex[j] = k; /* 将下标为k的顶点存入adjvex */
}
}
}
}
- 分析Prim算法时间复杂度,适用什么图结构,为什么?
Prim算法中有两重for循环,所以时间复杂度为O(n^2),其中n为图的顶点个数。
由于Prim算法的执行时间与顶点数有关,与图中的边数e无关,所以更适合与解决边的绸密度更高的连通网。
1.3.2 Kruskal算法求解最小生成树
- Kruskal算法描述
Kruskal算法是基于贪心的思想得到的。首先我们把所有的边按照权值先从小到大排列,接着按照顺序选取每条边,如果这条边的两个端点不属于同一集合,那么就将它们合并,直到所有的点都属于同一个集合为止。至于怎么合并到一个集合,那么这里我们就可以用到一个工具——-并查集。换而言之,Kruskal算法就是基于并查集的贪心算法。 - Kruskal算法思路
(1)邻接链表按权值排序后
(2)依次选边,若成环则跳过,否则加入最小生成树并计数。
这里判断是否成环用的是并查集:如果新加入的边两个端点在同一个集合中,就说明已经有一条路径联通这两个端点。
(3)重复(2),直到加入了n-1条边或遍历完成(无最小生成树)。 - 模拟一下Kruskal算法
始终找最小边,且不能形成环
{(v0,v1)}
{(v0,v1),(v2,v4)}
{(v0,v1),(v2,v4),(v3,v4)}
{(v0,v1),(v2,v4),(v3,v4),(v6,v7)}
{(v0,v1),(v2,v4),(v3,v4),(v6,v7),(v1,v2)}
{(v0,v1),(v2,v4),(v3,v4),(v6,v7),(v1,v2),(v4,v5)}
{(v0,v1),(v2,v4),(v3,v4),(v6,v7),(v1,v2),(v4,v5),(v3,v6)}
{(v0,v1),(v2,v4),(v3,v4),(v6,v7),(v1,v2),(v4,v5),(v3,v6),(v7,v8)}
展开后得到的二叉树则为Kruskal算法生成的最小生成树
实现Kruskal算法的辅助数据结构是邻接表,收集边时需要遍历图,使用邻接表可以更快的得到所有边信息
- Kruskal算法代码
/* Kruskal算法生成最小生成树 */
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n, m;
int k = 0;
int parent[MAXVEX]; /* 定义一数组用来判断边与边是否形成环路 */
Edge edges[MAXEDGE];/* 定义边集数组,edge的结构为begin,end,weight,均为整型 */
/* 用来构建边集数组并排序********************* */
for ( i = 0; i < G.numVertexes-1; i++)
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<GRAPH_INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
/* ******************************************* */
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0; /* 初始化数组值为0 */
printf("打印最小生成树:
");
for (i = 0; i < G.numEdges; i++) /* 循环每一条边 */
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) /* 假如n与m不等,说明此边没有与现有的生成树形成环路 */
{
parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中。 */
/* 表示此顶点已经在生成树集合中 */
printf("(%d, %d) %d
", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
- 分析Kruskal算法时间复杂度,适用什么图结构,为什么?
此算法的Find函数由边数e决定,时间复杂度为O(loge),而外面由一个for循环e次,所以Kruskal算法的时间复杂度为O(eloge)。
从边的角度求网的最小生成树,时间复杂度为O(eloge)。和Prim算法恰恰相反,Kruskal算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势,而Prim算法对于稠密图,即边数非常多的情况会更好一些
1.4 最短路径
最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。
1.4.1 Dijkstra算法求解最短路径
初始状态
数据变化Ⅰ
数据变化Ⅱ
数据变化Ⅲ
数据变化Ⅳ
最终结果
可得到v0到v8的最短路径为v0->v1->v2->v4->v3->v6->v7->v8
D数组值为{0,1,4,7,4,8,10,12,16}
P数组值为{-1,-1,1,4,2,4,3,6,7}
- Dijkstra算法需要哪些辅助数据结构
1.需要D数组用来表示v0到v的最短路径和
2.需要P数组用来表示前驱顶点下标 - Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
/* Dijkstra算法,求有向网G的v0顶点到其余顶点v的最短路径P[v]及带权长度D[v] */
/* P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和 */
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *P, ShortPathTable *D)
{
int v,w,k,min;
int final[MAXVEX]; /* final[w]=1表示求得顶点v0至vw的最短路径 */
for(v=0; v<G.numVertexes; v++) /* 初始化数据 */
{
final[v] = 0; /* 全部顶点初始化为未知最短路径状态 */
(*D)[v] = G.arc[v0][v]; /* 将与v0点有连线的顶点加上权值 */
(*P)[v] = -1; /* 初始化路径数组P为-1 */
}
(*D)[v0] = 0; /* v0至v0路径为0 */
final[v0] = 1; /* v0至v0不需要求路径 */
/* 开始主循环,每次求得v0到某个v顶点的最短路径 */
for(v=1; v<G.numVertexes; v++)
{
min=GRAPH_INFINITY; /* 当前所知离v0顶点的最近距离 */
for(w=0; w<G.numVertexes; w++) /* 寻找离v0最近的顶点 */
{
if(!final[w] && (*D)[w]<min)
{
k=w;
min = (*D)[w]; /* w顶点离v0顶点更近 */
}
}
final[k] = 1; /* 将目前找到的最近的顶点置为1 */
for(w=0; w<G.numVertexes; w++) /* 修正当前最短路径及距离 */
{
/* 如果经过v顶点的路径比现在这条路径的长度短的话 */
if(!final[w] && (min+G.arc[k][w]<(*D)[w]))
{ /* 说明找到了更短的路径,修改D[w]和P[w] */
(*D)[w] = min + G.arc[k][w]; /* 修改当前路径长度 */
(*P)[w]=k;
}
}
}
}
- Dijkstra算法的时间复杂度,使用什么图结构,为什么。
从循环嵌套可以很容易得到此算法的时间复杂度为O(n2),其中n为图中顶点的个数。
邻接矩阵方便通过下标找到顶点间权值并在数组中改变,若为邻接表则较难找到权值。
1.4.2 Floyd算法求解最短路径
- Floyd算法解决什么问题?
可以求任意两个地点的最短路径 - Floyd算法需要哪些辅助数据结构
二维数组D[][],D代表顶点到顶点的最短路径权值和的矩阵
二维数组P[][],P代表对应顶点的最短路径的前驱矩阵,用来存储路径。 - Floyd算法优势,举例说明。
代码足够精简,简洁到就是一个二重循环初始化到一个三重循环权值修正,就完成了所有顶点到所有顶点的最短路径计算
Dijkstra不能处理权值为负数图,而Flyod可以。 - Floyd算法求最短路径
初始状态
数据变化
最终结果
- Floyd算法代码
void ShortestPath_Floyd(MGraph G, Patharc *P, ShortPathTable *D)
{
int v,w,k;
for(v=0; v<G.numVertexes; ++v) /* 初始化D与P */
{
for(w=0; w<G.numVertexes; ++w)
{
(*D)[v][w]=G.arc[v][w]; /* D[v][w]值即为对应点间的权值 */
(*P)[v][w]=w; /* 初始化P */
}
}
for(k=0; k<G.numVertexes; ++k)
{
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
if ((*D)[v][w]>(*D)[v][k]+(*D)[k][w])
{/* 如果经过下标为k顶点路径比原两点间路径更短 */
(*D)[v][w]=(*D)[v][k]+(*D)[k][w];/* 将当前两点间权值设为更小的一个 */
(*P)[v][w]=(*P)[v][k]; /* 路径设置为经过下标为k的顶点 */
}
}
}
}
}
虽然Floyd算法的时间复杂度为O(n3),但当我们面临需要求所有顶点到所有顶点的最短路径问题时,Floyd算法仍旧是个不错的选择。
- 其他求最短路径的算法————Bellman-Ford 算法
通过递推迭代方式反复对边集 E中每条边进行松弛操作,使得源点到其他每个顶点 u∈V 的最短路径估计值逐渐逼近其最短距离.Bellman-Ford 算法最多有 n-1 个阶段,在每一个阶段,都需要循环遍历图中的每一条边进行松弛操作.在前 k 个阶段结束后,求得从源点最多经过 k 条边到达其他所有顶点的最短路.n-1 个阶段结束后, 就求得了源点最多经过 n-1 条边到达其他顶点的最短路径.由于最短路径问题是不包含回路的,所以经过 n-1 条边得到的最短路径就是源点到其他顶点的最短路径的确定值.
- Bellman-Ford 算法的具体步骤
(1)初始化所有点到源点之间的最短距离.将源点到自身的距离设置为 0,源点到其他点的距离设置为无穷大(表示不可达);
(2)对边集 E 中的每条边进行循环遍历,进行松弛操作,循环最多需进行 n-1 次;
(3)检测边集 E 中的每一条边的两个断点是否收敛.如果存在未收敛的顶点,则说明图中存在负权回路.
1.5 拓扑排序
- 拓扑排序定义
把AOV网(用定点表示活动,用弧表示活动间优先关系的有向图)络中各个顶点按照它们互相之间的优先关系排列成一个线性序列的过程叫做拓扑排序。
- 拓扑排序算法基本思路
从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止
- 找一个有向图,并求其对应的拓扑排序序列
在算法中,还需要辅助的数据结构————栈,用来存储处理过程张入度为0的顶点,目的是为了避免每次查找时都要去遍历顶点表找有没有入度为0的顶点。
- 实现拓扑排序代码,结构体如何设计?
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
int weight; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
int in; /* 顶点入度 */
int data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}graphAdjList,*GraphAdjList;
- 书写拓扑排序伪代码,介绍拓扑排序如何删除入度为0的结点?
拓扑排序中通过栈存储所有入度为0的顶点,栈顶出栈,就把栈顶对应的所有邻接点的入度都减1,实现入度为零的顶点的删除。 - 如何用拓扑排序代码检查一个有向图是否有环路?
如果栈空了,但统计的顶点数-不足-总顶点数,那么顶点未删除完,说明有存在环
如果栈空了,统计的顶点数-恰好-等于总顶点数,那么不存在环
1.6 关键路径
- 什么叫AOE-网?
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网。
AOE网是用来表示工程流程的,所以它具有明显的工程特性,如有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始。只有在进入某顶点的各活动都已经结束,该顶点所代表的事件才能发生。
AOE网是用边表示活动的网,边上的权值表示活动持续的事件。
AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题。
- 什么是关键路径概念?
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径
- 什么是关键活动?
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
2.PTA实验作业(4分)
2.1 六度空间(2分)
- 解题思路:
1、利用邻接矩阵存储图结构,利用广度遍历计算两顶点间的距离
2、广度遍历函数:count计算两点间的距离,layer计算层,队列q
3、将每次要访问的点v入队,并标记v已访问,利用while(队不空时,并且层小于6时,出队;
利用for 若某一顶点i未被访问,并且该点i与v是相邻结点,结点间距加1,将i入队,标记i被访问,判断结点关系,计算layer++,更新变量结点
2.1.1 伪代码
- 主函数伪代码
int main()
{
邻接矩阵建图
for i = 1 to n
for j = 1 to G->n
初始化各点,将visited置0
temp = BFS(i);
输出百分比 result = (temp * 100.0) / n
return
}
- BFS求百分比伪代码
int BFS(int v)
{
while (q非空并且层数小于6时)
temp = 队头元素,出队
for i = 1 to n i++
if i未被访问到,并且i v两点是邻接点
then 层数加1,将i入队,标记i被访问,令end = i;
if last == temp
then 层数加1,last = end
return
}
2.1.2 提交列表
2.1.3 本题知识点
- 由于这题要找层次小于6的结点个数,故采用广度遍历(层序遍历),层序遍历需要借助队列
- 需要保存层数(高度),故定义一个last表示最右结点,每当temp的队头==last时,说明该行结束,层数要+1,并且last值修改为下一行的最右结点
2.2 村村通
- 解题思路
1、这道题一开始走偏了想直接同floyd算法求出最短路径然后相加了
floyd最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
最短路径是从一点出发,到达目的地的路径最小。
2、理清楚了最小生成树与最短路径之间的区别以后就很容易想到用最小生成树算法了,这里选用的是Prim算法
2.2.1 伪代码
- 主函数伪代码
int main()
{
输入顶点数和边数
建图
判断镇的数目是否足以保证通畅
输出判断结果
}
- 邻接矩阵建图伪代码
void CreateGraph()
{
二维数组初始化为无穷大
将边的信息输入到邻接矩阵中
}
- prim算法伪代码
int Prim()
{
初始化第一个权值为0,即v0加入生成树
for (遍历)
while (遍历lowcost数组)
if (权值不为 0 且小于 min)
保存最小值
保存最小值的下标存入k
if (k == 0)
return 不连通
将当前顶点设置为0,表示此结点已经完成任务
for (遍历)
if (下标为k顶点各边权值小于此前这些顶点未被加入生成树的权值)
lowcost[j] = G[k][j];
return cost;
}
2.2.2 提交列表
2.2.3 本题知识点
- 本题为最小生成树问题--采用Prim算法,若用floyd算法,最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
- 邻接矩阵的相关知识。