Python 实现遗传算法解决资源受限项目调度问题(RCPSP)

目录

前言

一、染色体的编码

二、染色体的交叉

三、染色体的选择

四、染色体的变异

五、适应度函数

六、完整代码

六、运行结果分析

总结



前言

        b站账号:伊江痕,有视频讲解。

        RCPSP问题我之前的博客做过简要的介绍,并且用了SSGS算法(串行调度机制)解决了以工期最小为目标函数的问题,这里不多赘述,可以翻翻我之前的博客。本篇博客则是展示了如何用遗传算法解决RCPSP问题,目标函数同样是求工期最小。

        遗传算法解决RCPSP问题是一个很热门的算法了,但是对具体的实现过程,各类教科书、论文里面讲得都特别得不详细,好像生怕人理解了一样(当然!可能是因为本人太过于愚笨了!),本文的代码也是我自己反复推敲出来的,不一定对,请诸君阅读时自行斟酌。

一、染色体的编码

        RCPSP问题的编码主要分为三种类型:离散编码,连续编码,和混合编码。

        离散编码就是把项目中的每个任务的序号存储到列表中,序号之间的前后关系代表着任务间的紧前、紧后关系。

        连续编码则是把每个任务的开始时间和结束时间作为一个子列表存储到大列表中,子列表之间的前后关系也能表示任务间的紧前、紧后关系。

        混合编码就是将任务的序号,任务的开始时间和结束时间三个元素作为一个子列表存储到大列表中。

        我们可以很清楚的看出来,混合编码的方式所蕴含的信息更多,不仅能表达任务具体的开始、结束时间(离散编码做不到这点),又能表达任务的序列号(连续编码做不到这点)。同时这种编码在迭代过程中自然是较前二者来说,需要更多的算力。形状如下:

# 每个子列表的第一个元素为任务序号,第二个为任务开始时间,第三个为任务结束时间
[[1, 0, 0], [2, 0, 2], [3, 0, 4], [4, 0, 6], [9, 6, 9], [6, 2, 10], [10, 10, 15], [7, 10, 20], [5, 15, 24], [8, 20, 24], [11, 24, 25], [12, 25, 25]]

        本文采用混合编码的方式。

二、染色体的交叉

        本文采用PMX(部分匹配交叉)算子做交叉,部分匹配交叉保证了每个染色体中的基因仅出现一次,通过该交叉策略在一个染色体中不会出现重复的基因。这很适用于RCPSP问题,因为RCPSP问题中,项目中的每个任务都是独一无二的,A任务不能代替B任务。即染色体中的基因不能出现重复。

        PMX算子的核心在于,要对交叉后的两个染色体做冲突检测,即检查交叉后的染色体是否有重复的基因,然后根据两组基因间建立的映射关系,对重复的基因进行修改。在此不赘述具体过程,可以查看本站内其他博主的博客。

三、染色体的选择

        本文采取的是二元竞标法进行染色体的选择,同时保证种群规模不变。(某教科书上说,种群规模是否扩大,对求解效果影响不大,我也没验证,就暂时保证种群规模不变了)。

四、染色体的变异

        由于目标函数是求工期最小,因此本文的变异方向就是针对染色体的基因中第二个元素和第三个元素进行减法的变异。如某个基因如下:

[6, 2, 10]

        该子列表即表示这个染色体中某个代表6号任务的基因,这个6号任务的开始时间是2,结束时间是10。那么我们变异的方向就是使得这个2和10尽可能的小,这样才能朝着总的工期最小的方向进化。

        这里有个核心要注意的点,也是困扰我一两天的“bug”。就是:这里的2和10要同速率的变异,因为这个任务的工期是不变的,如果2变成了3,10变成了12,那么该6号任务的工期就变成了9了,很明显同该任务的本来工期8是不一致的。

        另外一点需要注意的是,变异率指的是某个染色体发生变异的概率,而非该染色体某个基因发生变异的概率。

        最后一点需要注意的是,无论是交叉还是变异后,都要对基因进行筛选,判断该基因是否满足任务间的紧前紧后关系以及是否满足资源限量需求。这点很关键。也是编程过程中的难点。

