0.PTA得分截图
1.本周学习总结
1.1 总结图内容
图的基本概念
1.有向图:
- 有向图称由顶点集和弧集构成的图。“弧”是有方向的边,用尖括号表示。
- 若存在一条边(i,j),则称顶点i和顶点j互为邻接点。
- 每两个顶点之间都存在着一条边,称为完全无向图, 包含有n(n-1)/2条边。
- 若从顶点i到顶点j有路径,则称顶点i和j是连通的。若图中任意两个顶点都连通称为连通图,否则称为非连通图。无向图G中的极大连通子图称为连通分量。
2.无向图:
-
没有方向边。边用圆括号表示。
-
存在一条边<i,j>,则称此边是顶点i的一条出边,同时也是顶点j的一条入边;称顶点i 和顶点j 互为邻接点。
-
每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有n(n-1)条边。
-
若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。 否则,其各个强连通子图称作它的强连通分量。
3.带权图(网)
-
图中每一条边都可以附有一个对应的数值,这种与边相关的数值称为权。
-
边上带有权的图称为带权图,也称作网。
图存储结构
邻接矩阵
1.有向图
设顶点i,j,若有一条边从i指向j,则将edges[i][j]赋为1,否则为0
2.无向图
设顶点i,j,若有一条边连接i和j,则将edges[i][j]和edges[j][i]赋为1,否则为0
3.带权图
设顶点i,j,若有一条边从i指向j或连接i和j,则按照有向图或无向图的方法将相应权值赋给二维数组,若i=j,则相应值为0,若i!=j,且不相连,则相应值为无穷大
4.结构体定义
typedef struct //图的定义
{ int edges[MAXV][MAXV]; //邻接矩阵
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //存放顶点信息
} MatGraph;
MatGraph g;//声明邻接矩阵存储的图
5.创建图
void CreateMGraph(MGraph &g, int n, int e)
{
int i, j;
g.n = n;
g.e = e;
int a, b;
for (i = 1;i <= n;i++)
{
for (j = 1;j <= n;j++)
{
g.edges[i][j] = 0;
}
}
for (i = 0;i < e;i++)
{
cin >> a >> b;
g.edges[a][b] = g.edges[b][a] = 1;
}
}
- 时间复杂度为O(n²)
- 优点:可以快速查找到两个顶点之间有无边
- 缺点:浪费空间
- 若顶点数量过多,可以将edges[][]换为**edges。
邻接表
用链存储,将每个顶点所连接的顶点构成一条链,用一维数组将每个顶点存储起来,即将所有链集合
1.有向图
2.无向图
3.带权图
4.结构体定义
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
5.创建图
void CreateAdj(AdjGraph *&G, int n, int e) //创建图邻接表
{
int i, j, a, b;
ArcNode *p;
G = new AdjGraph;
for (i = 1;i < n;i++)
{
G->adjlist[i].data = i;
G->adjlist[i].firstarc = NULL;
}
for (i = 1;i <= e;i++)
{
cin >> a >> b;
ArcNode *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;*/无向图
}
}
- 时间复杂度O(n+e)
- 优点:空间利用率较高
- 缺点:不利于判断顶点之间是否有边
邻接表转邻接矩阵
void ListToMat(ALGraph *G,MGraph &g)
{ int i,j,n=G->n;ArcNode *p;
for (i=0;i<n;i++)
{ p=G->adjlist[i].firstarc;
while (p!=NULL)
{ g.edges[i][p->adjvex]=1;
p=p->nextarc;
}
}
g.n=n;g.e=G->e;
}
图遍历及应用
深度优先遍历
1.过程
- 从图中某个初始顶点v出发,首先访问初始顶点v。
- 选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
2.邻接矩阵的深度优先遍历
void DFS(MGraph g, int v)//深度遍历
{
visited[v] = 1;
if (!flag)
{
flag = 1;
cout << v;
}
else cout << " " << v;
for (int i = 1;i <= g.n;i++)
{
if (g.edges[v][i] == 1 && (!visited[i]))
DFS(g, i);
}
}
3.邻接表的深度优先遍历
void DFS(AdjGraph *G, int v)//v节点开始深度遍历
{
ArcNode *p;
visited[v] = 1; //置已访问标记
if(!flag)
{
cout<<v;
flag=1;
}
else cout<<" "<<v;
p = G->adjlist[v].firstarc;
while (p != NULL)
{
if (visited[p->adjvex] == 0) DFS(G, p->adjvex);
p = p->nextarc;
}
}
4.非连通图
图还存在非连通的可能,所以考虑该情况的代码
void DFSTraverse(Graph G) {
// 对非连通图 G 作深度优先遍历。
for (v=0; v<G.vexnum; ++v)
visited[v] = FALSE; // 访问标志数组初始化
for (v=0; v<G.vexnum; ++v)
if (!visited[v]) DFS(G,v); // 对尚未访问的顶点调用DFS
}
广度优先遍历
1.过程
- 访问初始点v,接着访问v的所有未被访问过的邻接点。
- 按照次序访问每一个顶点的所有未被访问过的邻接点。
- 依次类推,直到图中所有顶点都被访问过为止。
2.邻接矩阵的广度优先遍历
void BFS(MGraph g, int v)//广度遍历
{
int num, front, rear;
int Q[MAXV];
front = rear = -1;
cout << v;
visited[v] = 1;
Q[++rear] = v;
while (front != rear)
{
v = Q[++front];
for (int i = 1;i <= g.n;i++)
{
if (g.edges[v][i] == 1 && (!visited[i]))
{
cout << " " << i;
visited[i] = 1;
Q[++rear] = i;
}
}
}
}
3.邻接表的广度优先遍历
void BFS(AdjGraph *G, int v) //v节点开始广度遍历
{
ArcNode *p;
queue<int>qu;
visited[v] = 1;
cout << v;
qu.push(v);
while (!qu.empty())
{
v = qu.front();
qu.pop();
p = G->adjlist[v].firstarc;
while (p != NULL)
{
if (visited[p->adjvex] == 0)
{
cout << " " << p->adjvex;
visited[p->adjvex] = 1;
qu.push(p->adjvex);
}
p = p->nextarc;
}
}
}
4.非连通图
void BFS1(AdjGraph *G)
{ int i;
for (i=0;i<G->n;i++) //遍历所有未访问过的顶点
if (visited[i]==0)
BFS(G,i);
}
判断图是否连通
1.过程
- 采用某种遍历方式来判断无向图G是否连通。这里用深度优先遍历方法,先给visited[]数组(为全局变量)置初值0,然后从0顶点开始遍历该图。
- 在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通。
2.代码
int visited[MAXV];
bool Connect(AdjGraph *G) //判断无向图G的连通性
{ int i;
bool flag=true;
for (i=0;i<G->n;i++) //visited数组置初值
visited[i]=0;
DFS(G,0);
for (i=0;i<G->n;i++)
if (visited[i]==0)
{ flag=false;
break;
}
return flag;
}
找到一条简单路径
1.过程
- 采用深度优先遍历的方法。
- 增加path[i],存放路径。
- 递归函数添加形参d,表示目前递归深度。path[d]=图结点
- 当从顶点u遍历到顶点v后,输出path并返回。
2.代码
void FindaPath(AGraph *G,int u,int v,int path[],int d)
{ //d表示path中的路径长度,初始为-1
int w,i; ArcNode *p;
visited[u]=1;
d++; path[d]=u; //路径长度d增1,顶点u加入到路径中
if (u==v) //找到一条路径后输出并返回
{ printf("一条简单路径为:");
for (i=0;i<=d;i++) printf("%d ",path[i]);
printf("
");
return; //找到一条路径后返回
}
p=G->adjlist[u].firstarc; //p指向顶点u的第一个相邻点
while (p!=NULL)
{ w=p->adjvex; //相邻点的编号为w
if (visited[w]==0)
FindaPath(G,w,v,path,d);
p=p->nextarc; //p指向顶点u的下一个相邻点
}
}
找两个顶点中最短路径
1.过程
- 采用广度遍历
- 利用parent记录前驱
- 输出时逆向,利用parent关系输出
2.代码
typedef struct
{
int data; //顶点编号
int parent; //前一个顶点的位置
} QUERE; //非环形队列类型
void ShortPath(ALGraph *G,int u,int v)
{
//输出从顶点u到顶点v的最短逆路径
ArcNode *p;
int w,i;
QUERE qu[MAXV]; //非环形队列
int front=-1,rear=-1; //队列的头、尾指针
int visited[MAXV];
for (i=0; i<G->n; i++) //访问标记置初值0
visited[i]=0;
rear++; //顶点u进队
qu[rear].data=u;
qu[rear].parent=-1;
visited[u]=1;
while (front!=rear) //队不空循环
{
front++; //出队顶点w
w=qu[front].data;
if (w==v) //找到v时输出路径之逆并退出
{
i=front; //通过队列输出逆路径
while (qu[i].parent!=-1)
{
printf("%2d ",qu[i].data);
i=qu[i].parent;
}
printf("%2d
",qu[i].data);
break;
}
p=G->adjlist[w].firstarc; //找w的第一个邻接点
while (p!=NULL)
{
if (visited[p->adjvex]==0)
{
visited[p->adjvex]=1;
rear++; //将w的未访问过的邻接点进队
qu[rear].data=p->adjvex;
qu[rear].parent=front;
}
p=p->nextarc; //找w的下一个邻接点
}
}
}
应用
六度空间
伪代码
int BFS(int v, int N, int ** snap)
{
将第一个顶点v入队
最后顶点last=v
while 队列不空
出队顶点temp
for j=1toN
if temp和j之间有边且未访问过
入队j节点
记录访问过j顶点
该六度空间内人数+1
记录该节点为本层最后一个顶点tail=j
end if
end for
if 出队顶点等于该层最后一个顶点
层数加一
记录最后一个顶点last=tail
end if
if 层数等于6
结束循环
end if
end while
返回六度空间内的人数
}
代码:
最小生成树相关算法及应用
1.概念
1.一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。不能回路。
2.由深度优先遍历得到的生成树称为深度优先生成树。由广度优先遍历得到的生成树称为广度优先生成树。
3.对于带权连通图G ,n个顶点,n-1条边。根据深度遍历或广度遍历生成生成树,树不唯一。其中权值之和最小的生成树称为图的最小生成树。
4.非连通图:需多次调用遍历过程。每个连通分量中的顶点集和遍历时走过的边一起构成一棵生成树。所有连通分量的生成树组成非连通图的生成森林。
普里姆算法(Prim)
1.过程:
- 初始化U={v}。v到其他顶点的所有边为候选边;
- 从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
- 考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边。
- 重复后两个步骤骤n-1次,使得其他n-1个顶点被加入到U中
2.具体设置2个辅助数组。 - closest[i]:最小生成树的边依附在U中顶点编号。
- lowcost[i]表示顶点i(i ∈ V-U)到U中顶点的边权重,取最小权重的顶点k加入U。并规定lowcost[k]=0表示这个顶点在U中
- (closest[k],k)构造最小生成树一条边。
3.代码
#define INF 32767 //INF表示∞
void Prim(MGraph g,int v)
{ int lowcost[MAXV],min,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
",closest[k],k,min);
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;
}
}
}
4.贪心算法:
- 算法原理:以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪心法不要回溯。
- 算法优点:因为省去了为寻找解而穷尽所有可能所必须耗费的大量时间,因此算法效率高。
- 贪心算法的精神就是“只顾如何获得眼前最大的利益”,有时不一定是最优解。
克鲁斯卡尔算法(Kruskal)
1.过程
- 置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
- 将图G中的边按权值从小到大的顺序依次选取: 若选取的边未使生成树T形成回路,则加入TE;否则舍弃,直到TE中包含(n-1)条边为止。
2.类型
typedef struct
{ int u; //边的起始顶点
int v; //边的终止顶点
int w; //边的权值
} Edge;
Edge E[MAXV];
3.代码
void Kruskal(AdjGraph *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,邻接表
{ p=g->adjlist[i].firstarc;
while(p!=NULL)
{ E[k].u=i;E[k].v=p->adjvex;
E[k].w=p->weight;
k++; p=p->nextarc;
}
}
Sort(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
",u1,v1,E[j].w);
k++; //生成边数增1
for (i=0;i<g.n;i++) //两个集合统一编号
if (vset[i]==sn2) //集合编号为sn2的改为sn1
vset[i]=sn1;
}
j++; //扫描下一条边
}
}
4.采用并查集的方法
void Kruskal(AdjGraph *g)
{ int i,j,k,u1,v1,sn1,sn2;
UFSTree t[MAXSize];//并查集,树结构
ArcNode *p;
Edge E[MAXSize];
k=1; //e数组的下标从1开始计
for (i=0;i<g.n;i++) //由g产生的边集E
{ p=g->adjlist[i].firstarc;
while(p!=NULL)
{ E[k].u=i;E[k].v=p->adjvex;
E[k].w=p->weight;
k++; p=p->nextarc;
}
HeapSort(E,g.e); //采用堆排序对E数组按权值递增排序
MAKE_SET(t,g.n); //初始化并查集树t
k=1; //k表示当前构造生成树的第几条边,初值为1
j=1; //E中边的下标,初值为1
while (k<g.n) //生成的边数为n-1
{ 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
",u1,v1,E[j].w);
k++; //生成边数增1
UNION(t,u1,v1);//将u1和v1两个顶点合并
}
j++; //扫描下一条边
}
}
本算法的时间复杂度是O(elog2e)。由于它与n无关,只与e有关,所以克鲁斯卡尔算法适合于稀疏图
5.比较
- 普里姆算法:O(n2)、适用于稠密图,克鲁斯卡尔算法:O(eloge)、适用于稀疏图
- 实现普里姆算法,用邻接矩阵。实现克鲁斯卡尔算法,用邻接表。
应用
1.伪代码
用vest数组作为集合辅助数组
while k不等于顶点数n
遍历寻找最小边
if 找到最小边权值为INF
非连通 输出-1
end if
if 该边两顶点集合不一
并查集改为同一集合
ans加上该边权值
记录边个数+1
end if
将该边权值修正为INF
end while
2.代码
最短路径相关算法及应用
狄克斯特拉(Dijkstra)算法
1.单源最短路径问题:Dijkstra算法
- S={入选顶点集合,初值V0},T={未选顶点集合}。
- 若存在<V0,Vi>,距离值为<V0,Vi>弧上的权值
- 若不存在<V0,Vi>,距离值为∞
- (1)从T中选取一个其距离值为最小的顶点W, 加入S
- (2)S中加入顶点w后,对T中顶点的距离值进行修改:重复上述步骤1,直到S中包含所有顶点,即S=V为止。
- (3)重复上述步骤1,直到S中包含所有顶点,即S=V为止。
2.最短路径证明
- 采用邻接矩阵存储
- 数组dist[]:源点V0到每个终点的最短路径长度。
- 数组path[]:最短路径序列的前一顶点的序号;初值或无路径用-1表示
- 数组s[]:表示最短路径顶点集合S。
- 算法过程
3.代码
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; //s[]置空
if (g.edges[v][i]<INF) //路径初始化
path[i]=v; //顶点v到i有边时
else
path[i]=-1; //顶点v到i没边时
}
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;
}
}
Dispath(dist,path,s,g.n,v); //输出最短路径
}
4.缺点及其他:
- 不适用带负权值的带权图求单源最短路径。
- 不适用求最长路径长度:最短路径长度是递增,顶点u加入S后,不会再修改源点v到u的最短路径长度
- 求每一对顶点之间的最短路径时,每次以一个顶点为源点,重复执行Dijkstra算法n次。时间复杂度为O(n3)。也可以用弗洛伊德(Floyd)算法:时间复杂性也是O(n3),但形式上简单些。
弗洛伊德(Floyd)算法
1.过程:
- 有向图G=(V,E)采用邻接矩阵存储
- 二维数组A用于存放当前顶点之间的最短路径长度,分量A[i][j]表示当前顶点i到顶点j的最短路径长度。
- 递推产生一个矩阵序列A0,A1,…,Ak,…,An-1。Ak+1[i][j]表示从顶点i到顶点j的路径上所经过的顶点编号k+1的最短路径长度。
2.代码
void Floyd(MatGraph 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 //i和j顶点之间没有一条边时
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]=k; //修改经过顶点k
}
}
}
应用
1.伪代码
建图
定义将结构体edges为距离,cost为花费资金
定义dist数组为到首顶点距离,costs数组为花费资金,s数组记录是否访问过
初始化dist,costs,s数组
for 1 to n
找到最短路径,记录距离、花费及下标
相应s数组值修改为1
for 0 to n
if 未访问且路径长小于之前的最短路径
更新最短路径及最少花费
dist[t] = min1 + g.edges[k][t];
costs[t] = min2 + g.cost[k][t];
else if 未被访问,路径长与之前相等且花费少
更新最少花费
end for
end for
输出路径长及花费
2.代码
拓扑排序、关键路径
拓扑排序
1.在一个有向无环图中找一个拓扑序列的过程称为拓扑排序。条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
2.过程 - 从有向图中选取一个没有前驱的顶点,并输出之;
- 从有向图中删去此顶点以及所有以它为尾的弧;
- 重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
3.结构体
typedef struct //表头节点类型
{ vertex data; //顶点信息
int count; //存放顶点入度
ArcNode *firstarc; //指向第一条弧
} VNode;
4.代码
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.基本概念
- 用顶点表示事件,用有向边e表示活动,边的权c(e)表示活动持续时间。是带权的有向无环图
- 整个工程完成的时间为:从有向图的源点到汇点的最长路径。又叫关键路径(critical path)
- “关键活动(key activity)”指的是:关键路径中的边
2.AOE网关键词 - AOE网——带权的有向无环图
- 顶点--事件或状态
- 弧(有向边)--活动及发生的先后关系
- 权--活动持续的时间
- 起点--入度为0的顶点(只有一个)
- 终点--出度为0的顶点(只有一个)
3.过程 - 事件的最早开始和最迟开始时间
事件v最早开始时间ve(v):v作为源点事件最早开始时间为0。
v为源点事件最早开始时间一定是前驱事件x,y,z已完成。 - 事件v的最迟开始时间vl(v):定义在不影响整个工程进度的前提下,事件v必须发生的时间称为v的最迟开始时间
4.活动:边 - 活动a(边)的最早开始时间e(a)指该活动起点x事件的最早开始时间,即:e(a)=ve(x)
- 活动a的最迟开始时间l(a)指该活动终点y事件的最迟开始时间与该活动所需时间之差,即:l(a)=vl(y)-c
- 关键活动:d(a)=l(a)-e(a),若d(a)为0,则称活动a为关键活动。
- 关键活动不存在富余时间,适当增加对关键活动的投资,减少关键活动的持续时间,缩短工程工期。
5.例题
试对图所示的AOE-网:
① 求这个工程最早可能在什么时间结束;
② 求每个活动的最早开始时间和最迟开始时间;表格表示。
③ 确定哪些活动是关键活动。
1.2.谈谈你对图的认识及学习体会。
对于图的学习时间也比较长,但感觉图的内容很多,而且比较复杂。虽说相较于树来说,递归算法比较少,但算法比较多,刚学的时候一时间难以分清楚,在写代码的时候也不能很准确地完成,总是需要看课件。其实感觉这些算法比较固定,就像是一个模板,题目有浅有深,浅显的题目,直接套用算法,复杂的题目,在算法的基础之上拓展出满足题意的代码即可。其实仔细想想,大的知识点也就几个,缺少的是仔细钻研和总结。实验课上,看同学们集思广益,有各种巧妙的算法,不禁有些惭愧。五一假期没有规划好学习的时间,感觉理解还是很浅显,之后还要继续学习相关内容。
2阅读代码
2.1题目及解题代码
2.1.1该题的设计思路
时间复杂度:O(n²)
空间复杂度:O(n)
2.1.2该题的伪代码
定义flag判断数据走向
逻辑处理是一样的,x,y的上限值是相反的
for 0 to m+n
判断序列走向
找到该次序列起点
循环将序列存入vector容器
改变flag,下次反向
end for
返回该序列
2.1.3运行结果
2.1.4分析该题目解题优势及难点
优势:将两种情况融合在了一起解题,还有将不同方向的情况分开来讲。这样使代码更精简,时间和空间上也节约了不少。
难点:不能一眼看出来各个点之间以及输出序列之间的关系,需要仔细分析。而且将两种情况结合也是比较难的。
2.2题目及解题代码
2.2.1该题的设计思路
1.图中一个节点可以拥有任意数量的邻接点。为了避免在复制时陷入死循环,需要以某种方式跟踪已经复制的节点。
2.从给定节点开始遍历图。使用used数组存放已被访问过的节点
3.如果当前访问的节点不在used中,创建他的克隆节点存进used中
4.递归调用每个节点的邻接点。每个节点递归调用的次数等于邻接点的数量,每一次调用返回其对应邻接点的克隆节点,最终返回这些克隆邻接点的列表,将其放入对应克隆节点的邻接表中。这样就可以克隆给定的节点和其邻接点。
时间复杂度:O(n)
空间复杂度:O(n)
2.2.2伪代码
如果是空指针,返回空
若该节点已拷贝,返回该节点指针
创建拷贝节点
将拷贝后的指针放入used数组used[node->val]=p;
for 0 to 邻接点数量,将该节点的邻接节点放入拷贝节点邻接数组
递归实现每个节点的更新
返回该节点
2.2.3运行结果
2.2.4分析该题目解题优势及难点
优势:典型的深度优先遍历,不过是换了一种“克隆”方式,加上了创建节点等
难点:本题题意乍一看有点不知所云,感觉就是把输入的东西输出,实际上就是考查图的遍历的,容易被题目忽悠住。
2.3题目及解题代码
2.3.1该题的设计思路
使用广度优先搜索的方法得到从 start 开始能够到达的所有位置,如果其中某个位置对应的元素值为 0,那么就返回 True。
具体的将start加入队列。每次搜索过程中,取出队首的节点 u,它可以到达的位置为 u + arr[u] 和 u - arr[u]。
如果某个位置落在数组的下标范围 [0, len(arr)) 内,并且没有被搜索过,则将该位置加入队尾。只要搜索到一个对应元素值为 0 的位置,我们就返回 True。在搜索结束后,如果仍然没有找到符合要求的位置,我们就返回 False。
时间复杂度:O(n)
空间复杂度:O(n)
2.3.2伪代码
若初始节点为0,直接返回true
定义vector数组
定义队列q
初始节点入队,并记录
while 队不空
出队队头
if 跳跃后节点:后跳下标小于n,前跳下标大于等于0,且节点未访问过
if 节点值为0 返回true end if
入队节点,记录节点
end if
end while
返回 false
2.3.3运行结果
2.3.4分析该题目解题优势及难点
优势:巧妙运用广度优先搜索解决问题,将数组的问题转化为类似于图的问题
难点:有经验的话这题看来就是深度或广度优先搜索问题,但不熟练的话,刚开始会不知道如何下手