这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 郑俊佳 |
0.PTA得分截图
1.本周学习总结(6分)
本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。
1.1 图的存储结构
1.1.1 邻接矩阵(不用PPT上的图)
邻接矩阵的结构体定义
#define MAXV<最大顶点个数>
#define INF 32767 //定义无穷大
typedef struct
{
int no; //顶点的编号
infoType info; //顶点的其他信息
}VertexType; //顶点的类型
typedef struct
{
int edges[MAXV][MAXV]; //邻接矩阵数组
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //存放顶点信息
}MatGraph; //完整的图邻接矩阵类型
建图函数
1.1.2 邻接表
邻接矩阵的结构体定义
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; //完整的图邻接表类型
建图函数
1.1.3 邻接矩阵和邻接表表示图的区别
邻接矩阵适合用于稠密图,而邻接表更适合用于稀疏图。
邻接矩阵时间复杂度为O(n2<\sup>),n为顶点个数。
邻接表时间复杂度为O(n+e),n为顶点个数,e为边数。
1.2 图遍历
1.2.1 深度优先遍历
选上述的图,继续介绍深度优先遍历结果
深度遍历代码
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 广度优先遍历
选上述的图,继续介绍广度优先遍历结果
广度遍历代码
void BFS(AdjGraph * G,int v)
{
int w,i;
ArcNode * p;
SqQueue * qu;
InitQueue(qu);
int visited[MAXV];
for (i=0;i<G->n;i++)
visited[i]=0;
printf("%2d",v);
visited[v]=1;
enQueue(qu,v);
while(!QueueEmpty(qu))
{
deQueue(qu,w);
p=G->adjlist[w].firstarc;
while(p!=NULL)
{
if(visited[p->adjvex]==0)
{
printf("%2d",p->adjvex);
visited[p->adjvex]=1;
enQueue(qu,p->adjvex);
}
p=p->nextarc;
}
}
printf("\n");
}
适用于解决:求解单源路径问题、求解联通分量的个数、具体的每一个连通分量都包含哪些顶点、环检测、二分图的检测等等
引用一篇博客:广度优先遍历的应用
1.3 最小生成树
最小生成树是一条最短路径(包含所有顶点的路径),所有路径的权值相加最小,且边最少即为n-1(n为顶点数);
1.3.1 Prim算法求最小生成树
基于上述图结构求Prim算法生成的最小生成树的边序列
Prim算法的两个辅助数组是:closest和lowcost。
对于V-U中的一个顶点j,它的最小边对应U中的某个顶点,则用closest[j]保存U中的这个顶点。并用lowcost[j]保存该最小边所对应的权值。
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.wdges[k][j]<lowcost[j])
{
lowcost[j]=g.edges[k][j];
closest[j]=k; //修改数组lowcost和closest
}
}
}
Prim算法的时间复杂度是:O(n2)
Prim算法适用于稠密图,可以不用判断是否产生回路,因为在待选边表中不停计算的过程中,可以有效避免产生回路的情况。其时间复杂度只与节点数量有关,在特定情况下可以更快的执行完程序。
1.3.2 Kruskal算法求解最小生成树
基于上述图结构求Kruskal算法生成的最小生成树的边序列
实现Kruskal算法的辅助数据结构是什么?其作用是什么?Kruskal算法代码。
实现Kruskal算法的辅助数据结构是一个辅助数组vset[0...(n-1)],用于记录一个顶点i所在的连通分量编号,即vset[i];
其辅助数组用于判断选取的一条边会不会使其最小生成树出现回路。
typedef struct
{
int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
}Edge;
void Kruskal(MatGraph g) //Kruskal算法
{
int i,j,u1,v1,sn1,sn2,k;
int vset[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.edges[i][j];
k++;
}
InsertSort(E,g.e); //采用直接插入排序对E数组按权值递增排序
for(i=0;i<g.n;i++) //初始化辅助数组
vset[i]=i;
k=1; //k表示当前构造生成树的第几条边,初值为1
j=0; //E中边的下标,初值为0
while(k<g.n) //生成的边数小于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++; //生成边数增1
for(i=0;i<g.n;i++) //两个集合统一编号
if(vest[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
}
void Kruskal(MatGraph g) //改进的Kruskal算法
{
int i,j,k,u1,v1,sn1,sn2;
UFSTree t[MaxSize];
Edge E[MaxSize];
k=1; //e数组的下标从1开始计
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.edges[i][j];
}
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++; //扫描下一条边
}
}
未改进前Kruskal算法的时间复杂度是O(e2)
改进后Kruskal算法的时间复杂度是O(elog2e)
Kruskal算法适用于求稀疏图中的最小生成树,不需要用邻接表或者邻接矩阵存图,只需要用个结构体存边即可。其思路比Prim算法清晰很多。
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
Dijkstra算法需要两个辅助数组dist[MAXV], path[MAXV],前者用于存储各点到所求点的最短路程,后者用于存储其他的点到所求点路径的上一点。
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; //源点编号v放入S中
path[v]=0;
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)
{
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");
}
}
}
Dijkstra算法的时间复杂度为O(n2),适用于有权图,并要求其权不为负的。
因为dijkstra是基于贪心策略,每次都找一个距源点最近的点,然后将该距离定为这个点到源点的最短路径;
但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点,再通过这个负权边,使得路径之和更小,这样就出现了错误。
例题:
对于上图将A添加到集合中标记已访问,之后选出从A到所有节点中的最短的点,于是把C加入集合中标记已访问,之后C不能在更新了。
而显然,A与C之间最短路径权值为0(A-B-C),发生错误。
1.4.2 Floyd算法求解最短路径
Floyd算法解决求每一个顶点到其他顶点的最短路径问题。
Floyd算法使用邻接矩阵来存储图结构,需要以下两个辅助数据结构:
1.二维数组 Path[i][j]:最短路径上顶点 vj 的前一顶点的序号;
2.二维数组 A[i][j]:记录顶点 vi 和 vj 之间的最短路径长度;
void Floyd(MatGraph g) //Floyd算法
{
int A[MAXV][MAXV],path[MAXV][MAXV];
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][j]=-1; //顶点i到j没有边时
}
for(k=0;k<g.n;k++) //依次考查所有顶点
{
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
if(A[i][j]>A[i][k]+A[k][j])
{
A[i][j]=A[i][k]+A[k][j]; //修改最短路径长度
path[i][j]=path[k][j]; //修改最短路径
}
}
Dispath(g,A,path); //输出最短路径
}
void Dispath(MatGraph g,int A[][MAXV],int path[][MAXV])
{
int i,j,k,s;
int apath[MAXV],d; //存放一条最短路径中间顶点(反向)及其顶点个数
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
{
if(A[i][j]!=INF&&i!=j) //若顶点i和j之间存在路径
{
printf("从%d到%d的路径为:",i,j);
k=path[i][j];
d=0; //路径上添加终点
apath[d]=j;
while(k!=-1&&k!=i) //路径上添加中间点
{
d++;
apath[d]=k;
k=path[i][k];
}
d++; //路径上添加起点
apath[d]=i;
printf("%d",apath[d]); //输出起点
for(s=d-1;s>=0;s--) //输出路径上中间顶点
printf(",%d",apath[s]);
printf("\t路径长度为:%d\n",A[i][j]);
}
}
}
Floyd算法是一种动态规划算法,稠密图效果最佳,边权可正可负。
此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行V次SPFA算法。
优缺点如下:
优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度为O(n3)比较高,不适合计算大量数据。
1.5 拓扑排序
上有向图的一种拓扑序列A->E->B->C->F->D->G
实现拓扑排序代码,结构体如何设计?
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]; //出栈的一个顶点i
top--;
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; //找下一个邻接点
}
}
}
参考伪代码:
TOPOLOGICAL-SORTING-GREEDY(g)
let inDegree be every verties inDegree Array
let stack be new Stack
let result be new Array
for v equal to every vertex in g
if inDegree[v] == 0
stack.push(v)
end
while stack.empty() == false
vertex v = stack.top()
stack.pop()
result.append(v)
for i equal to every vertex adjacent to v
inDegree[i] = inDegree[i] - 1
if inDegree[i] == 0
stack.push(i)
end
end
return result.reverse()
1.6 关键路径
AOE-网是用边去表示活动的网,它是一种带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。
在AOE网中,从源点到汇点的所有路径中具有最大路径长度的路径称为关键路径。
完成整个工程的最短时间就是AOE网中关键路径的长度,或者说是AOE网中一条关键路径上各活动持续时间的总和,把关键路径上的活动称为关键活动。
2.PTA实验作业(4分)
2.1 六度空间(2分)
2.1.1 伪代码(贴代码,本题0分)
void BFS(MGraph& g,int u){
将u顶点标记
初始化dist[u]=0记录距离
将u进队列
cnt++;//用来表示几个人
while q
取出队头,判断距离dist是否大于6
循环矩阵,只要没有被标记并且有边
进队列,距离+1;标记已访问,cnt++
}
2.1.2 提交列表
2.1.3 本题知识点
-
- new申请空间:new int* [MAXV + 1]
-
- dis[]进行距离计算,visited[]进行标记
-
- 每次每个人都要将数组初始化
2.2 村村通或通信网络设计或旅游规划(2分)
2.2.1 伪代码(贴代码,本题0分)
void Dijkstra(MGraph g, int v)
{
初始化dist数组、s数组、pay数组,dist数组
遍历图中所有节点
for(i = 0; i < g.n; i++)
若s[i]! = 0,则数组找最短路径,顶点为u
s[u] = 1进s
for(i = 0; i < g.n; i++)
if(g.edges[u][j].len < INF && dist[u] + g.edges[u][j].len < dist[j])
则修正dist[j] = dist[u] + g.edges[u][j].len;
pay[j] = pay[u] + g.edges[u][j].pay;
else if(路径一样长但是花费更少)
则修正pay[j] = pay[u] + g.edges[u][j].pay;
}
2.2.2 提交列表
2.2.3 本题知识点
-
- 邻接矩阵,无向图
-
- 最短路径Dijkstra算法
-
- 用另一个结构体,存路径长度和费用,并用dist[]与pay[]存储