题目要求
完成一部电影需要很多环节,具体环节如下:
a) 项目启动到确定导演需要 1 个时间,已确定导演到完善细节需要 2 个时间,已经完善细
节到开始拍摄需要 2 个时间。
b) 项目启动到确定演员需要 3 个时间,已确定导演到已确定演员需要 1 个时间,已确定演
员到开始拍摄需要 2 个时间。
c) 项目启动到完成场地确认需要 5 个时间,完成演员确定到完成场地确认需要 1 个时间,
完成场地确认到完善细节需要 1 个时间,完成场地确认到开始拍摄需要 2 个时间。
请选择合适的结构,设计算法,计算出从项目启动到开始拍摄之间至少需要多少个时间?
要求:
- 请画出完成一部电影的 AOE 图,找出关键路径,计算出从项目启动到开始拍摄之间至
少需要多少个时间。 - 请将你的思路用代码来实现,并输出关键路径,以及计算出从项目启动到开始拍摄之间
至少需要多少个时间。(需要代码和运行结果的截图) - 请分析算法的效率,至少包括时间复杂度和空间复杂度等
解答
1.AOE图等的呈现
2.代码以及截图
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define MVNum 10 //最大顶点数,实际只有6个。
int indegree[MVNum];//统计入度个数的数组。
int Stack1[MVNum];//临时栈
int Stack2[MVNum];//装topo序列
int Queue[MVNum*2];//装最终的关键路径下标与权值
int lowestsumtime=0;//最终最短路径的和
int etv[MVNum],ltv[MVNum];//装事件(顶点)发生最早所需要的时间 和 事件(顶点)发生最迟所需要的时间。 (事件的发生时间:从最初开始 到 某个事件开始发生)
int top1=-1,top2=-1,rear=0,front=0;//和栈与队列相关的信息
typedef int VerTexType; //顶点类型
/*边结点 */
typedef struct ArcNode//边结点
{
int adjvex;//装的是弧头指向顶点的下标
int weight;//弧的权重
struct ArcNode *nextarc;
}ArcNode;
/*顶点结点*/
typedef struct VNode
{
VerTexType data;
char name[20];
ArcNode *firstarc;
}VNode, AdjList[MVNum];
/*图的信息结构体*/
typedef struct INF
{
AdjList vertices; //邻接表
AdjList converse_vertices;//逆邻接表
int vexnum, arcnum; //最大节点数、最大边数
}Graph;
/*创建有向图*/
int CreateDN(Graph *G)
{
int i,j,k;
int v1,v2,weight;
int xb;
char name[20]={0};
ArcNode *p,*p2;
/*输入顶点个数 和边的个数*/
printf("请输入顶点总个数 和 弧的总条数:\n");
scanf("%d%d",&G->vexnum,&G->arcnum);
/*给每一个顶点录入信息,包括下标,和名字字符串*/
printf("请依次输入顶点(0...n-1)下标和已完成的活动名称(字符串)【中间必须只用一个空格隔开】 :\n");
for(i=0;i<G->vexnum;i++)
{
scanf("%d %s",&xb,name);
G->vertices[i].data=xb;//邻接表的表头信息
strcpy(G->vertices[i].name,name);
G->converse_vertices[i].data=xb;//逆邻接表的表头信息
strcpy(G->converse_vertices[i].name,name);
/*为什么要放个NULL? 因为后面 要用到头插法*/
G->vertices[i].firstarc=NULL;
G->converse_vertices[i].firstarc=NULL;
memset(name,'\0',20);//将name数组全部用'\0'覆盖,也可以不用。
}
printf("请连续依次输入两个顶点v1,v2与两顶点间的权值(三者以空格分开,其中v1—>v2)。\n");
for(j=0;j<G->arcnum;j++)
{
scanf("%d%d%d",&v1,&v2,&weight);
//创造结点,并让两个边指针分别指向两个不同结点。
p=(ArcNode*)malloc(sizeof(ArcNode));
p2=(ArcNode*)malloc(sizeof(ArcNode));
//给边结点成员赋值
p->adjvex=v2;//为了邻接表而准备
p->weight=weight;
p2->adjvex=v1;//为了逆邻接表而准备,所以不用 给p2指向的结点赋权重。
/*头插法操作。这里了体现了为什么顶点的data值是顶点的下标(从0开始),这样就不需要用自己写LocateVex函数定位顶点了*/
p->nextarc=G->vertices[v1].firstarc;
G->vertices[v1].firstarc=p;
p2->nextarc=G->converse_vertices[v2].firstarc;
G->converse_vertices[v2].firstarc=p2;
}
}
/*获取拓扑序列,并把topo序列存入栈Stack2,并获取最早事件发生数组etv[]*/
void getTopoStack2(Graph G)
{
int i;
int cur;
ArcNode *p;
/*首先把入度为0的顶点装入临时栈Stack1*/
for(i=0;i<G.vexnum;i++)
{
if(indegree[i]==0)
{
Stack1[++top1]=i;
indegree[i]--;
}
}
//初始化etv
memset(etv,0,sizeof(int)*MVNum);
while(top1!=-1)//栈不为空
{
cur=Stack1[top1--];//出栈,顶点信息暂时用cur保存
Stack2[++top2]=cur;//将topo序列装入栈Stack2,以后好使用它获取 事件(顶点)发生最迟时间数组 ltv[]。这里可以用逆向邻接表吗?
p=G.vertices[cur].firstarc;
while(p!=NULL)
{
indegree[p->adjvex]--;
if(indegree[p->adjvex]==0)
{
Stack1[++top1]=p->adjvex;
}
/*这里用到了贪心思想。etv[cur]:向量(起点下标0,终点下标cur),事件0发生 到 事件cur发生 所需的最早发生时间。
下面if语句理解:(起点下标0,终点下标cur)+(起点下标cur,终点p->adjvex) > (起点下标0,终点p->adjvex) */
if(etv[cur]+p->weight > etv[p->adjvex])
{
etv[p->adjvex]=etv[cur]+p->weight;
}
p=p->nextarc;
}
}
}
/*获取关键路径*/
void getCriticalPath(Graph G)
{
int i,xb;
int gettop;
ArcNode *p;
int ete,lte;
/*初始化 ltv[]数组,必须都是拓扑序列最后一个值(最后发生的事件),因为if语句*/
for(i=0;i<=top2;i++)
{
ltv[i]=etv[Stack2[top2]];//初始化为最大etv【最后一个结点】
}
/*获取ltv[]数组*/
while(top2!=-1)//栈Stack2不为空
{
gettop=Stack2[top2--];
p=G.vertices[gettop].firstarc;
while(p!=NULL)
{
if(ltv[gettop] > ltv[p->adjvex]-p->weight)
{
ltv[gettop]= ltv[p->adjvex]-p->weight;
}
p=p->nextarc;
}
}
/*把 事件1、关键活动的权值、事件2 装入到队列里面,方便以后打印直观 */
for(i=0;i<G.vexnum;i++)
{
xb=Stack2[i];
ete=etv[xb];//公式一:活动(边)最早发生时间 = 事件1(顶点)最早发生时间。
p=G.vertices[xb].firstarc;
while(p!=NULL)
{
lte=ltv[p->adjvex]-p->weight;//公式二:活动(边)最迟发生时间 = 事件2(顶点)最迟发生时间 - 权重(事件1-->事件2)
/*利用这个条件,可以把 事件1、关键活动的权值、事件2都找到*/
if(ete==lte)
{
/*存入队列以后好直观输入*/
Queue[rear++]=xb;
Queue[rear++]=p->weight;
Queue[rear++]=p->adjvex;
lowestsumtime+=p->weight;//最后输出最短消耗时间要用到。
}
p=p->nextarc;
}
}
}
int main()
{
int i,cnt;
Graph G;
int xb1,xb2,timecost;//最后打印所需
CreateDN(&G);
//统计每个顶点的入度。
for(i=0;i<G.vexnum;i++)
{
cnt=0;
ArcNode* p=G.converse_vertices[i].firstarc;
while(p!=NULL)
{
cnt++;
p=p->nextarc;
}
indegree[i]=cnt;
}
//获得拓扑序列
getTopoStack2(G);
//获得关键路径
getCriticalPath(G);
/*把结果打印出来(可以不用)*/
printf("关键路径为:");
xb1=Queue[front++];
timecost=Queue[front++];
xb2=Queue[front++];
printf("%s--%d-->%s",G.vertices[xb1].name,timecost,G.vertices[xb2].name);
for(i=0;front<rear;i++)
{
front++;
timecost=Queue[front++];
xb2=Queue[front++];
printf("--%d-->%s",timecost,G.vertices[xb2].name);
}
printf("\n从项目启动到开始拍摄之间至少需要时间数为:%d\n",lowestsumtime) ;
return 0;
}
/*测试数据入下*/
/*
6 10
0 项目启动
1 完成导演确认
2 完成完善细节
3 开始拍摄
4 完成确定演员
5 完成场地确认
0 1 1
1 2 2
2 3 2
1 4 1
0 4 3
4 3 2
0 5 5
4 5 1
5 2 1
5 3 2
*/
注意:此程序编译环境是DevC++。
3.效率分析
时间复杂度分析
该程序最花费时间的三个步骤是 1.求拓扑序列 2.求 件最晚发生时间ltv[] 数组3.将关键路径装入队列Queue[]。因为只有他们都用到了双层循环嵌套结构,其他的最多就是单层循环。如图:
分析一下,假如一个AOE网有n个顶点,e条弧。那么上面的三个步骤都是外层循环把每个顶点都遍历一遍,时间复杂度是O(n),内层循环把每一条弧都遍历了一遍,时间复杂度是O(e)。所以整个程序时间复杂度应该是O(n+e)。
空间复杂度分析
呃,我不确定。教材上提到过,如果用邻接表和逆邻接表存储AOE网的顶点和弧,假设一个AOE网有n个顶点和e条弧,那么空间复杂度为O(n+e)。但是我感觉这些存储空间不可避免,多余的空间才算(就像简单排序算法的空间复杂度是O(1),但是还是需要O(n)的空间存放元素。)。这个程序用到了Stack1作为临时栈存放数据元素,所以时间复杂度是O(n)。
回顾几个问题
1.获得ete的公式是什么?获得lte的公式是什么?
2.etv[]与ltv[]是怎么得到的?
3.两个if语句的理解?
if(etv[cur]+p->weight > etv[p->adjvex])
{
etv[p->adjvex]=etv[cur]+p->weight;
}
与
if(ltv[gettop] > ltv[p->adjvex] - p->weight)
{
ltv[gettop]= ltv[p->adjvex] - p->weight;
}
4. 边结点中adjvex装的是什么? 弧头一方的顶点下标。
5. 为什么顶点结点的data用来装的是下标值?因为头插法需要定位顶点,这样就避免了自己写LocateVex函数定位顶点了。
6.可以用逆邻接表来实现其他功能吗?我使用逆邻接表主要是为了获取每个顶点的入度大小。