五、适应度函数

        该染色体所代表的总工期(这里的总工期即等于最后一个基因的结束时间,最后一个基因是虚工作,开始时间=结束时间)的倒数。

        在进行二元竞标选择时,要对适应度函数的值进行归一化。比较的指标即是:某个染色体的适应度函数值/整个种群的适应度值。

六、完整代码

        完整代码如下:

import copy
import random
import time
import matplotlib.pyplot as plt
import numpy as np
from collections import defaultdict
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
plt.rcParams["axes.labelsize"]=14
plt.rcParams["xtick.labelsize"]=12
plt.rcParams["ytick.labelsize"]=12


class Activity(object):
    '''
    活动类:包含  1.活动ID  2.活动持续时间    3.活动资源需求量   4.活动紧前活动    5.活动最早开始时间  6.活动最晚开始时间  7.活动是否被访问
    '''

    def __init__(self, id, duration, resourceRequest, successor):
        self.id = id
        self.duration = duration
        self.resourceRequest = np.array(resourceRequest)
        self.predecessor = None
        self.successor = successor
        self.es = 0
        self.ef = 0
        self.ls = 0
        self.lf = 0
        self.tf=0
        self.visited = False
class Painter:
    def __init__(self,project=None,total_resource=None):
        self.project=project
        self.total_resource=total_resource

    def draw_activities(self):
        print("开始画了")
        colors = ['#ff1493', 'g', 'r', 'c', 'm', 'y', 'k', '#FFA500', '#800080', '#00FFFF', '#008080', '#FFC0CB']
        # 第一张图
        plt.figure(figsize=(6, 8))
        plt.subplot(4, 1, 1)
        rects4 = []
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
        plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
        left, right=plt.xlim([0, self.project[-1].es])
        bottom, top=plt.ylim([0,self.total_resource[0]] )  # 改
        y_ticks = [y for y in range(0,int(top),2)]
        y_tick_labels = [y for y in range(0,int(top),2)]
        plt.yticks(y_ticks, y_tick_labels)
        x_ticks = [x for x in range(0,int(right)+1)]
        x_tick_labels = ["" for x in range(0,int(right+1))]
        plt.xticks(x_ticks, x_tick_labels)
        plt.text(self.project[-1].es/2,self.total_resource[0]+1
                 ,'mv1.rcp数据集项目活动进度图(活动工期优先)', fontsize=16,ha="center",va="center")
        for activity in self.project:
            if activity.resourceRequest[0]!=0:  # 改
                pillar=(activity.es,activity.es+activity.duration,activity.resourceRequest[0],0)  # 改
                rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                if len(rects4)!=0:
                    for r in rects4:
                        if Painter.is_intersected(rect,r):
                            pillar = (activity.es, activity.es + activity.duration, activity.resourceRequest[0], r.get_height()+r.get_y())  # 改
                            rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                rects4.append(rect)
                bar_container=self.draw_pillar(plt,pillar,color=colors[activity.id-1])
                plt.text(bar_container[0].get_x()+bar_container[0].get_width()/2
                         ,bar_container[0].get_height()/2+bar_container[0].get_y()-0.5
                         ,"活动 {}".format(activity.id),ha='center', color='white', fontweight='bold')
                plt.text(-4.5,total_resource[0]/2,"1号资源")

        # 第二张图
        plt.subplot(4, 1, 2)
        rects4 = []
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
        plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
        left, right=plt.xlim([0, self.project[-1].es])
        bottom, top=plt.ylim([0,self.total_resource[1]] )  # 改
        y_ticks = [y for y in range(0,int(top),2)]
        y_tick_labels = [y for y in range(0,int(top),2)]
        plt.yticks(y_ticks, y_tick_labels)
        x_ticks = [x for x in range(0,int(right)+1)]
        x_tick_labels = ["" for x in range(0,int(right+1))]
        plt.xticks(x_ticks, x_tick_labels)
        for activity in self.project:
            if activity.resourceRequest[1]!=0:  # 改
                pillar=(activity.es,activity.es+activity.duration,activity.resourceRequest[1],0)  # 改
                rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                if len(rects4)!=0:
                    for r in rects4:
                        if Painter.is_intersected(rect,r):
                            pillar = (activity.es, activity.es + activity.duration, activity.resourceRequest[1], r.get_height()+r.get_y())  # 改
                            rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                rects4.append(rect)
                bar_container=self.draw_pillar(plt,pillar,color=colors[activity.id-1])
                plt.text(bar_container[0].get_x()+bar_container[0].get_width()/2
                         ,bar_container[0].get_height()/2+bar_container[0].get_y()-0.5
                         ,"活动 {}".format(activity.id),ha='center', color='k', fontweight='bold')
                plt.text(-4.5,total_resource[1]/2,"2号资源")


        # 第三张图
        plt.subplot(4, 1, 3)
        rects4 = []
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
        plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
        left, right=plt.xlim([0, self.project[-1].es])
        bottom, top=plt.ylim([0,self.total_resource[2]] )  # 改
        y_ticks = [y for y in range(0,int(top),2)]
        y_tick_labels = [y for y in range(0,int(top),2)]
        plt.yticks(y_ticks, y_tick_labels)
        x_ticks = [x for x in range(0,int(right)+1)]
        x_tick_labels = ["" for x in range(0,int(right+1))]
        plt.xticks(x_ticks, x_tick_labels)
        for activity in self.project:
            if activity.resourceRequest[2]!=0:  # 改
                pillar=(activity.es,activity.es+activity.duration,activity.resourceRequest[2],0)  # 改
                rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                if len(rects4)!=0:
                    for r in rects4:
                        if Painter.is_intersected(rect,r):
                            pillar = (activity.es, activity.es + activity.duration, activity.resourceRequest[2], r.get_height()+r.get_y())  # 改
                            rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                rects4.append(rect)
                bar_container=self.draw_pillar(plt,pillar,color=colors[activity.id-1])
                plt.text(bar_container[0].get_x()+bar_container[0].get_width()/2
                         ,bar_container[0].get_height()/2+bar_container[0].get_y()-0.5
                         ,"活动 {}".format(activity.id),ha='center', color='white', fontweight='bold')
                plt.text(-4.5,total_resource[2]/2,"3号资源")

        # 第四张图
        plt.subplot(4, 1, 4)
        rects4 = []
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
        plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题
        left, right=plt.xlim([0, self.project[-1].es])
        bottom, top=plt.ylim([0,self.total_resource[3]] )  # 改
        y_ticks = [y for y in range(0,int(top),2)]
        y_tick_labels = [y for y in range(0,int(top),2)]
        plt.yticks(y_ticks, y_tick_labels)
        x_ticks = [x for x in range(0,int(right)+1)]
        x_tick_labels = [x for x in range(0,int(right)+1)]
        plt.xticks(x_ticks, x_tick_labels)
        plt.xlabel("时间进度",fontsize=12)
        for activity in self.project:
            if activity.resourceRequest[3]!=0:  # 改
                pillar=(activity.es,activity.es+activity.duration,activity.resourceRequest[3],0)  # 改
                rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                if len(rects4)!=0:
                    for r in rects4:
                        if Painter.is_intersected(rect,r):
                            pillar = (activity.es, activity.es + activity.duration, activity.resourceRequest[3], r.get_height()+r.get_y())  # 改
                            rect = plt.Rectangle((pillar[0], pillar[3]), width=pillar[1] - pillar[0], height=pillar[2])
                rects4.append(rect)

                bar_container=self.draw_pillar(plt,pillar,color=colors[activity.id-1])

                plt.text(bar_container[0].get_x()+bar_container[0].get_width()/2
                         ,bar_container[0].get_height()/2+bar_container[0].get_y()-0.5
                         ,"活动 {}".format(activity.id),ha='center', color='white', fontweight='bold')
                plt.text(-4.5,total_resource[3]/2,"4号资源")
        plt.subplots_adjust(hspace=0)
        plt.show()
    def draw_pillar(self,plt,pillar:tuple,color):
        x1=pillar[0]
        x2=pillar[1]
        height=pillar[2]
        bottom=pillar[3]
        x_pillar=[x1]
        height=[height]
        bar_container=plt.bar(x_pillar,height=height,width=x2-x1,bottom=bottom,align="edge",edgecolor='black',color=color)

        return bar_container
    @staticmethod
    def is_intersected(bbox1,bbox2):
        """
        判断长方形是否相交
        :param bbox1:
        :param bbox2:
        :return:
        """
        if bbox1.get_x() < bbox2.get_x() + bbox2.get_width() and bbox1.get_x() + bbox1.get_width() > bbox2.get_x() and bbox1.get_y() < bbox2.get_y() + bbox2.get_height() and bbox1.get_y() + bbox1.get_height() > bbox2.get_y():
            return True
        else:
            return False
