在上一篇博客中我們討論了車間調度問題的編碼問題,具體說就是根據工件的個數和每個工件的工序數來生成01011這樣的編碼列表來表示可行解,具體的說一個工件包含多少道工序,那么這個工件的編號就出現多少次。從0101中我們可以看出總共有兩個工件0和1,工件0下面有2道工序,工件1下面有2道工序,所以編碼值0出現了2次,編碼值1出現了2次。
如果想采用暴力法或隨機搜索法,我們不能只生成一個可行解的編碼列表,我們需要生成一組可行解的編碼列表,再從這組可行解中挑選最優的。這組可行解的編碼列表組成的集合叫作種群。大家思考一下如何編程實現種群的生成,大家看一下下面的python代碼:
2 gene = [j for j in range(n) for t in I[j]] 3 population = [] 4 for i in range(ps): 5 random.shuffle(gene) 6 population.append([j for j in gene]) 7 return population
第1行: 我們定義了一個生成初始種群的函數,叫作InitPopulation,這個函數接收兩個參數,ps 和 I。ps是個整型變量,它的值表示種群里可行解的個數,I是個list, 里面存放的是每個工件下每道工序使用的機器號和在該機器上加工的時間(即下面這個已知條件表格)。我們還是用上一篇博客的那兩個工件,兩台機器的已知條件表格來說明
在調用InitPopulation這個函數之前我們需要先定義好ps和I:
ps=10 //代表種群里有10個可行解
I=[[(1,3),(2,2)],[(2,5),(1,1)]]。
I 是一個list, 下面又分別有兩個子list, 每個子list代表一個工件的信息,它們是I[0]=[(1,3),(2,2)] 和I[1]=[(2,5),(1,1)],子list里每個有序偶代表該工件下的各個工序信息。
n 表示工件的個數,這里 n=2。
m 表示機器的個數,這里 m=2。
第2行 gene是固定死的0011,即按工件號由小到大排列,每個工件號出現的次數與這個工件包含的工序數(子list的個數)相同。
第3行 定義一個空list命名為population用來存放10個可行解。
第 4,5,6 行 for循環,生成ps=10個染色體,存放在population中,最后返回population。具體做法是利用python的shuffle洗牌函數來隨機打亂gene(即固定的0011)的順序來生成新編碼列表(第5行),Example:第一次shuffle(0011)洗牌后的結果可能是0101,第二次的結果是shuffle(0011)洗牌后的結果可能是1100,這樣經過ps次shuffle操作就可以生成ps=10個編碼列表存到population中(第6行)。
有了已知條件表格I,又有了每個可行解的編碼列表s(如0011,0101等),我們可以為每個可行解構造出其對應的有向無環圖G(解碼環節)。代碼如下:
1 def ComputeDAG(s, I): 4 G = [] 5 for t in s: G.append([]) 6 G.append([]) 7 T = [0 for j in range(n)] 8 last_task_job = [-1 for j in range(n)] 9 tasks_resource = [[-1 for j in range(n)] for m in range(m)] 10 st = [] # Returns for each task, its id within a job 11 for i in range(len(s)): 12 j = s[i] 13 t = T[j] 14 st.append(t) 15 r = I[j][t][0] 16 # If this is the final task of a job, add edge to the final node 17 if t + 1 == len(I[j]): G[-1].append(i) 18 # Wait for the previous task of the job 19 if t > 0: G[i].append(last_task_job[j]) 20 # Wait for the last task from other jobs using the same resource 21 G[i].extend([tasks_resource[r][j2] for j2 in xrange(I.n) 22 if j2 != j and tasks_resource[r][j2] > -1]) 23 T[j] = T[j] + 1 24 last_task_job[j] = i 25 tasks_resource[r][j] = i 26 return G, st
對上面代碼的解釋如下:
def ComputeDAG(s, I): """s是編碼信息列表如0011,0101等,I存儲已知條件表的信息""" G = [] """構造一個空list來存儲圖G,如下圖所示""" for t in s: G.append([])"""每個工件的每道工序在G中都用一個節點表示,即每個節點也都是一個子list,存放其前驅節點在編碼列表s中的下標""" G.append([])"""添加結束節點(無需添加開始節點),如下圖所示""" T = [0 for j in range(n)]"""初始化一個長度為工件個數n的全0列表""" last_task_job = [-1 for j in range(n)]"""last_task_job記錄每個工件自己最近加入G的工序在s中所對應的下標,初始化均為-1 """" tasks_resource = [[-1 for j in range(n)] for m in range(m)]"""見后面正文部分""" st = [] "記錄s里每個下標(即G的節點)在其自身工件內的相對順序,如:0,1,2,3..." for i in range(len(s)):""" 依次處理編碼列表s的每個下標,s的下標與G的節點一一對應,相當於依次處理G的節點""" j = s[i]"""提取下標為i的節點對應的編碼值,編碼值就是該工序所屬的工件號""" t = T[j]"""T[j]是個計數器 ,它記錄下標為i的節點是其自身工件j的第幾道工序(即內部相對順序),如0,1,2,3,4...""" st.append(t)""" 將t存入st,st的解釋見返回值部分""" r = I[j][t][0]""" 將下標為i的節點所使用的機器號存入r中,PS:機器號是I有序偶中的第一個數字(參見上文I的定義(已標紅))""" # 判斷i在其工件j的內部相對順序號是否等於j的工序總數,若相等,則說明是j的最后一道工序,應該作為結束節點的前驅(對應下圖黑色箭頭) if t + 1 == len(I[j]): G[-1].append(i) # 若i不是其所屬工件j的第一道工序,則工件j最近加入G的那個工序節點應該作為i的前驅(對應下圖藍色箭頭) if t > 0: G[i].append(last_task_job[j]) # 如果之前加入G中的其他別的工件的節點也在使用和當前處理的節點i相同的機器,則它們都應該作為i的前驅(對應下面紅色箭頭) G[i].extend([tasks_resource[r][j2] for j2 in xrange(I.n) if j2 != j and tasks_resource[r][j2] > -1]) T[j] = T[j] + 1"""j工件的內部工序相對順序計數器加1""" last_task_job[j] = i"""更新last_task_job[j],定義見上面""" tasks_resource[r][j] = i""" 更新tasks_resource, 定義見下面正文(已標紅)""" return G, st""" 返回圖G, st記錄每個節點(即編碼列表的下標)對應的工序在其自身工件內的相對順序,如:0,1,2,3,4...(從0開始算)"""
特別說明一下:task_resource這個list, 以上面的已知條件表格為例,它的初始化形式是:
task_resource=[[-1,-1],[-1, -1]]
因為有兩台機器, m=2,所以task_resource里面有兩個子list分別對應這兩個機器, 又因為有兩個工件,n=2,所以每個子list里面各有兩個數字分別對應這2個工件,都先初始化為-1, 以后每個數字代表相應工件最近一次使用這台機器的工序在s中的下標值。
上面的程序輸入為s和 I, 輸出為有向圖G和列表st,G中節點的個數為所有工序的總數(包括工件1的和工件2的)+1(結束節點),注意:G存放的是下標不是編碼值:
G=[ [ ],[ ],[0,1],[0,1], [2,3] ]
利用可行解的編碼列表重構出可行解對應的有向無環圖后,我們可以利用關鍵路徑法在圖G上計算出這個可行解的總完成時間,具體流程參見上一篇博客。代碼如下:
1 def ComputeStartTimes(s, I): 2 """This computes the start time of each task encoded in a chromosome of 3 the genetic algorithm. The last element of the output list is the 4 timespan.""" 5 G, st = ComputeDAG(s, I) 6 C = [0 for t in G] 7 for i in range(len(G)): 8 if len(G[i]) == 0: C[i] = 0 9 else: C[i] = max(C[k] + I[s[k]][st[k]][1] for k in G[i]) 10 return C
對代碼的解釋如下:
def ComputeStartTimes(s, I): """該函數返回一個一維列表C,列表C里各個值對應編碼列表s中每道工序(對應G中的節點)的開工時間,C列表最后一個值(對應結束節點)存放可行解的總完成時間""" G, st = ComputeDAG(s, I)"""調用上面的構圖函數創建有向無環圖""" C = [0 for t in G]"C列表初始化為全0" for i in range(len(G)):"""遍歷G中每一個節點""" if len(G[i]) == 0: C[i] = 0""" 如果節點i沒有前驅,則i的開始時間為0,代表無需等待立即開始""" """否則,節點i的開始時間等於所有其前驅節點的開始時間+前驅節點自己的處理時間中和最大的那個值""" else: C[i] = max(C[k] + I[s[k]][st[k]][1] for k in G[i]) return C"""返回每個節點的開始時間"""
以編碼0101為例,C=[0,0,5,5,7 ] 表示工件1的工序1的開始時間是0,工件2的工序1的開始時間是0,工件1的工序2的開始時間是5,工件2的工序2的開始時間是5,總完成時間是7。