一、问题描述:
要完成一部电影需要很多环节,如下:
1、 项目启动到确定导演需要 1 个时间,已确定导演到完善细节需要 2 个时间,已经完善细节到开始拍摄需要 2 个时间。
2、 项目启动到确定演员需要 3 个时间,已确定导演到已确定演员需要 1 个时间,已确定演员到开始拍摄需要 2 个时间。
3、 项目启动到完成场地确认需要 5 个时间,完成演员确定到完成场地确认需要 1 个时间,完成场地确认到完善细节需要 1 个时间,完成场地确认到开始拍摄需要 2 个时间。
可以看出,每个环节的时间不同,且有着严格的先后关系,可以同时做多个工作,如何求出从项目启动到开始拍摄的最短时间呢?下面就是解决此问题的办法。
二、解决方法:
此问题很明显是一个寻找关键路径的问题,我们可以建立一个图的模型,将每一个顶点代表每一个环节,用有向边来表示两顶点之间的活动,边的权值就代表所需要的时间。
由此,我们也看画出关于此问题的带权有向图(AOE图):
我们能够很容易的找到关键路径为:项目启动->场地确认->完善细节->开始拍摄。即为:
我们可以计算出最短的时间为8个时间。
虽然对这种整体环节不是很多的情况下,我们能够用肉眼观察到,但是如果所需要的的环节很多,关系也较为复杂的情况时,我们无法较为容易的观察出最短的路径,这时我们自己要设计一个小程序,来解决这种问题,下面就是代码实现。
三、代码实现:
1、代码思路
首先考虑的是对于有向图的表示方法,由于涉及到权值,我们选择用邻接矩阵的方法对有向图的表示,若两顶点之间不连通,则相对应的坐标表示为无限大(代码中表示为一个很大的值),若存在,则对应坐标代表权值。
先将上述的各项环节转换成数字(即为数组下标)这样能更加容易的代码实现:
由此,我们可以得到上述问题的邻接矩阵:
再通过关键路径算法来求得关键路径与最短时间,下面是算法分析。
2、主要算法
首先是设置了一个函数来对邻接矩阵进行初始化,并将边权信息输入进去:
void Initialize()
{
int a,b,c;
for(i = 0;i < maxs;i++)
for(j = 0;j < maxs;j++)
graph[i][j] = maxwt;
printf("请输入边数:");
scanf("%d",&m);
printf("请输入每条边的顶点与权重:\n");
for(i = 0;i < m;i++)
{
scanf("%d%d%d",&a,&b,&c);
graph[a][b] = c;
}
}
由公式:
我们可以利用递归算法求得每个节点的最早时间,存入到e[i]数组中:
void Getearly(int a,int last)
{
if(a == last)
return;
for(int i = 0;i < maxs;i++)
{
if(graph[a][i] < maxwt)
{
if((e[a] + graph[a][i]) > e[i])
{
e[i] = e[a] + graph[a][i];
}
Getearly(i,last);
}
}
}
同理,也能够得到最迟的时间,存入到l[i]数组中:
void Getlater(int a,int first)
{
if(a == first)
return;
for(int i = 0;i < maxs;i++)
{
if(graph[i][a] < maxwt)
{
if(l[a] - graph[i][a] < l[i])
{
l[i] = l[a] - graph[i][a];
}
Getlater(i,first);
}
}
}
得到以上两个关键数组,就从输入的起始点k = first开始,到k = last结束。如果 l[i] - graph[k][i] == e[k],则k–>i 为关键路径,再将i加入到path[i]数组中。由此得到了path[i] 关于路径的数组,再通过数组代表的每一个顶点,与邻接矩阵所代表的权值为时间可以得到关键路径的总时间。
完整代码:
编译程序:Dev-C++ 编译环境:C++ (因为有些语句在C++里面能够运行,而在C语言里面可能会报错,为了方便选择了C++编译环境)
#include<stdio.h>
#include<stdlib.h>
#define maxwt 1000000 //两点不连通时权值为无限大
#define maxs 10 //最大边数
int graph[maxs][maxs]; //全局变量邻接矩阵来表示有向图
int e[maxs]; //存储各个点最早开始的时间的数组
int l[maxs]; //存储各个点最晚开始的时间的数组
int p[maxs]; //记录关键路径
void Initialize();
void Getearly(int a,int last);
void Getlater(int a,int first);
void Getpath();
int i,j,m;
int time = 0;
int main()
{
Initialize(); //初始化邻接矩阵
Getpath();
}
void Initialize()
{
int a,b,c;
for(i = 0;i < maxs;i++)
for(j = 0;j < maxs;j++)
graph[i][j] = maxwt;
printf("请输入边数:");
scanf("%d",&m);
printf("请输入每条边的顶点与权重:\n");
for(i = 0;i < m;i++)
{
scanf("%d%d%d",&a,&b,&c);
graph[a][b] = c;
}
}
void Getpath()
{
int first,last;
int k,f;
printf("请输入开始的点和结束的点:\n");
scanf("%d%d",&first,&last);
for(i = 0;i < maxs;i++)
{
l[i] = maxwt;
e[i] = 0;
p[i] = maxwt;
}
k = first;
p[0] = first;
f = 1;
Getearly(k,last);
k = last;
l[k] = e[k];
Getlater(k,first);
k = first;
while(k != last) // 得到关键路径
{
for(i = 0;i < maxs;i++)
{
if(graph[k][i] != maxwt && (l[i] - graph[k][i] == e[k]))
{
p[f++] = i;
k = i;
break;
}
}
}
printf("关键路径为:\n");
for(i = 0;p[i] != last;i++)
{
printf("%d->",p[i]);
} /*输出关键路径 */
printf("%d\n",p[i]);
for(;i > 0;i--)
{
time = time + graph[p[i-1]][p[i]]; //path所记录的下标对应矩阵的权值进行相加
}
printf("此方案所花费的时间为:%d",time);
}
void Getearly(int a,int last) //得到每个节点的最早时间,存入到e[i]数组中
{
if(a == last)
return;
for(int i = 0;i < maxs;i++)
{
if(graph[a][i] < maxwt)
{
if((e[a] + graph[a][i]) > e[i])
{
e[i] = e[a] + graph[a][i];
}
Getearly(i,last);
}
}
}
void Getlater(int a,int first) //得到每个节点的最晚时间,存入到e[i]数组中
{
if(a == first)
return;
for(int i = 0;i < maxs;i++)
{
if(graph[i][a] < maxwt)
{
if(l[a] - graph[i][a] < l[i])
{
l[i] = l[a] - graph[i][a];
}
Getlater(i,first);
}
}
}
代码初步测试截图:
与初步预测相同。
为了测试代码的正确性,我们把开始的点由0(项目启动)改为1(确定导演)初步预测得到的结果为1(确定导演)->4(确定演员)->5(场地确认)->2(完善细节)->3(开始拍摄)
所用总时间为5个时间。
代码二次测试结果:
测试正确。
四、算法效率分析:
对各个函数的时间复杂度与空间复杂度分析:
先来看 Initialize()函数(邻接矩阵的初始化)与Getpath()函数(得到关键路径)其主体都是用了两个for循环,其临时占用空间大小主要为一个二维数组,数组的大小跟最大边长有关:很容易此函数的时间复杂度为O(n2),空间复杂度为O(n2)。
Getearly()函数与Getlater()函数很类似,都是用了递归来求得每次调用递归时的时间复杂度为O(n),设调用次数为m次,时间复杂度即为O(mn)由于m与n的值都不确定,我们可以近似的看作为O(n2)。而空间复杂度,由于递归都要存储返回信息,而且每个函数中都有一个二维数组所以空间复杂度也为O(n2).
综上,总结为下图:
由此可得,此算法的总体时间复杂度为O(n2),而空间复杂度为O(n2).