class Genetic_Algorithms:

    def __init__(self,project=None,total_resource=None):
        self.project=project
        self.total_resource=total_resource
    # 种群初始化算子
    def initialize_population(self,num_population):
        population=[]
        for i in range(num_population):
            project=copy.deepcopy(self.project)  # 深拷贝一份project,防止每次产生的活动序列太过相似
            all_activities = [value for value in project.values()]  # 所有的活动列表
            assigned_activities = [project[1]]  # 已经分配好的活动的列表
            unassigned_activities = all_activities.copy()  # 还没被分配好的活动的列表
            unassigned_activities.remove(project[1])
            candidate_activities = self._get_activities_via_ids(project[1].successor,project)  # 将要被分配的活动的列表
            while unassigned_activities:  # 当还没被分配好的活动的列表不为空时,就继续循环,直到把每个活动分配完成
                random_activity=random.sample(candidate_activities,1)[0]
                candidate_activity=random_activity
                """
                以下,选择candidate_activity的所有紧前活动的
                开始时间+活动工期的最大值作为candidateActivity的
                开始时间
                """
                early_start_time_s = [predecessor_candidate_activity.es + predecessor_candidate_activity.duration
                                      for predecessor_candidate_activity in
                                      self._get_activities_via_ids(candidate_activity.predecessor, project)
                                      ]
                early_start_time = max(early_start_time_s) # +random.randint(0,10)
                while True:

                    if self._is_resource_enough(candidate_activity, early_start_time, assigned_activities, self.total_resource):
                        candidate_activity.es = early_start_time
                        candidate_activity.ef = early_start_time + candidate_activity.duration
                        assigned_activities.append(candidate_activity)
                        unassigned_activities.remove(candidate_activity)
                        candidate_activities.remove(candidate_activity)
                        successors_ids = candidate_activity.successor
                        successors = self._get_activities_via_ids(successors_ids, project)
                        for successor in successors:
                            if successor in assigned_activities:
                                successors.remove(successor)
                            if set(self._get_activities_via_ids( successor.predecessor,project)).intersection(unassigned_activities):
                                successors.remove(successor)
                        candidate_activities = candidate_activities + successors
                        if len(candidate_activities) > 1:
                            for a in candidate_activities:
                                if a.id == len(project):
                                    candidate_activities.remove(a)
                        break
                    else:
                        early_start_time+=1

            activities_info = []
            for i in assigned_activities:
                activity_info=[i.id,i.es,i.es+i.duration]
                activities_info.append(activity_info)
            population.append(activities_info)
        return population

    def pmx_cross(self,population:list):
        population_crossed=[]
        population=copy.deepcopy(population)
        total_resource=self.total_resource
        for i in range(int(len(population)/2)):
            parent=random.sample(population,2)
            parent1=parent[0]
            parent2=parent[1]
            child1,child2=self._pmx_cross(parent1,parent2)
            count1=0
            count2=0
            # 交叉算子似乎还有问题
            while (not self._is_resource_enough_for_entire_project(child1,total_resource)) or (self._checkIfDuplicates(child1)) :
                child1, temp = self._pmx_cross(parent1, parent2)  # 多次迭代,保证子代满足资源需求
                count1+=1
                if count1>10:
                    break
            while (not self._is_resource_enough_for_entire_project(child2,total_resource)) or (self._checkIfDuplicates(child2)):
                temp,child2=self._pmx_cross(parent1, parent2)
                count2+=1
                if count2>10:
                    break
            population_crossed.append(child1)
            population_crossed.append(child2)
        return population_crossed
    def mutate(self,population,mutation_rate):
        population_mutated=[]
        for child in population:
            child_mutated = self._mutate(child,mutation_rate)
            population_mutated.append(child_mutated)
        return population_mutated
    def sum_fitness_population(self,population):
        sum=0
        for parent in population:
            sum+=self.fitness_parent(parent)
        return sum
    def fitness_parent(self,parent):
        parent.sort(key=lambda x:x[2])
        return 1/(parent[-1][2])  # 选每个child的最后一项任务的最早结束时间的倒数做为适应度
    def encode(self,project:dict):

        activities_info = []
        for values in project.values():
            activity_info=[values.id,values.es,values.es+values.duration]
            activities_info.append(activity_info)
        activities_info.sort(key=lambda x:x[2])
        return activities_info
    def decode(self,activities_info:list,project:dict):
        for a in activities_info:
            project[a[0]].es=a[1]
            project[a[0]].ef=a[2]
        return project
    def _select(self,parent1,parent2):
        pass
    def _mutate(self,child,mutation_rate):
        child_copy=copy.deepcopy(child)
        if not self._can_mutate(child):
            return child_copy
        if mutation_rate > random.random():
            gene_index=random.sample([x+1 for x in range(len(child_copy)-1)],1)[0]
            # print("child_copy:"+str(child_copy))
            child_copy[gene_index][1]-=1
            child_copy[gene_index][2]-=1
            project_child = self.decode(child_copy, self.project)
            if  not self._is_resource_enough_for_entire_project(child_copy,self.total_resource) \
                or  not self._is_slower_than_predecessor(self._get_activity_via_id(child_copy[gene_index][0],project_child),project_child)\
                :

                self._mutate(child,mutation_rate)
        return child_copy
    def _can_mutate(self,child):
        child_copy=copy.deepcopy(child)
        for i in [x+1 for x in range(len(child_copy)-1)]:
            gene_index=i
            child_copy[gene_index][1]-=1
            child_copy[gene_index][2]-=1
            project_child = self.decode(child_copy, self.project)
            if   self._is_resource_enough_for_entire_project(child_copy,self.total_resource) \
                and   self._is_slower_than_predecessor(self._get_activity_via_id(child_copy[gene_index][0],project_child),project_child)\
                :
                return True
            else:
                continue
        return False
    def _pmx_cross(self,parent1, parent2):
        """
        使用PMX算子对两个染色体进行交叉操作
        """
        # 随机选择交叉区域
        start,end=sorted(random.sample([x for x in range(1,len(parent1)-1)],2))
        # 将父代1中交叉区域的基因复制到子代1和2中
        child1 = parent1[:start] + parent2[start:end+1] + parent1[end+1:]
        child2 = parent2[:start] + parent1[start:end+1] + parent2[end+1:]
        # 处理重复的基因
        non_cross_gene_child1=child1[:start]+child1[end+1:]
        cross_gene_child1=child1[start:end+1]
        for i in range(start, end+1):
            if parent1[i] not in child1:
                indexs=[ii for ii, x in enumerate(child1) if x[0] == parent2[i][0]]
                try:
                    index = [ii for ii in indexs if ii not in range(start, end + 1)][0]  # 选择索引是 不在交叉区域的基因的索引
                    child1[index] = parent1[i]

                except:
                    continue

        non_cross_gene_child2=child2[:start]+child2[end+1:]
        cross_gene_child2=child2[start:end+1]
        for i in range(start, end+1):
            if parent2[i] not in child2:
                indexs = [ii for ii, x in enumerate(child2) if x[0] == parent1[i][0]]
                try:

                    index=[ii for ii in indexs if ii not in range(start,end+1)][0]
                    child2[index]=parent2[i]
                except:
                    continue


        return child1, child2
    def _is_resource_enough(self,candidate_activity, early_start_time, assigned_activities, total_resources):
        """
        判断如果把candidate_activity插入到某一个时间段以后了,能否满足该时间段的资源限量要求
        :param candidate_activity:
        :param early_start_time:
        :param assigned_activities:
        :param total_resources:
        :return:
        """
        t = early_start_time + 1
        while t <= early_start_time + candidate_activity.duration:
            sum_resource = np.zeros(len(total_resources))
            for assigned_activity in assigned_activities:
                if assigned_activity.es + 1 <= t <= assigned_activity.es + assigned_activity.duration:
                    sum_resource += assigned_activity.resourceRequest
            sum_resource += candidate_activity.resourceRequest
            if (sum_resource > total_resources).any():
                return False
            t += 1

        return True
    def _get_activities_via_ids(self,ids: list, activities: dict):
        '''
        通过活动的id的集合ids获得活动对象的集合
        :param ids:
        :param project:
        :return:
        '''
        activities_wanted = []
        for i in ids:
            activities_wanted.append(activities[i])
        return activities_wanted
    def _get_activity_via_id(self,id, activities: dict):
        '''
        通过活动的id获得活动对象
        :param ids:
        :param project:
        :return:
        '''
        return activities[id]
    def _is_slower_than_predecessor(self,activity,project:dict):
        predecessor_id=activity.predecessor
        predecessor=self._get_activities_via_ids(predecessor_id,project)
        alist=[x.es+x.duration for x in predecessor]
        if len(alist):
            return False
        max_time=max(alist)
        if activity.es+activity.duration >= max_time:
            return True
        else:
            return False
    def _is_resource_enough_for_entire_project(self,project, total_resources):
        """
        判断整个项目是否满足资源限量需求
        :param candidate_activity:
        :param early_start_time:
        :param assigned_activities:
        :param total_resources:
        :return:
        """

        for activity in project:
            t = activity[1] + 1
            while t <= activity[2]:
                sum_resource = np.zeros(len(total_resources))
                if activity[0] != 1:
                    for before_activity in project[:project.index(activity)]:
                        if before_activity[1] + 1 <= t <= before_activity[2]:
                            sum_resource += self.project[before_activity[0]].resourceRequest
                    sum_resource += self.project[activity[0]].resourceRequest
                    if (sum_resource > total_resources).any():
                        return False
                    t += 1

        return True
    def _checkIfDuplicates(self,test_list):
        """
        检查列表里是否有相同的任务
        :param test_list:
        :return:
        """
        for i, x in enumerate(test_list):
            for j, y in enumerate(test_list):
                if i != j and x[0] == y[0]:
                    return True
        return False
    @staticmethod
    def is_resource_enough_for_entire_project_staticmethod(activities:dict,project:list, total_resources):
        """
        判断整个项目是否满足资源限量需求
        :param activities:
        :param project:
        :param total_resources:
        :return:
        """
        for activity in project:
            t = activity[1] + 1
            while t <= activity[2]:
                sum_resource = np.zeros(len(total_resources))
                if project.index(activity) != 0:
                    for before_activity in project[:project.index(activity)]:
                        if before_activity[1] + 1 <= t <= before_activity[2]:
                            sum_resource +=activities[before_activity[0]].resourceRequest
                    sum_resource += activities[activity[0]].resourceRequest
                    if (sum_resource > total_resources).any():
                        return False
                    t += 1

        return True
    @staticmethod
    def checkIfDuplicates_staticmethod(test_list):
        for i, x in enumerate(test_list):
            for j, y in enumerate(test_list):
                if i != j and x[0] == y[0]:
                    return True
        return False

