首先简单介绍一下问题的描述
一个工程Project,其包含
l
l
个项目,即Work Package(WP),这个工程用WPs表示,则:
{
wp1,wp2,...,wpl
w
p
1
,
w
p
2
,
.
.
.
,
w
p
l
}
其中
wpi
w
p
i
有两个属性,
ei
e
i
和
depi
d
e
p
i
。
ei
e
i
表示要完成这个项目需要花费的人力(efforts),单位是日/人;
depi
d
e
p
i
表示这个项目的先决项目(predecessors),先决的意思是说只有当
depi
d
e
p
i
中的项目都被完成后,这个项目才能开始动工(很像先修课和后修课的关系),显然,
depi
d
e
p
i
是一个集合。这三者的数学描述如下:
E=
E
=
{
e1,e2,...,el
e
1
,
e
2
,
.
.
.
,
e
l
}
Dep=
D
e
p
=
{
dep1,dep2,...,depl
d
e
p
1
,
d
e
p
2
,
.
.
.
,
d
e
p
l
}
depi=
d
e
p
i
=
{
wpj,...,wpk
w
p
j
,
.
.
.
,
w
p
k
}
注:
depi
d
e
p
i
中wp的个数不确定,还有可能为空。
总共有n个工作人员(staff),他们将会被分配到m个团队(team)中,分配方式不固定,所以,对于不同的团队而言,他们的人员数目可能不同。第
i
i
个团队的人员数(capacity,容量)用表示:
C=
C
=
{
c1,c2,...,cm
c
1
,
c
2
,
.
.
.
,
c
m
}
要求,
ci>0
c
i
>
0
此外,还有几点说明:
- 1个wp只能由1个team负责
- 1个team只能完成手中的wp后才能接手其他的wp
- 不要求每个team都得完成至少1个wp,有些team可以从头至尾不接手,而有些team可以接手多个
- 为了简化问题,假设每一个staff的效率都是一样的
- 最后是要求得这样一个解决方案,使得最终完成所有wp的总用时最短
这道题或许可以套用大学生程序设计竞赛中的一些思路来进行解答,不过我没有去写相应的代码,读者可以自己尝试一下。我要记录的是用启发式优化算法进行的作答方法。
思路
这个方法涉及协同演化的思想和遗传算法的算子计算。要解决这样的一个问题,首先我们可以设定这样两个种群,WPO 和 TC。
WPO种群 由许多解决方案构成,每个解决方案的染色体(chromosome)是由一串数字组成的,数字范围从
0
0
到(假设有
l
l
个),分别代表
wp
w
p
的序号
比如下边这个染色体:
Distributing Order(执行的顺序,即染色体的角标+1) | 1st | 2nd | 3rd | … | lth |
---|---|---|---|---|---|
Work Package ID(wp的序号) | 3 3 | l l | … |
它的意思是,按照先来先服务的原则,按顺序依次解决wp,即先动工项目3,如果此时此刻项目2可以被执行(即它的先决wp均以完成)且有可供选择的团队(即该团队目前无任务)时,可以同时执行2,接下来的项目也是这样;但是,后边的项目只能在前边的所有项目均开工(包括已完成)下才能被考虑,即如果项目2没有开工,则项目l和之后的项目也就不会被考虑了。
TC种群 也是由许多解决方案构成,每个解决方案的染色体(chromosome)也是由一串数字组成,数字范围从 0 0 到(假设有 m m 个),分别代表 team t e a m 的序号,比如下边这个染色体:
Staff No.(员工序号,即染色体的角标+1) | s1 s 1 | s2 s 2 | s3 s 3 | … | sn s n |
---|---|---|---|---|---|
Assigned To Team No.(team的序号) | 2 2 | m m | … |
它的意思是,员工1号被分配到第2组,员工2号被分配到第4组,员工3号被分配到第m组,… ,员工n号被分配到第1组,这样,通过简单的统计便可以知道每个组有多少位员工了。
结合这样的两个种群我们便能够对这个问题进行操作求解了。考虑到遗传算法的特点,它能够产生很多序列(当然对于每个序列我们都得对其进行检查看是否符合约束),对于固定的WPO和TC中的某个解决方案的组合,我们可以将处理方法简单化,即规定,根据先来先服务的原则,用拥有最多员工数的且暂时没有任务的团队负责下一个可以被执行的项目。这样的简化原则上是不会对结果造成影响的。
整个问题中最核心的应该就是“对于固定WPO和TC中的某个解决方案的组合,得到对应的适应度fitness,作为之后演化的重要参考”。下面,我们举个例子,手动模拟一下这个核心操作。
有6个wp,5个team,10个staff,每个wp的effort见下表,
wp No. | 0 0 | 2 2 | 4 4 | |||
---|---|---|---|---|---|---|
effort of wp | 8 8 | 11 11 | 2 2 | 5 5 |
wp之间的约束见下表,
wp No. | 1 1 | 3 3 | 5 5 | |||
---|---|---|---|---|---|---|
predecessors of wp | 0 和 5 | 4 和 3 | 2 |
wp之间约束的直白的图如下:
给定的WPO的一个解决方案的染色体为 3 4 0 2 5 1:
Distributing Order | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
Work Package ID | 3 | 4 | 0 | 2 | 5 | 1 |
给定的TC的一个解决方案的染色体为 0 1 2 1 3 4 0 4 2 2:
staff ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
Assigned to team No. | 0 | 1 | 2 | 1 | 3 | 4 | 0 | 4 | 2 | 2 |
对TC的这个解决方案进行简单的统计后,得到每个团队对应的员工人数,若根据员工数进行降序排序,则可以得到:
team No. | 2 | 1 | 0 | 4 | 3 |
---|---|---|---|---|---|
capacity of the team | 3 | 2 | 2 | 2 | 1 |
按照WPO解决方案的染色体的顺序和TC解决方案的染色体顺序,我们应该是先用执行
wp3
w
p
3
,完成
wp3
w
p
3
将耗费 2/3=0.667 个时间单位,也意味着
team2
t
e
a
m
2
在0.667个时间单位后将再次变成可利用的;同时,我们发现下一个wp,即
wp4
w
p
4
,它没有约束wp,因此也可以被执行,此时我们应该用
team1
t
e
a
m
1
执行
wp4
w
p
4
,完成
wp4
w
p
4
将耗费 3/2=1.5 个时间单位,也意味着
team1
t
e
a
m
1
在1.5个时间单位后将再次变成可利用的;还有,下一个wp,即
wp0
w
p
0
,它也没有约束wp,因此也可以被执行,此时我们应该用
team0
t
e
a
m
0
执行
wp0
w
p
0
,完成
wp0
w
p
0
将耗费 8/2=4 个时间单位,也意味着
team0
t
e
a
m
0
在4个时间单位后将再次变成可利用的;再来看下一个wp,即
wp2
w
p
2
,它有两个约束wp,即
wp3
w
p
3
和
wp3
w
p
3
,此时这两个wp都没有完成,因此 ,
wp2
w
p
2
不能被执行。
时间来到1.5个时间单位后,此时,
wp3
w
p
3
和
wp4
w
p
4
都已经完成了,且
team2
t
e
a
m
2
和
team1
t
e
a
m
1
也再次变得可利用,因此,
wp2
w
p
2
可以被执行了,我们应该用
team2
t
e
a
m
2
执行
wp2
w
p
2
,这将耗费 11/3=3.667 个时间单位,完成的时间则是 1.5+3.667=5.167 个时间单位;同时我们考虑下一个wp,即
wp5
w
p
5
,它的约束wp即
wp2
w
p
2
没有完成,所以
wp5
w
p
5
不能执行。
时间来到5.167个时间单位后,这时,
wp2
w
p
2
完成了,且
team2
t
e
a
m
2
、
team1
t
e
a
m
1
、
team0
t
e
a
m
0
都变为了可使用,因此,
wp5
w
p
5
可以被执行了,我们应该用
team2
t
e
a
m
2
去执行
wp5
w
p
5
,这将耗费 5/3 个时间单位,完成的时间则是 5.167+5/3 个时间单位;同时我们考虑下一个wp,即
wp1
w
p
1
,它的约束wp有两个,即
wp0
w
p
0
和
wp5
w
p
5
,
wp0
w
p
0
完成了,而
wp5
w
p
5
还没有完成,所以
wp1
w
p
1
不能执行。
时间来到5.167+5/3个时间单位后,这时,
wp5
w
p
5
完成了,且
team2
t
e
a
m
2
、
team1
t
e
a
m
1
、
team0
t
e
a
m
0
都变为了可使用,因此,
wp1
w
p
1
可以被执行了,我们应该用
team2
t
e
a
m
2
去执行
wp1
w
p
1
,这将耗费 10/3 个时间单位,完成的时间则是 5.167+5/3+10/3=10.167 个时间单位。
最终,完成所有的wp耗费的时间为10.167个时间单位。
为了直观,我们做一个以下表格:
Work Package ID | 3 | 4 | 0 | 2 | 5 | 1 |
---|---|---|---|---|---|---|
effort of the wp | 2 | 3 | 8 | 11 | 5 | 10 |
start time of the wp | 2 | 3 | 8 | 11 | 5 | 10 |
finish time of the wp | 2/3=0.666 | 3/2=1.5 | 8/2=4 | 1.5+11/3=5.167 | 5.167+5/3 | 5.167+5/3+10/3=10.167 |
to team | 2 | 1 | 0 | 2 | 2 | 2 |
大体的思路就是这样子了,接下来贴上c++的源代码:
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
using namespace std;
const int WP_NUM = 6; //WP(需要被解决的项目)的数量
const int STAFF_NUM = 10; //工作人员的数量
const int TEAM_NUM = 5; //工作团队的数量
struct WP
{
int m_efforts; //完成该WP需要花费的成本,单位是 日/人
};
WP wps[WP_NUM]; //存放所有WP的数组
vector<int> constraints[WP_NUM]; //存放WP之间的约束条件。比如,若constraints[1]={2,4,0},则表示1号WP需要在0、2、4号WP被完成后才能进行
int chromosome_of_wpo[WP_NUM];
int chromosome_of_tc[STAFF_NUM];
void initWpsAndCons(); //初始化WP数组,数据存放在文件"wpsandcons.txt"中
void initChromosomes();
bool isChromosomesMatchConditions(); //判断从文件中读入的染色体chromosomes是否满足题目给的约束条件
double calFitness(int *chromosome_of_wpo,int *chromosome_of_tc);
void sortTeamAccordingCapacity(int *team,int *capacity,int arr_size);
int main()
{
initWpsAndCons();
initChromosomes();
if(isChromosomesMatchConditions() == false)
{
cout<<"前提条件没能被满足,请适当更改文件中的染色体"<<endl;
return 1;
}
cout<<calFitness(chromosome_of_wpo,chromosome_of_tc)<<endl;
return 0;
}
void initWpsAndCons()
{
ifstream in("wpsandcons.txt"); //文件的每一行数据中第一列是该WP的efforts,之后的数字代表约束
string str;
int cons;
for(int i=0;i<WP_NUM;++i)
{
getline(in,str); //读进文件中的每一行数据
stringstream ss(str);
ss>>wps[i].m_efforts;
while(ss>>cons)
{
constraints[i].push_back(cons);
}
ss.clear();
}
}
void initChromosomes()
{
ifstream in("chromosomes.txt"); //第一行是wps的chromosome,第二行是tc的chromosome
string str;
getline(in,str);
stringstream ss1(str);
for(int i=0;i<WP_NUM;++i)
{
ss1>>chromosome_of_wpo[i];
}
ss1.clear();
getline(in,str);
stringstream ss2(str);
for(int i=0;i<STAFF_NUM;++i)
{
ss2>>chromosome_of_tc[i];
}
ss2.clear();
}
bool isChromosomesMatchConditions()
{
bool flag = true;
//先判断chromosome_of_wps,其需要满足的是每个wp的约束都需要在这个wp之前
for(int i=0;i<WP_NUM;++i)
{
for(int j=i+1;j<WP_NUM;++j)
{
for(vector<int>::iterator it=constraints[chromosome_of_wpo[i]].begin();it!=constraints[chromosome_of_wpo[i]].end();++it)
{ //列举chromosome_of_wps[i]的约束条件,看其是否出现在其后出现,是的话就不满足
if(*it == chromosome_of_wpo[j])
{
flag = false;
break;
}
}
}
}
if(flag == false)
{
return false;
}
//接着判断chromosomes_of_tc,满足的条件是每个team都必须要有一个staff
int cnt;
for(int k=0;k<TEAM_NUM;++k)
{
cnt = 0;
for(int i=0;i<STAFF_NUM;++i)
{
if(chromosome_of_tc[i]==k) cnt++; //统计这个team有多少个staff
}
if(cnt==0) //不满足条件
{
flag = false;
break;
}
}
return flag;
}
double calFitness(int *chromosome_of_wpo,int *chromosome_of_tc)
{
int capacity_of_team[TEAM_NUM]; //每个队伍team中的staff人数
int id_of_team[TEAM_NUM]; //保存原先team的序号,因为过程中需要对team进行排序,所以有必要保存原先的序号
double date_of_team[TEAM_NUM]; /*这个team的下一个可被选择的时间,因为当前这个team有可能正在工作中(处理某一个WP),
当它完成这项工作时,它又成为了可被其他WP选择的team,因此有必要保存这个时间*/
double start_time_of_wpo[WP_NUM];//WP开始的时间
double finish_time_of_wpo[WP_NUM];//WP结束的时间,即该WP被完成的时间
double efforts_of_wpo[WP_NUM]; //完成该WP所需要的efforts
int to_team_of_wpo[WP_NUM]; //保存该WP是被哪个team完成的
for (int i=0;i<TEAM_NUM;++i)
{
id_of_team[i] = i;
capacity_of_team[i] = 0;
date_of_team[i] = 0;
}
for(int i=0;i<WP_NUM;++i)
{
start_time_of_wpo[i] = 0;
finish_time_of_wpo[i] = 0;
efforts_of_wpo[i] = wps[chromosome_of_wpo[i]].m_efforts;
}
for (int i=0;i<STAFF_NUM;++i)
{
(capacity_of_team[chromosome_of_tc[i]])++; //统计每一个team的staff人数
}
sortTeamAccordingCapacity(id_of_team,capacity_of_team,TEAM_NUM); //降序
//for(int i=0;i<TEAM_NUM;++i) cout<<id_of_team[i]<<"\t"<<capacity_of_team[i]<<endl;
double curr_time = 0;
double shortest_time; //保存接下来最先完成的项目的完成时间
for(int i=0;i<WP_NUM;)
{
bool flag = true;
for(int k=0;k<constraints[chromosome_of_wpo[i]].size(); ++k) //遍历约束
{
int m = constraints[chromosome_of_wpo[i]][k]; //m存放染色体chromosome_of_wpo中第i个元素所对应的wp的约束wp的id
for(int t=0;t<WP_NUM;++t)
{
if(m==chromosome_of_wpo[t])
{
m = t; //在染色体chromosome_of_wpo中找到m所在的位置
break;
}
}
if(finish_time_of_wpo[m]==0 || finish_time_of_wpo[m]>curr_time) //如果该约束还没开始做或者还没有完成
{
flag = false; //表明约束没能满足
break;
}
}
if (flag == false) //约束没能满足,接下来则需要改变当前时间cerr_time,使得游戏能够继续
{
shortest_time = 1e10; //给shortest_time赋一个较大的数
for(int k=0;k<i;++k)
{
if(finish_time_of_wpo[k]>curr_time)
//从已经在进行中了却还没完成的项目中选择一个最早完成的项目时间作为curr_time
{
shortest_time = min(shortest_time,finish_time_of_wpo[k]);
}
}
curr_time = shortest_time;
}
else //所有约束都已经满足,接下来就需要为这个WP寻找team
{
bool flag1 = false;
for(int j=0;j<TEAM_NUM;++j) //顺序寻找能够提供服务的队伍team
{
if(date_of_team[j]<=curr_time) //这个队伍这时候能提供服务
{
to_team_of_wpo[i] = id_of_team[j]; //指明完成该WP的team是谁
start_time_of_wpo[i] = curr_time; //该WP的开始时间
finish_time_of_wpo[i] = curr_time+efforts_of_wpo[i]/capacity_of_team[j];//该WP的预计完成时间
date_of_team[j] = finish_time_of_wpo[i]; //该team的下一次可供选择的时间
i++; //进行下一个WP的工作
flag1 = true;
break;
}
}
if(flag1==false) //没有队伍能够为该WP提供服务,即所有队伍这时候都在工作中
{
shortest_time = 1e10; //给shortest_time赋一个较大的数
for(int k=0;k<i;++k)
{
if(finish_time_of_wpo[k]>curr_time)
//从已经在进行中了却还没完成的项目中选择一个最早完成的项目时间作为curr_time
{
shortest_time = min(shortest_time,finish_time_of_wpo[k]);
}
}
curr_time = shortest_time; //更改curr_time使得游戏能够继续
}
}
}
double res = 0; //最后的结果应该是所有的WP中完成时间最迟的那个时间
for (int i=0;i<WP_NUM;++i)
{
res = max(res,finish_time_of_wpo[i]);
}
return res;
}
void sortTeamAccordingCapacity(int *team,int *capacity,int arr_size)
{ //冒泡法排序,根据capacity降序,同时改变team的下标使得两个数组能够同步更改
for(int i=0;i<arr_size;++i)
{
for(int j=i+1;j<arr_size;++j)
{
if(capacity[i]<capacity[j])
{
int tmp1 = capacity[i];
capacity[i] = capacity[j];
capacity[j] = tmp1;
int tmp2 = team[i];
team[i] = team[j];
team[j] = tmp2;
}
}
}
}
输入的数据存放在文件中,下面是文件的内容:
chromosomes.txt 文件内容(第一行是wpo的染色体,第二行是tc的染色体):
3 4 0 2 5 1
0 1 2 1 3 4 0 4 2 2
wpsandcons.txt文件内容(每行第一列是effort,后边的是约束wp的id):
8
10 0 5
11 4 3
2
3
5 2