DS博客作业04--图
2021-05-18 20:49 米奈希尔。 阅读(64) 评论(1) 编辑 收藏 举报这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业04--图 |
这个作业的目标 | 学习图结构设计及相关算法 |
姓名 | 罗发槺 |
0.PTA得分截图
1.本周学习总结(6分)
1.1 图的存储结构
1.1.1 邻接矩阵(不用PPT上的图)
造一个图,展示其对应邻接矩阵
邻接矩阵的结构体定义
#define MAXV <最大顶点个数>
typedef struct
{ int no; //顶点编号
InfoType info; //顶点其他信息
} VertexType;//声明顶点的类型
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //存放顶点信息
} MatGraph;//声明的邻接矩阵类型
MatGraph g;//声明邻接矩阵存储的图
建图函数
void CreateMGraph(MGraph& g, int n, int e)//建无向图
{
int i, j, a, b;
g.e = e;
g.n = n;
for (i = 1; i < g.n; i++)
for (j = 1; j < g.n; j++)
g.edges[i][j] = 0; //初始化邻接矩阵
for (i = 1; i <= g.e; i++)
{
cin >> a >> b;
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
}
1.1.2 邻接表
造一个图,展示其对应邻接表(不用PPT上的图)
邻接矩阵的结构体定义
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode;
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
InfoType info; //该边的权值等信息
} ArcNode;
typedef struct
{ VNode adjlist[MAXV] ; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph;
AdjGraph *G;//声明一个邻接表存储的图G
建图函数
void CreateAdj(AdjGraph*& G, int n, int e)
{
ArcNode* p, * q;
int i, a, b;
G = new AdjGraph;
for (i = 1; i <= n; i++)
{
G->adjlist[i].firstarc = NULL; //头结点初始化
}
for (i = 1; i <= e; i++)
{
cin >> a >> b; //输入边
/*无向图需要双向赋值*/
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 邻接矩阵和邻接表表示图的区别
1.对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
2.邻接矩阵的空间复杂度为0(n2),而邻接表的空间复杂度为0(n+e)。
3.在邻接表上容易找到任意一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi,vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,还不及邻接矩阵方便。
4.邻接矩阵多用于稠密图的存储(e接近n(n-1)/2),而邻接表多用于稀疏图的存储(e<<n2)。
对于一个具有n个顶点e条边的无向图
它的邻接表表示有n个顶点表结点2e个边表结点
对于一个具有n个顶点e条边的有向图
它的邻接表表示有n个顶点表结点e个边表结点
如果图中边的数目远远小于n2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。
1.2 图遍历
1.2.1 深度优先遍历
邻接矩阵
void DFS(MGraph g, int v)//深度遍历
{
static int n = 0;
int j;
if (visited[v] == 0)
{
if (n == 0)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
visited[v] = 1;
}
for (j = 1; j <= g.n; j++)
{
if (g.edges[v][j] && visited[j] == 0)
DFS(g, j);
}
}
邻接表
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
static int n = 0;
ArcNode* p;
visited[v] = 1;
if (!n)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
p = G->adjlist[v].firstarc;
while (p != NULL && n < G->n)
{
if (visited[p->adjvex] == 0)DFS(G, p->adjvex);
p = p->nextarc;
}
}
1.2.2 广度优先遍历
邻接矩阵
void BFS(MGraph g, int v)
{
queue<int>q;
int top;
int j;
visited[v] = 1; //v元素被访问
cout << v;
q.push(v); //入队根节点
while (!q.empty())
{
top = q.front(); //取队头元素,并出队
q.pop();
for (j = 1; j <= g.n; j++)
{
if (g.edges[top][j] == 1 && visited[j] != 1)
{
cout << " " << j;
visited[j] = 1; //入队所有未被访问的邻接点
q.push(j);
}
}
}
}
邻接表
void BFS(AdjGraph* G, int v)
{
queue<int>q; //定义队列q
ArcNode* p;
int d;
cout << v; //输出起始顶点
visited[v] = 1; //已访问顶点
q.push(v); //顶点加入队列
while (!q.empty()) //队列不空时循环
{
d = q.front(); //出队顶点d
q.pop();
p = G->adjlist[d].firstarc; //顶点d的边结点
while (p)
{
if (visited[p->adjvex] == 0) //每个边结点入队并输出
{
cout << " " << p->adjvex;
visited[p->adjvex] = 1;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
1.3 最小生成树
最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。
1.3.1 Prim算法求最小生成树
基于上述图结构求Prim算法生成的最小生成树的边序列
实现Prim算法的2个辅助数组是什么?其作用是什么?
1.closest[i]:最小生成树的边依附在U中顶点编号。
2.lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。并规定lowcost[k]=0表示这个顶点在U中
Prim算法代码。
#define INF 32767//INF表示∞
void Prim(Graph 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记录最近顶点编号
}
lowcost[k]=0;//标记k已经加入U
for (j=0;j<G.n;j++)//修改数组lowcost和closest
if (lowcost[j]!=0 && G.edges[k][j]<lowcost[j])
{
lowcost[j]=G.edges[k][j];
closest[j]=k;
}
}
}
- Prim()算法中有两重for循环,所以时间复杂度为O(n'),其中n为图的顶点个数。大家可以看出,Prim()算法
的执行时间与图中的边数e无关,所以它特别适合用稠密图求最小生成树。 - 由于 Prim算法中需要频繁地取一条条边的权﹐所以图采用邻接矩阵更合适。
1.3.2 Kruskal算法求解最小生成树
实现Kruskal算法的辅助数据结构是什么?其作用是什么?
数组vest[],用于记录一个顶点i所在的连通分量编号。
Kruskal算法代码。
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
Edge E[MAXV];
void Kruskal(Graph G)
{
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<G.n;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) //两顶点属于不同的集合
{
k++; //生成边数增1
for (i=0;i<g.n;i++) //两个集合统一编号
if (vset[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
}
克鲁斯卡尔算法:O(eloge)、适用于稀统图
下面的Kruskal(g)算法利用上述过程来构造最小生成树,其中参数g 为邻接矩阵。
和Prim算法一样,在该算法中需要频繁地取一条条边的权﹐所以图采用邻接矩阵更合适。
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
- 源点v→j最短路径时,不经过顶点u的原来的最短路径表示为path[j]=a,而经过顶点u的路径若是最短路径,该路径
表示为path[j]=u。所以,在考虑中间点u以后, dist[j]=MIN{ dist[u]+ww; , dist[j门},若经过顶点u的路径
更短,修改path[ji-u,否则不修改 path[i]。
1.数组dist[:源点V到每个终点的最短路径长度
2.数组path[]:最短路径序列的前一顶点的序号;初值或无路径用-1表示
Dijkstra代码
void Dijkstra(Graph 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; //s[]置空
if (G.edges[v][i]<INF) //路径初始化
path[i]=v; //顶点v到i有边时
else
path[i]=-1;
}
s[v]=1; //源点v放入S中
for (i=0;i<G.n;i++) //循环n-1次
{
mindis=INF;
for (j=0;j<G.n;j++)
{
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中的顶点的距离
{
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;
}
}
}
}
//输出最短路径
}
Dijkstra算法的时间复杂度,使用什么图结构,为什么。
算法的时间复杂度为o(n2)。
和求最小生成树的算法一样,Dijkstra算法中需要频繁地取一条条边及其权值,所以图采用邻接矩阵更合适。
1.4.2 Floyd算法求解最短路径
Floyd算法需要哪些辅助数据结构
二维数组A用于存放当前顶点之间的最短路径长度,即分量A[i][j表示当前ij的最短路径长度。
另外用二维数组path保存最短路径,它与当前迭代的次数有关。path,[i][门存放着考查顶点0、1、… k之后得到的i→j的
最短路径中顶点j的前一个顶点编号,这和 Dijkstra算法中采用的方式相似。
Floyd算法的具体代码:
void Floyd(Graph G)
{
int A[MAXVEX][MAXVEX]; //建立A数组
int path[MAXVEX][MAXVEX]; //建立path数组
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;
}
for (k=0;k<G.n;k++) //求Ak[i][j]
{
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]; //修改最短路径为经过顶点k
}
}
}
1.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; //找下一个邻接点
}
}
}
1.6 关键路径
-
若用前面介绍过的有向无环图(directed acycline graph,DAG)描述工程的预计进度,以顶点表示事件,有向
边表示活动,边e的权c(e)表示完成活动e所需的时间(如天数),或者说活动e持续时间。图中入度为0的顶点表
示工程的开始事件(如开工仪式),出度为0的顶点表示工程结束事件,称这样的有向图为边表示活动的
网(activity on edge network,AOE 网)。 -
在AOE网中,从源点到汇点的所有路径中具有最大路径长度的路径称为关键路径( critical path)。
-
完成整个工程的最短时间就是AOE网中关键路径的长度网中一条关键路径上各活动持续的时时间而非
关键活动可能存在富余的时间。通常一个activity)。关键活动不存在富余的时间,而非关键活动可
能存在富余的AOE 网可能存在多条关键路径,但它们的长度是相同的。
2.PTA实验作业(4分)
2.1 六度空间(2分)
2.1.1 伪代码
int main()
{
建图
for i to n
输出 i :
用memset将visit数组中的元素置0
深度遍历i节点得到与该结点距离不超过6的结点数,除以总节点数储存在rate中
输出rate
endfor
}
int BFS(int i)
{
定义一个queue<int>q
int cnt = 1, level = 0, last = i, tail;//level代表层数,last指向每一圈的最后一个点,tail控制last的更新.
Visited[i] = 1
q.push(i);
while q不空
{
用temp暂存q的第一个元素
q.pop()
for j to n
if Visited[j]为0且该节点的边存在
Visited[j] = 1;
q.push(j);
count++;
tail=j
endif
endfor
}
if 当前元素等于该节点边最后一个元素
level++;
last=tail
endif
if层数等于6
break
返回 count
}
2.1.2 提交列表
2.1.3 本题知识点
图的深度遍历
建图
queue的使用
memset函数的使用,极其方便的大量初始化
2.2 村村通(2分)
2.2.1 伪代码
void Prim()
{
建图
for 1 to n
将城镇1的所有道路放进lowcost数组中
endfor
将lowcost[1]置0
for 1 to n
赋给min最大值
for 1 to n
if lowcost[j]有被遍历过且min大于loucost[j]
min更新
mark记住当前下标
endif
endfor
将所有最小值相加
lowcost[mark] = 0
for 1 to n
找当前下标的最小权值
endfor
endfor
for 1 to n
if lowcost[j] != 0
break
endif
endfor
if j <= n
输出-1并退出函数
输出最小值的求和结果
}
2.2.2 提交列表
2.2.3 本题知识点
图的建图
图的遍历
define的使用
全局变量的使用