def read_data_from_RCP_file(file_name):
    '''
    读取标准化文件中的所有活动信息,包括  1.活动数   2.项目资源数 3.项目资源种类数   4.项目资源限量
    5.所有活动的ID,持续时间,资源需求,紧前活动
    :param fileName:
    :return: 标准化文件数据
    '''
    f = open(file_name)
    taskAndResourceType = f.readline().split('      ')  # 第一行数据包含活动数和资源数
    num_activities = int(taskAndResourceType[0])  # 得到活动数
    num_resource_type = int(taskAndResourceType[1])  # 得到资源数
    total_resource = np.array([int(value) for value in f.readline().split('      ')[:-1]])  # 获取资源限量
    # 将每个活动的所有信息存入到对应的Activity对象中去
    activities = {}
    preActDict = defaultdict(lambda: [])
    for i in range(num_activities):
        nextLine = [int(value) for value in f.readline().split('      ')[:-1]]
        task = Activity(i + 1, nextLine[0], nextLine[1:5], nextLine[6:])
        activities[task.id] = task
        for act in nextLine[6:]:
            preActDict[act].append(i + 1)
    f.close()
    # 给每个活动加上紧前活动信息
    for actKey in activities.keys():
        activities[actKey].predecessor = preActDict[activities[actKey].id].copy()
    return num_activities, num_resource_type, total_resource, activities  # 活动数int, 资源数int, 资源限量np.array, 所有活动集合dic{活动代号:活动对象}


