DS博客作业04--图
DS博客作业04--图
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 姚庆荣 |
0.PTA得分截图
1.本周学习总结(6分)
1.1 图的存储结构
图的存储主要分两种,一种邻接矩阵,另一种邻接表
用不同的方式存储图的信息
1.1.1 邻接矩阵
无向图: 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。
- 结构体定义:
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph;
//另一种表示方式
typedef struct {
int** edges;//邻接矩阵
int n, e;//顶点数,边数
}MGraph;
-
建图函数:
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 邻接表
-
结构体定义
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, j, a, b; G = new AdjGraph; for (i = 1; i <= n; i++)//邻接表头结点置零 { G->adjlist[i].firstarc = NULL; } for (j = 1; j <= e; j++)//无向图 { cin >> a >> b; ArcNode *p,*q; p = new ArcNode; q = new ArcNode; p->adjvex = b;//用头插法进行插入 q->adjvex = a; p->nextarc = G->adjlist[a].firstarc; G->adjlist[a].firstarc = p; q->nextarc = G->adjlist[b].firstarc; G->adjlist[b].firstarc = q; } G->n = n; G->e = e; }
1.1.3 邻接矩阵和邻接表表示图的区别
- 邻接矩阵的空间复杂度为0(n2),而邻接表的空间复杂度为0(n+e)。
- 如果图中边的数目远远小于n2称作稀疏图,这时用邻接表表示比用邻接矩阵表示节省空间;
如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。因此当数据的边关系较为复杂时用邻接矩阵,稀疏时用领接表,内存空间不会浪费。
1.2 图遍历
1.2.1 深度优先遍历
深度遍历图解
1:需要遍历的图
1:从A点出发,红色代表A点被访问。
2:有A->B,A->C都可行,都可以遍历,这里我选择遍历C。
3:再遍历C之后,也可遍历C->B,C->D 我这里选择遍历C->D。
4:现在由D->E 这里遍历了E。
5:E已经不可以再遍历了。然后返回D,D又不可以遍历B,放回C,然后C->B可以。所以从C->B 就实现遍历。
-
深度遍历代码
-
矩阵
void DFS(MGraph g, int v)//深度遍历 { int flag = 0; visited[v] = 1; for (int j = 0; j <g.n; j++) { if (visited[j] == 1) { flag++; } } if (flag == 1) cout << v; else cout << " " << v; for (int i = 0; i < g.n; i++) { if (g.edges[v][i] != 0 && visited[i] == 0) { DFS(g, i); } } }
-
链表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历 { int flag = 0; visited[v] = 1; for (int j = 1; j <= G->n; j++) { if (visited[j] == 1) { flag++; } } if (flag == 1) cout << v; else cout << " " << v; ArcNode* p; for (int i = 0; i < G->n; i++) { p = G->adjlist[v].firstarc; while (p) { if (visited[p->adjvex] == 0&&p!=NULL) { DFS(G, p->adjvex); } p = p->nextarc; } } }
-
-
深度遍历适用哪些问题的求解。(可百度搜索)
深度遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
问题类型:1.全排列问题;2. ABC+DEF=GHI 问题 ;3.二维数组寻找最短路径问题;4.求岛屿的面积.
1.2.2 广度优先遍历
-
广度遍历代码
-
矩阵
void BFS(MGraph g, int v)//广度遍历 { int front; queue<int>q; q.push(v); visited[v] = 1; cout << v; while (!q.empty()) { front = q.front(); q.pop(); for (int i = 0; i < g.n; i++) { if (g.edges[front][i] == 1 && visited[i] == 0) { q.push(i); visited[i] = 1; cout << " " << i +; } } } }
-
链表
void BFS(AdjGraph* G, int v) //v节点开始广度遍历 { int i, j; int front; queue<int>q; ArcNode* p; q.push(v); visited[v] = 1; cout << v; while (!q.empty()) { front = q.front(); q.pop(); p = G->adjlist[front].firstarc; do { if (p != NULL&&visited[p->adjvex]==0) { q.push(p->adjvex); visited[p->adjvex ] = 1; cout << " " << p->adjvex ; } p = p->nextarc; }while (p != NULL); } }
-
-
广度遍历适用哪些问题的求解。(可百度搜索)
广度遍历:BFS算法对于解决最短路径问题比较有效。
问题类型:1.求取最短路径问题;2.求迷宫路径;哪些问题的求解。(可百度搜索)
1.3 最小生成树
有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个连通网,当构造这个连通网所花的权值最小时,搭建该连通网的生成树,就称为最小生成树。
1.3.1 Prim算法求最小生成树
-
Prim算法的步骤(引用)
第一步:随意选取起点
图中有9个顶点v1-v9,集合表示为:V={v1,....,V9},每条边的边权值都在图上;在进行prim算法时,我们先选择v1作为起始点,现在我们设U集合为当前所找到最小生成树里面的顶点,TE集合为所找到的边。
状态如下:U={v1}; TE={};
第二步:在前一步的基础上寻找最小权值
查找一个顶点在U={v1}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。
通过图中我们可以看到边v1-v8的权值最小为2,那么将v8加入到U集合,(v1,v8)加入到TE。
状态如下:U={v1,v8}; TE={(v1,v8)};
第三步:继续寻找最小权值
查找一个顶点在U={v1,v8}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。 通过图中我们可以看到边v8-v9的权值最小为4,那么将v9加入到U集合,(v8,v9)加入到TE。
状态如下:U={v1,v8,v9}; TE={(v1,v8),(v8,v9)};
第四步:在前一步的基础上,继续寻找最小权值
查找一个顶点在U={v1,v8,v9}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。 通过图中我们可以看到边v9-v2的权值最小为1,那么将v2加入到U集合,(v9,v2)加入到TE。
状态如下:U={v1,v8,v9,v2};
TE={(v1,v8),(v8,v9),(v9,v2)};
第五步:继续在前一步的基础上,寻找最小权值
查找一个顶点在U={v1,v8,v9,v2}集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。通过图中我们可以看到边v2-v3的权值最小为3,那么将v3加入到U集合,(v2,v3)加入到TE。
状态如下:U={v1,v8,v9,v2,v3};
TE={(v1,v8),(v8,v9),(v9,v2),(v2,v3)};
第五~九步:继续在前一步的基础上,寻找最小权值
如此循环一下直到找到所有顶点为止。到这大家应该对普利姆算法求解最小生成树的过程有所知晓,但需注意以下三点:
(1)每次都选取权值最小的边,但不能构成回路,构成环路的边则舍弃。如图中的(v1,v9),(v1,v2)等构成回路舍弃
(2)遇到权值相等,又均不构成回路的边,随意选择哪一条,均不影响生成树结果。如图中的(v3,v4),(v6,v5)权值均为9,选择哪一条在先均不影响最小生成树的生成结果。
(3)选取n-1条恰当的边以连通n个顶点。
-
Prim算法代码
#define INF 32767 void Peim(MGraph g, int v) { int lowcost[MAXV]; int min; int closest[MAXV]; int i, j, k; for (i = 0; i < g.n; i++) { lowcost[i] = g.edges[v][i];//置初值,放入顶点v和所有顶带你的权值 closest[i] = v; } for (i = 1; i < g.n; i++)//n-1条边,进行n-1次 { min = INF; for (j = 0; j < g.n; j++)//遍历找到权值最小的 { if (lowcost[j] != 0 && lowcost[j] < min) { min = lowcost[j]; k = j;//记录下标 } } lowcost[k] = 0;//lowcost为0表示该顶点已使用 for (j = 0; i < g.n; j++)//遍历所有顶点,比较找到的顶点与其他顶点的权值是否比原来小 { if (lowcsost[j] != 0 && g.edges[k][j] < lowcost[j]) { lowcost[j] = g.edges[k][j]; closest[j] = k;//改变权值和相邻的顶点 } } } }
-
Prim算法的两个辅助数组是:closest和lowcost。
-
时间复杂度为O(n^2),prim算法适合稠密图。
1.3.2 Kruskal算法求解最小生成树
-
实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
void Kruskal(AdjGraph *g) { 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++) { p=g->adjlist[i].firstarc; while(p!=NULL) { E[k].u=i;E[k].v=p->adjlist; E[k].w=p->weight; k++;p=p->nextarc; } InsertSort(E,g.e); for(i=0;i<g.n;i++) vest[i]=i; k=1; j=0; while(k<g.n) { u1=E[j].u;v1=E[j].v; sn1=vset[u1]; sn2=vset[v1]; if(sn1!=sn2) { printf("(%d,%d):%d\n",u1,v1,E[j].w); k++; for(i=0;i<g.n;i++) if(vest[i]==sn2) vest[i]=sn1; } j++; } }
-
分析Kruskal算法时间复杂度,适用什么图结构,为什么?时间复杂的为O(elog2e),由于其与n无关,只与e有关,适用于稀疏图。
1.4 最短路径
有两种最短路径,一种是一个顶点点到其余各顶点之间的最短路径,另一种是任意两点之间的最小路径,分别用Dijkstra算法和Floyd算法来求解
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
- 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\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");
}
}
}
4、Dijkstra算法的时间复杂度
Dijkstra算法的时间复杂度为o(n^2),其中n为图中的顶点数。
1.4.2 Floyd算法求解最短路径
-
Floyed算法
Floyd算法求解最短路径是每个顶点之间的,也可以Dijkstra调用n次,可能达到求解得出每个顶点之间的最短路径。两个时间的复杂性都是O(n^3),不过Floyd形式上更简单点。
Floyd用一个二维数组A存放当前顶点最短路径长度,如:A[i][j]代表低你干点i到j之间的最短路径。
还有一个path二维数组,和之前Dijkstra算法中的path的用途一致,用来回溯寻找路径进过的顶点,Floyd中的是每个顶点的集合成二维数组。 -
Floyd算法需要哪些辅助数据结构
二维数组A用于存放当前顶点之间的最短路径长度,即分量A[i][j]表示当前i->j的最短路径长度。
-
Floyd算法优势,举例说明
Floyd算法的优势:它能一次求得任何两个节点之间的最短路径,而Dijkstra算法只能求得以特定节点开始的最短路径。
最短路径算法还有其他算法,可以自行百度搜索,并和教材算法比较。
-
SPFA算法(Shortest Path Fast Algorithm的缩写)
int SPFA(int s, int t) { int dist[maxn], inq[maxn]; for(int i = 0; i < n; i ++ ) { dist[i] = inf, inq[i] = 0; } queue<int>que; que.push(s), inq[s] = 1, dist[s] = 0; while(!que.empty()) { int now = que.front(); que.pop(); inq[now] = 0; for(int i = first[now]; ~i; i = edge[i].next) { //每次拿出一个点开始松弛。 int to = edge[i].to, w = edge[i].w; if(dist[to] > dist[now] + w) { //这个if看下面的图 dist[to] = dist[now] + w; if(!inq[to]) { //松弛过的点dist变换了,可能影响其他的点。需要继续松弛 inq[to] = 1; que.push(to); } } } } return dist[t] == inf ? -1 : dist[t]; }
-
与其他两种算法的比较:
PFA也叫bellman ford的队列优化。但是bellman ford的复杂度比较高。SPFA的平均复杂度是O(n*log2n),复杂度不稳定,在稠密图(边多的图)跑的比dijkstra慢,稀疏图(边少的图)跑的比Dijkstra快。在完全图达到最坏的平方级复杂度。(引用博文)
1.5 拓扑排序
- 找一个有向图,并求其对要的拓扑排序序列
上有向图的一种拓扑序列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; ArcNode *p; for (i=0;i<G->n;i++) 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++) if (G->adjlist[i].count==0) { top++; St[top]=i; } while (top>-1) { i=St[top];top--; printf("%d ",i); p=G->adjlist[i].firstarc; while (p!=NULL) { j=p->adjvex; G->adjlist[j].count--; if (G->adjlist[j].count==0) { top++; St[top]=j; } p=p->nextarc; } } }
1.6 关键路径
- 什么叫AOE-网?
在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,一个工程常被分为多个小的子工程,这些子工程被称为活动(Activity),在带权有向图中若以顶点表示事件,有向边表示活动,边上的权值表示该活动持续的时间,这样的图简称为AOE网。——百度百科
-
什么是关键路径概念?
路径长度:路径上各活动持续时间的总和(即路径上所有权之和)。
完成工程的最短时间:从工程开始点(源点)到完成点(汇点)的最长路径称为完成工程的最短时间。关键路径:路径长度最长的路径称为关键路径。
-
什么是关键活动?
关键活动:在关键路径上的活动称为关键活动。
2.PTA实验作业(4分)
2.1 六度空间(2分)
2.1.1 伪代码(贴代码,本题0分)
while (队不为空)
{
出队顶点V
遍历V的所有邻接点
{
找到没有遍历过的顶点,进队列
sum++;
}
如果在遍历的时候发现遍历的节点是这一层的最后一个
{
level++;
更新这层的最后一个
}
如果层数为6 //说明已经到了六层,剩下的就是不符合六度空间理论的顶点
返回我们记录下的sum
}
2.1.2 提交列表
2.1.3 本题知识点
2.2 村村通(2分)
2.2.1 伪代码(贴代码,本题0分)
Prim算法
{
初始化lowcost,closest数组
for(遍历lowcost数组)
if(lowcost[i]!=0 找最下边邻接点node 若权值不为 0 且小于 min)
计算总费用
end if
检查新加入生成树的顶点和其他顶点是否存在更小的边
if(lowcost[i]!=0&& edges[i][node]<=lowcost[node])
lowcost[node]=edges[i][node]
end if
end for
if(是否连通)
for i=1 to g->n
end if
}