0.PTA得分截图
图题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3,否则本次作业最高分5分。
1.本周学习总结(6分)
本次所有总结内容,请务必自己造一个图(不在教材或PPT出现的图),围绕这个图展开分析。建议:Python画图展示。图的结构尽量复杂,以便后续可以做最短路径、最小生成树的分析。
1.1 图的存储结构
1.1.1 邻接矩阵(不用PPT上的图)
图
对应邻接矩阵
邻接矩阵的结构体定义
typedef struct
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,弧数
} MGraph; //图的邻接矩阵表示类型
建图函数
void CreateMGraph(MGraph& g, int n, int e)//建图
{
int i, j;
int a, b;
g.n = n; g.e = e; //
//建邻接矩阵
for (i = 0; i <= n; i++)
{
for (j = 0; j <= n; j++)
g.edges[i][j] = 0; //邻接矩阵初始化
}
for (i = 0; i < e; i++) //构建邻接矩阵
{
cin >> a >> b;
g.edges[a][b] = 1;
g.edges[b][a] = 1;
}
}
1.1.2 邻接表
图
对应邻接表
邻接矩阵的结构体定义
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef struct
{ AdjList adjlist; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph; //邻接表类型
建图函数
void CreateAdj(AdjGraph*& G, int n, int e) //创建图邻接表
{
int i, j, a, b;
int A[MAXV][MAXV];
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 = 0; j <= n; j++)
{
A[i][j] = 0;
}
}
for (i = 0; i < e; i++)//邻接矩阵对应边置1
{
cin >> a >> b;
A[a][b] = 1; A[b][a] = 1;
}
//查找邻接矩阵中的每个元素
for (i = 1; i <= n; i++)
{
for (j = 1; j <= n; j++)
{
if (A[i][j])
{
p = (ArcNode*)malloc(sizeof(ArcNode));
p->adjvex = j; //存放临节点
p->info = A[i][j]; //放权值
p->nextarc = G->adjlist[i].firstarc; //头插法插入节点
G->adjlist[i].firstarc = p; //
}
}
}
G->n = n; G->e = e;
}
1.1.3 邻接矩阵和邻接表表示图的区别
各个结构适用什么图?时间复杂度的区别。
邻接矩阵用二维数组表示,适用于稠密图,时间复杂度为O(n^2)
邻接表用链表表示,适用于稀疏图,时间复杂度为O(n+e)
1.2 图遍历
1.2.1 深度优先遍历
深度优先搜索遍历的过程是:
从图中某个初始顶点 v 出发,首先访问初始顶点 v ,然后迭择一个与顶点 v 相邻且没被访问过的
顶点 w 为初始顶点,再从 w 出发进行深度优先搜索,直到图中与当前顶点 v 邻接的所有顶点都被访问过为止。
从顶点2出发开始深度遍历: 2->1->4->5->3
深度遍历代码
void DFS(MGraph g, int v)//深度遍历
{
int i;
visited[v] = 1;
if (!flag)
{
cout << v;
flag = 1;
}
else cout << " " << v;
for (i = 1; i <= g.n; i++)
{
if (g.edges[v][i] && !visited[i])
DFS(g, i);
}
}
void DFS(AdjGraph* G, int v)//v节点开始深度遍历
{
ArcNode* p;
visited[v] = 1; //置1已经访问过的节点
if (!flag)
{
cout << v;
flag = 1;
}
else cout << " " << v;
p = G->adjlist[v].firstarc; //p指向顶点v的第一个临节点
while (p!=NULL) //遍历完一条链
{
if (!visited[p->adjvex])
DFS(G, p->adjvex);
p = p->nextarc; //p指向v的下一个临jiedian
}
//return;
}
深度遍历适用问题:
是否有解
最短距离
1.2.2 广度优先遍历
广度优先遍历的过程是:
初始点v首先访问初始点vi 接着访问vi的所有来被访问过的邻接点vi1,vi2,vit,然后再
按照vi1,vi2,vit的次序,访问每一个顶点的所有未被访问过的邻接点,依次类推,直到图中所有和初始点vi有路径
相通的顶点都被访问过为止。
从顶点2出发开始广度遍历: 2->1->3->4->6
*广度遍历代码
int BFS(MGraph g,int v)//广搜
{
int w;
int visited[MAXV]={0};
queue<int> q;
q.push(v);
visited[v]=1;
count++;
while(!q.empty())
{
int j;
w=q.front();
q.pop();
for(j=1;j<=g.n;j++)
{
if(!visited[j]&&g.edges[w][j])
{
q.push(j);
visited[j]=1;
}
}
}
}
void BFS(AdjGraph* G, int v) //v节点开始广度遍历
{
int w,i;
ArcNode *p;
queue<int> q;
cout << v;
visited[v]=1; //置已访问顶点为1
q.push(v); //顶点入队
while(!q.empty())
{
w = q.front(); q.pop();
p=G->adjlist[w].firstarc;
while(p!=NULL)
{
if(!visited[p->adjvex])
{
cout<<" "<<p->adjvex;
visited[p->adjvex]=1;
q.push(p->adjvex);
}
p=p->nextarc;
}
}
}
广度遍历适用问题:
求解最短路径或者最短步数
走迷宫
1.3 最小生成树
用自己语言描述什么是最小生成树:
包含原图所有结点,并且有保持连通图的最少的边的连通图
1.3.1 Prim算法求最小生成树
普里姆(Prim)算法是一种构造性算法,用于构造最小生成树。
过程如下:
(1)初始化U={v}。v到其他顶点的所有边为候选边;
(2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边
思路
给定一个图,和一个起始点x。初始化U,使得U={x}。
在x到其他顶点的边里,选择最小的那条边,并得到x的邻接点y,将y放入U中。
判断y的加入是否产生了更短的路径,如果有更短的路径,修改候选边。
重复2、3两步直到所有的点都在U中为止。
最小生成树求解
找最小lowcost
加入新顶点,修正lowcost, closest
基于上述图结构求Prim算法生成的最小生成树的边序列
结果为:(2,1)、(1,3)、(3,5)、(5,4)
实现Prim算法的2个辅助数组
visited[]:遍历时用到的数组,将经过的结点值置1
lowcost[]:prim算法的核心数组, 用于比对并存放最小路径
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;
}
}
}
时间复杂度为 O(n^2) 涉及二层循环
适合稠密图:与网中的边数无关,从点的方面考虑构建
1.3.2 Kruskal算法求解最小生成树
基于上述图结构求Kruskal算法生成的最小生成树的边序列
结果为:(4,5)、(1,2)、(3,5)、(1,3)
实现Kruskal算法的辅助数据结构、作用
假设连通网G=(V,E),令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。
在E中选择代价最小的边,若该边依附的顶点分别在T中不同的连通分量上,则将此边加入到T中;否则,舍去此边而选择下一条代价最小的边。
依此类推,直至T中所有顶点构成一个连通分量为止
设计Kruskal算法的关键是如何判断选取一条边(i,j)加入到T中是否会出现回路,可以通过判断顶点i,j是否同属于一个连通分量的方法来解决。
为此,设置一个辅助数组vext[0...、n-1],vext[i]用于记录一个顶点所在的连通分量编号,
初值时每个顶点构成一个联通分量,所以有vext[i]=i,vest[j]=j(所有顶点的连通分量编号等于该顶点编号。
当选中边(i,j)时,如果顶点i,j的连通分量编号相同,表示加入后会产生回路,不能加入;否则不会产生回路,可以加入,然后将这两个顶点所在连通分量中所有顶点的连通分量编号改为相同(改为vext[i]或vest[j]均可)。
Kruskal算法代码
typedef struct
{
int u;//边的起始顶点
int v;//边的终止顶点
int w;//边的权值
}Edge;
void Kruskal(MatGraph g)//Kruskal算法
{
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++)//由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.edgess[i][j];
k++;
}
InsertSort(E, g, e);//采用直接插入排序对E数组按权值递增排序
for (i = 0;i < g.n;i++)//初始化辅助数组
vest[i] = i;
k = 1; j = 0;//k表示当前构造生成树的第几条边,初值为1;E中边的下标,初值为0
while (k < g.n)//生成的边数小于n时循环
{
u1 = E[j].u; v1 = E[j].v;//取一条边的两个顶点
sn1 = vest[u1];
sn2 = vest[v1];//分别得到两个顶点所属的集合编号
if (sn1 != sn2)//两个顶点属于不同的集合,该边是最小生成树的一条边
{
printf("(%d,%d):%d
", u1, v1, E[j].w);//输出最小生成树的一条边
k++;//生成边数增1
for (i = 0;i < g.n;i++)//两个集合统一编号
if (vest[i] == sn2)//集合编号为sn2的改为sn1
vest[i] = sn1;
}
j++;//扫面下一条边
}
}
分析Kruskal算法时间复杂度
二层循环,考虑最坏情况,Kruskal算法时间复杂度为O(n^2)
适用图结构
Dijkstra算法适用于计算正权图(边权为正)上的单源最短路,即从单个源点出发,到所有节点的最短路。该算法同时适用于有向图和无向图。
1.4 最短路径
1.4.1 Dijkstra算法求解最短路径
基于上述图结构,求解某个顶点到其他顶点最短路径。(结合dist数组、path数组求解)
Dijkstra算法辅助数据结构
设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。
在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
和求最小生成树的算法一样,Dijkstra算法需要频繁的一次次取一条条边及其权值,所以采用邻接矩阵合适。
用一个一维数组dist[]存放最短路径长度,如dist[j]表示从源点 v->j的最短路径长度,其源点v是默认的。
从源点到其他点的最短路径有n-1条,一条最短路径用一个一维数组path表示。
Dijkstra算法如何解决贪心算法无法求最优解问题?展示算法中解决的代码。
Dijkstra算法的时间复杂度,使用图结构
循环n次(顶点个数)直到所有顶点的最短路径都求出来,又要在循环中循环n次以来选取在U中的顶点且具有求小最短路径长度的顶点,即用了两层循环,考虑最坏情况,时间复杂度为O(n^2).
Dijkstra算法更适用于邻接矩阵结构。
1.4.2 Floyd算法求解最短路径
Floyd算法解决什么问题?
求最短路径。
求无向图中可以删除一些边,使得任意两点的最短路不改变,求这些边能删除的最大的条数。
无向图的最小环。
Floyd算法辅助数据结构
有向图G=(V,E)用邻接矩阵表示。
设置一个二维数组用于存放当前顶点之间的最短路径长度,即分量A[i][j]表示当前i->j的最短路径长度。
用二维数组path保存最短路径,它与当前迭代的次数有关。path[i][j]存放着考察顶点0、1、1....、k之后得到的i->j保存的最短路径中顶点j 的前一个顶点的编号。
Floyd算法优势。
最短路径算法其他算法
贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。 然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共 次,其中 是图的点的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。
1.5 拓扑排序
找一个有向图,并求其对要的拓扑排序序列
1.从有向图中选取一个没有前驱的顶点,并输出之;
2.从有向图中删去此顶点以及所有以它为尾的弧;
3.重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
得到:A B C E D
拓扑排序代码,结构体
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; //找下一个邻接点
}
}
}
拓扑排序伪代码
遍历邻接表
计算每个顶点的入度,存入头结点count成员中;
遍历图顶点
找到一个入度为0的顶点,入栈/队列/数组;
while(栈不为空)
出栈结点v,访问;
遍历v的所有邻接点
{
所有邻接点的入度-1;
若有邻接点入度为0,入栈/队列/数组;
}
拓扑排序如何删除入度为0的结点
使用邻接表
当某个顶点的入度为0时 输出顶点信息
设置栈来存放入度为0的顶点
如何用拓扑排序代码检查一个有向图是否有环路?
拓扑排序的核心就是每次找入度为0的点, 进入输出队列, 然后将与此点相连的节点入度减1, 重复做.
当做n-1 次后还有点没进输出队列, 那么这些点就是环上的, 因为环上的各点入度都为1, 没有0的, 就不能更新。就能说明图是有环路的。
1.6 关键路径
什么叫AOE-网?
AOE 网是在 AOV 网的基础上,其中每一个边都具有各自的权值,是一个有向无环网。其中权值表示活动持续的时间,用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图。
什么是关键路径概念?
关键路径是指有向图中从源点到汇点的最长路径。其中,关键路径中的边叫做关键活动。
什么是关键活动?
关键活动(key activity)指的是:关键路径中的边.
2.PTA实验作业(4分)
2.1 六度空间(2分)
2.1.1 伪代码(贴代码,本题0分)
int BFS( int v)
{
int w
int tail, last
int count = 1, level = 0
int visited[MAXV]//定义存放节点的访问标志的数组/定义顶点访问标记数组
for int x = 0 to MAXV
visited[x] = 0
end for
queue<int>q
visited[v] = 1
q.push(v)
last = v
while q不为空
int j
w = q.front()
q.pop()
for j = 1 to n
if visited[j] == 0 && edges[w][j]未访问过 then
q.push(j)
visited[j] = 1
tail = j
count++
end if
end for
if last == w 到下一层
level++
last = tail
end if
if level == 6 到6层返回
return count
end while
}
2.1.2 提交列表
2.1.3 本题知识点
BFS算法
队列应用
2.2 公路村村通
2.2.1 伪代码(贴代码,本题0分)
伪代码为思路总结,不是简单翻译代码。
int lowcost[MAXV]
int** edges
int n, e
int main()
{
//动态申请二维数组
edges = (int**)malloc(sizeof(int*) * 1001)
int i
for i = 0 to 1000
edges[i] = (int*)malloc(sizeof(int) * 1001)
end for
cin >> n >> e
if e < n - 1
cout << -1 << endl
else
CreateMGraph()
Prim()
end if
return 0
}
void CreateMGraph()//建图
{
int i, j, a, b, c
for i = 1 to n
for j = 1 to n
edges[i][j] = INF //初始化邻接矩阵
end for
end for
for i = 1 to e
cin >> a >> b >> c
edges[a][b] = c
edges[b][a] = c
end for
}
void Prim()
{
int i, j
int min
int mark = -1
int sum = 0
for i = 1 to n
lowcost[i] = edges[1][i]
lowcost[1] = 0
for i = 1 to n
min = INF
for j = 1 to n
if lowcost[j] != 0 && lowcost[j] < min
min = lowcost[j] 更新min的值
mark = j
end if
end for
sum = sum + min
lowcost[mark] = 0 表明k节点已被选了
for j = 1 to n
if edges[mark][j] < lowcost[j]
lowcost[j] = edges[mark][j]
end if
end for
end for
for j = 1 to n
if lowcost[j] != 0
break
end if
end for
if j <= n
cout << -1 << endl
return
end if
cout << sum << endl
}
2.2.2 提交列表
2.2.3 本题知识点
采用Prim算法
动态申请二维数组
邻接矩阵创建、初始化