这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 崔天赐 |
0. PTA得分
1. 本周学习总结
1.1 图的存储结构
1.1.1 邻接矩阵
- 邻接矩阵的结构体定义
//图的邻接矩阵
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
- 建图
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int a, b;
//初始化矩阵
for (int i = 1; i <= MAXV; i++) {
for (int j = 1; j <= MAXV; j++) {
g.edges[i][j] = 0;
}
}
//填入对应
for (int i = 1; i <= e; i++) {
cin >> a >> b;
g.edges[a][b] = 1;//无向图需要两个边都为一
g.edges[b][a] = 1;
}
g.e = e;
g.n = n;
}
1.1.2 邻接表
- 邻接矩阵的结构体定义
//声明边结点类型
typedef struct ANode
{
int adjvex; //该边的终点编号
struct ANode* nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
//声明邻接表头节点类型
typedef struct Vnode
{
Vertex data; //顶点信息
ArcNode* firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
//声明图邻接表类型
typedef struct
{
AdjList adjlist; //邻接表
int n, e; //图中顶点数n和边数e
} AdjGraph;
- 建图函数
void CreateAdj(AdjGraph*& G, int n, int e)//创建图邻接表
{
int i;
int a, b;
ArcNode* p;
G = new AdjGraph;
//G->adjlist = new VNode[n];//申请空间
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;
//无向图,另一个节点也存在相同的关系
p = new ArcNode;
p->adjvex = a;
p->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc = p;
}
G->e = e;
G->n = n;
}
1.1.3 邻接矩阵和邻接表表示图的区别
- 邻接矩阵的时间复杂度为0(n2),而邻接表的时间复杂度为0(n+e)。
- 在邻接表上容易找到任意一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi,vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,还不及邻接矩阵方便。
1.2 图遍历
1.2.1 深度优先遍历
- 选上述的图,继续介绍深度优先遍历结果
初始点从1开始的深度遍历:123465。 - 深度遍历代码
int visited[MAX] = { 0 }; //全局数组
void DFS(AdjGraph* G, int v) //深度优先遍历算法
{
ArcNode* p;
visited[v] = 1; //置已访问标记
printf("%d ", v) //输出被访问顶点的编号
p - G → > adjlist[v].firstarc; //p指向顶点v的第一个邻接点
while (p != NULL)
{
if (visited[p→ > adjvex) == 0) //若p->adjvex顶点未被访问,递归访问它
DFS(G, p->adjvex);
p = p->nextarc; //p指向顶点v的下一个邻接点
}
}
- 深度遍历适用哪些问题的求解
图的深度遍历可以找到两点之间的全部路径,因此可以找到迷宫问题的全部可能答案
1.2.2 广度优先遍历
- 选上述的图,继续介绍广度优先遍历结果
初始点从1开始的广度遍历:123645 - 广度遍历代码
void graph_bfs(Graph *pg){
printf("广度遍历:");
Queue que;
queue_init(&que,pg->vertexnum);
bool visited[pg->vertexnum];
int i=0;
for(i=0;i<pg->vertexnum;i++){
visited[i] = false; //顶点没有遍历
}
for(i=0;i<pg->vertexnum;i++){
if(!visited[i]){//顶点没有遍历
visited[i] = true;//设置为已经遍历
queue_push(&que,i);
while(!queue_is_empty(&que)){
int v = queue_pop(&que);
printf("%c ",pg->vertex[v].key);
ENode *node = pg->vertex[v].enodes;
while(node != NULL){
int pos = get_position(pg,node->key);
if(!visited[pos]){
visited[pos] = true;
queue_push(&que,pos);
}
node = node->next;
}
}
}
}
printf("
");
queue_destroy(&que);
}
- 广度遍历适用哪些问题的求解
求解最短路径或者最短步数
走迷宫
1.3 最小生成树
用自己语言描述什么是最小生成树:
包含原图所有结点,并且有保持连通图的最少的边的连通图
1.3.1 Prim算法求最小生成树
-
基于上述图结构求Prim算法生成的最小生成树的边序列
普里姆(Prim)算法是一种构造性算法,假设G=(V,E)是一个具有n个顶点的带权连通图,T=(U, TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始点v出发的最小生成树T的步骤如下:
(1)初始化U={2},以v到其他顶点的所有边为候选边。
(2)重复以下步骤(n-1)次,使得其他(n-1)个顶点被加入到U中。
①从候选边中挑选权值最小的边加入TE,设该边在V-U中的顶点是k,将k加入U中;
②考查当前V-U中的所有顶点j,修改候选边,若(k,j)的权值小于原来和顶点j关联的候选边,则用(k,i)取代后者作为候选边.
如下图所示的带权连通图,假设起始点为顶点0,采用Prim算法构造最小生成树
(1)首先的最小生成树T仅包含所有的顶点
(2) U={0},V-U={1,2,3,4,5,6},在这两个顶点集之间选择第1条最小边(0,5)添加到T中
(3) U={0, 5},V-U={1,2,3,4,6},在这两个顶点集之间选择第2条最小边(5,4)添加到T中
(4)依此类推,中间步骤下图所示,直到U中包含所"权连通图 有的顶点,这样一共选择了6条边,产生的最小生成树如下图所示
-
实现Prim算法的2个辅助数组是什么?其作用是什么?
visited[]:遍历时用到的数组,将经过的结点值置1.
lowcost[]:prim算法的核心数组, 用于比对并存放最小路径 -
Prim算法代码
int Prim(MGraph& g)//prim算法
{
int lowcost[MAXV] = { 0 };
int min, i, j, k;//最小 + 循环*2 + 防空
int lenth = 0;
lowcost[1] = 0;//从1村开始
for (i = 2; i <= g.n; i++)
{
lowcost[i] = g.edges[1][i];
}
for (i = 2; i <= g.n; i++)//行数
{
min = 66235;
j = 1;
k = 0;
while (j <= g.n)
{
if (lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;
}
j++;
}
if (k == 0)//有结点没遍历到
{
return -1;
}
lenth += min;
lowcost[k] = 0;
for (j = 2; j <= g.n; j++)
{
if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j])
{
lowcost[j] = g.edges[k][j];//最小路径存入
}
}
}
return lenth;
}
- 分析Prim算法时间复杂度,适用什么图结构
Prim算法其时间复杂度为O(n^2),与边得数目无关,适合稠密图.
1.3.2 Kruskal算法求解最小生成树
- 基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
克鲁斯卡尔(Kruskal)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。假设G=(V,E)是一个具有n个顶点的带权连通无向图,T=(U,TE)是G的最小生成树,则构造最小生成树的步骤如下:
(1)置U的初值为V(即包含有G中的全部顶点),TE的初值为空集(即图T中的每个顶点都构成一个分量)。
(2)将图G中的边按权值从小到大的顺序依次选取,若选取的边未使生成树T形成回路,则加入TE,否则舍弃,直到TE中包含(n-1)条边为止。
对于下图所示的带权连通图,采用Kruskal算法构造最小生成树的过程如下:
(1)将所有边按权值递增排序,其结果如图所示。图中边上的数字表示该边是第几小的边,如1表示是最小的边,2表示是第2小的边,依此类推。
(2)首先的最小生成树T仅包含所有的顶点,如下图所示
(3)选取最小边(0,5)直接加入到T中,此时不会出现回路,如下图所示。
(4)选取第2小的边(2,3)直接加入到T中,此时不会出现回路,如下图所示。
说明:在采用Kruskal算法构造最小生成树时,前面的两条边可以直接加入到T中,因为只有两条边的图不可能存在回路。
(5)选取第3小的边(1,6),加人到T中不会出现回路,将其加人,如下图所示。
(6)选取第4小的边(1,2),加入到T中不会出现回路,将其加入,如下图所示。
(7)选取第5小的边(3,6),加入到T中会出现回路,舍弃它。选取第6小的边(3,4),加入到T中不会出现回路,将其加入,如下图所示。
(8)选取第7小的边(4,6),加入到T中会出现回路,舍弃它。选取第8小的边(4,5).
- 辅助数据结构是数组vest[],集合应用,用来判断该条边加入后是否会形成回路
该算法采用邻接表结构更合适
该算法适用于稀疏图
该算法时间复杂度:O(n^2)-->O(eloge)
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
- 基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
- Dijkstra算法需要哪些辅助数据结构
借助最小索引堆作为辅助数据结构 - Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码
void Dijkstra(MatGraph 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;
if(g.edges[v][I]<INF)
path[I]=v;
else
path[I]=-1;
}
S[v]=1;path[v]=0;
for(I=0;i<g.n-1;i++)
{
MINdis=INF;
for(j=0;j<g.n;j++)
if(S[j]==0&&dist[j]<MINdis)
{
u=j;
MINdis=dist[j];
}
S[u]=1;
for(j=0;j<g.n;j++)
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);
}
输出单源最短路径的Dispath()函数如下
void Dispath(MatGraph g;int dist[],int path[],int S[],int v)
{
int I,j,k;
int apath[MAXV],d;
for(I=0;i<g.n;i++)
if(S[I]==1&&I!=v)
{
printf("从顶点%d到顶点%d的路径长度为:%d 路径为:",v,i,dist[I]);
d=0;apath[d]=I;
k=path[I];
if(k==-1)printf("无路径
");
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("
");
}
}
}
- 分析Kruskal算法时间复杂度,适用什么图结构,为什么?
从循环嵌套可以很容易得到此算法的时间复杂度为O(n2),其中n为图中顶点的个数。
邻接矩阵方便通过下标找到顶点间权值并在数组中改变,若为邻接表则较难找到权值。
1.4.2 Floyd算法求解最短路径
- Floyd算法解决什么问题?
Floyd算法是解决给定的加权图中顶点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包 - Floyd算法需要哪些辅助数据结构
1.有向图G=(V,E)用邻接矩阵g表示。
2.设置一个二维数组A用于存放当前顶点之间的最短路径长度,即分量A[i][j]表示当前i->j的最短路径长度。
3.用二维数组path保存最短路径,它与当前迭代的次数有关。在算法结束时,由二维数组path的值追溯,可以得到从i->j的最短路径。
4.Floyd算法代码 - Floyd算法优势,举例说明
Floyd算法容易理解,可以算出任意两个节点之间的最短距离,代码编写简单,但时间复杂度比较高,不适合计算大量数据。
1.5 拓扑排序
1.从有向无环图图中选择一个没有前驱(即入度为0)的顶点并输出。
2.从图中删除该顶点和所有以它为起点的有向边。
3.重复1. 和2. 直到当前的有向无环图为空或当前图中不存在无前驱的顶点为止,后一种情况说明有向图中必然存在环。
4.所以得到拓扑排序后的结果是 { 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-网?
在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间 - 什么是关键路径概念?
关键路径是指设计中从输入到输出经过的延时最长的逻辑路径。优化关键路径是一种提高
设计工作速度的有效方法。一般地,从输入到输出的延时取决于信号所经过的延时最大路径,
而与其他延时小的路径无关。在优化设计过程中关键路径法可以反复使用,直到不可能减少
关键路径延时为止。EDA工具中综合器及设计分析器通常都提供关键路径的信息以便设计者
改进设计,提高速度。 - 什么是关键活动?
关键活动是为准时完成项目而必须按时完成的活动。即处于关键路径上的活动。所有项目都是
由一系列活动组成,而在这些活动中存在各种链接关系和活动约束。其中有些活动如果延误就
会影响整个项目工期。在项目中总存在这样一类直接影响项目工期变化的活动
2.PTA实验作业
2.1 六度空间
2.1.1 伪代码
int main()
{
定义矩阵;
输入数据对矩阵的元素进行修改;
for (i = 1; i <= n; i++)
{
cout = BFS(i);
}
}
int BFS(int i)
{
static int visited[MAXV];//存放已访问过的结点
queue<int>q;
int level;
int last = i;//用于判断是否为该层最后一个
int tail;
int count = 0;
访问该结点并进队;
while (队不为空)
{
队头元素出;
for (遍历结点)
{
if (未访问过且边存在)
{
访问进队;
count++;
tail记录此时结点;
}
}
if (last == i)//为该层最后一个
{
level++;
last = j;//移动到下一层
}
if (达到6层)
{
return count;
}
}
}
2.1.2 提交列表
2.1.3 本题知识点
利用邻接矩阵进行广度遍历,通过广度遍历进行层数的判断,需要引入last和tail进行结点访问的层数判断以及结点层数的改变,通过比较last可以判断层数是否需要改变,并及时返回数量。
2.2 村村通或通信网络设计或旅游规划
2.2.1 伪代码
int* closest;//保存相关顶点的下标
int* lowcost;//保存相关顶点的权值
int cost;//总的路径
根据顶点、边数、边建图
初始化lowcost和closest数组
lowcost赋值1到邻边的值,不是邻边的就赋值无穷大
遍历n - 1次//有n-1和边
{
寻找lowcost的最小的那条边
{
cost += 最小边
lowcost[最小边的邻接点] = 0;//表示已经遍历过
}
遍历n个顶点
{
如果新加入顶点到为遍历过顶点之间的距离比原来小
我们更新lowcost
把closest也跟着更新
}
}
如果没有全都遍历过
不连通返回-1
否则
返回cost
2.2.2 提交列表
2.2.3 本题知识点
Dijkstra算法,
是否连通图的判断,因为我们用Dijkstra算法来遍历这个图,非联通发图才会在最后还有顶点没有遍历