if __name__ == "__main__":
    # 0、首先在这个网站上下载数据集:https://www.projectmanagement.ugent.be/research/data,下载“RCPLIB.zip”文件
    # 我用的是RCPLIB\DC\DC1\mv1.rcp这个目录下的文件,所以大家测试代码的时候也尽量用这个,其他的文件可能因为字符串格式问题
    # 会让我的代码失效,我粗略看了下,似乎mv1.rcp-mv9.rcp之间的文件大部分能用。
    file_name = r"RCPLIB\DC\DC1\mv1.rcp"
    # 1、从rcp文件中读取数据,得到活动个数,资源种类数,各个资源的限量np.array,所有活动的集合dict{活动id:活动对象}
    num_activities, num_resource_type, total_resource, activities = read_data_from_RCP_file(file_name)
    # 2、遗传算法初始化
    ga=Genetic_Algorithms(activities,total_resource)
    # 3、种群初始化
    population=ga.initialize_population(50)  # 尽量偶数个个体
    def f(population,n):
        fitness=[]
        for i in range(n):
            sum_fitness_population = ga.sum_fitness_population(population)
            print("交叉前的适应度:"+str(sum_fitness_population))
            population_not_for_cross=[]
            tournament_size = 2  # 比赛的个体数量
            num_offsprings = 10  # 选择的下一代的数量
            population_for_cross=[]  # 存储选择出的下一代的父母
            while len(population_for_cross) < num_offsprings:
                # 在种群中随机选择tournament_size个个体作为比赛的参与者
                parent1,parent2 = random.sample(population, tournament_size)
                # 从参与者中选择适应度最高的个体作为下一代的父母
                if ga.fitness_parent(parent1)/sum_fitness_population > ga.fitness_parent(parent2)/sum_fitness_population :
                    population_for_cross.append(parent1)
                else:
                    population_for_cross.append(parent2)
            population_crossed=ga.pmx_cross(population_for_cross)

            population_for_mutate=population_crossed
            population_mutated=ga.mutate(population_for_mutate,0.3)
            population=population_mutated
            print("变异后总适应度值:"+str(ga.sum_fitness_population(population)))
            fitness.append(ga.sum_fitness_population(population))
        return population,fitness
    # 4、迭代计算
    population,fitness=f(population,20)
    # 5、绘制适应度函数值的图像
    plt.plot([x for x in range(fitness.__len__())],fitness)
    plt.ylabel('fitness')
    plt.xlabel('num_iteration')
    plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))
    plt.show()
    print("最小工期为:"+str(population[-1][-1][2]))
    # 6、最终子代的展示
    for child in population:
        # print("child{}的适应度为:".format(population.index(child)+1) + str(ga.fitness_parent(child)))
        print(child)

六、运行结果分析

        多次运行的结果可知,工期的最小值为24,这也和我之前博客中通过串行调度机制算法求解的工期最小值保持了一致。有可能每次运行的结果不一致,这也是遗传算法的特性,这是由于算法收敛到局部最优解了。适应度函数值曲线如下:


总结

        代码还有很多需要完善的地方,本次产品其实也是个半成品,诸君认真查看代码应该也能看出端倪,等到有时间再完善。但是本代码的大致遗传算法思路基本没错,就是在细枝末节处需要深入打磨下。没错,我只是单纯地谦虚一下(狗头)。

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伊江痕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值