混合整數規劃(MIP)是一種有效的最佳化技術,適用於解決各種排程問題。本文首先探討了人員組態水平問題,其目標是在滿足人員需求的前提下,最小化人力成本。接著,文章深入研究了任務排程問題,討論瞭如何在多台機器上安排多個任務的執行順序和時間,以最小化整體完成時間。最後,文章介紹了切割庫存問題,重點是如何從大卷材料中切割出滿足客戶需求的小卷,同時最小化浪費。每個問題都包含了詳細的數學模型建立、約束條件設計以及 Python 程式碼實作,並對程式碼的邏輯和原理進行了深入的剖析,方便讀者理解和應用。

6.2.3 例項應用:光纖網路中的多重傳輸問題

在光纖網路中,我們經常遇到一個有趣的應使用案例項。假設有一組訊號源發出訊號,還有一組接收器需要接收這些訊號。此外,還有一組轉發節點,它們僅傳遞訊號(如果需要,還可以增強訊號)。在同一條光纜中,多個訊號可以同時透過,但必須使用不同的波長。由於可用的波長數量有限,這裡實際上存在多個疊加的傳輸問題。我們的目標是最大化已建立的連線數量。

6.3 人員組態水平問題

這個問題通常被最佳化專家描述為人員排班問題,但這是一個誤導性的名稱,因為它並沒有真正產生排班結果,而只是確定人員需求水平。真正的人員排班問題比這複雜得多,因此我們將這個問題稱為人員組態水平問題。

6.3.1 問題描述

假設有一個網格結構,其中一個維度表示時間間隔(如星期一到星期日或上午8點到下午5點),另一個維度表示不同的班次(如全職或兼職)。每個時間間隔都有相應的人員需求,而每個班次都有相關的成本。我們的目標是以最低的總成本來確定每個班次需要多少人,同時滿足人員需求並優先考慮全職員工。

6.3.1.1 決策變數

在這個問題中,我們需要決定每個班次需要多少人。由於全職和兼職班次有所不同,我們可以使用兩個變數來表示:$x_i$ 表示班次 $i$ 的工作人數,其中 $i$ 屬於全職和兼職班次的集合 $N = N_f \cup N_p$。

# 定義決策變數
x = {i: model.NewIntVar(0, upper_bound, f'x_{i}') for i in N}

內容解密:

  • 這段程式碼定義了決策變數 $x_i$,表示每個班次 $i$ 的工作人數。
  • model.NewIntVar 用於建立一個新的整數變數,範圍從 0 到 upper_bound
  • upper_bound 表示每個班次的最大工作人數。

6.3.1.2 目標函式

我們的目標是最小化總成本,假設每個班次 $i$ 的成本為 $C_i$,則目標函式可以表示為:

$$\min \sum_{i \in N} C_i x_i$$

# 定義目標函式
model.Minimize(sum(C[i] * x[i] for i in N))

內容解密:

  • 這段程式碼定義了目標函式,即最小化所有班次的總成本。
  • C[i] 表示班次 $i$ 的成本,x[i] 表示班次 $i$ 的工作人數。
  • model.Minimize 用於設定模型的最小化目標。

6.3.1.3 約束條件

  1. 滿足人員需求:對於每個時間間隔 $t$,相關班次的工作人數總和必須大於或等於該時間間隔的人員需求 $R_t$。

$$\sum_{i \in N} M_{t,i} x_i \geq R_t, \forall t \in T$$

# 新增滿足人員需求的約束
for t in T:
    model.Add(sum(M[t, i] * x[i] for i in N) >= R[t])

內容解密:

  • 這段程式碼新增了滿足人員需求的約束條件。
  • M[t, i] 表示矩陣 $M$ 中第 $t$ 行第 $i$ 列的元素,用於表示班次 $i$ 是否覆寫時間間隔 $t$。
  • R[t] 表示時間間隔 $t$ 的人員需求。
  1. 最小全職員工數量:對於每個全職班次 $i$,工作人數必須大於或等於最低要求 $Q_i$。

$$x_i \geq Q_i, \forall i \in N_f$$

# 新增最小全職員工數量的約束
for i in N_f:
    model.Add(x[i] >= Q[i])

內容解密:

  • 這段程式碼新增了最小全職員工數量的約束條件。
  • Q[i] 表示全職班次 $i$ 的最低工作人數要求。
  1. 全職員工優先:所有全職員工必須被安排工作,只有在全職員工無法滿足需求時,才會安排兼職員工。

$$\sum_{i \in N_f} x_i = P$$

# 新增全職員工優先的約束
model.Add(sum(x[i] for i in N_f) == P)

內容解密:

  • 這段程式碼新增了全職員工優先的約束條件。
  • P 表示可用的全職員工總數。

混合整數規劃在排班與工作坊排程的應用

在許多產業中,排班和排程是重要的最佳化問題。適當的排班可以降低成本、提高效率,而工作坊排程則需要合理安排多項任務在不同機器上的執行順序和時間,以最小化整體完成時間。本章節將探討如何使用混合整數規劃(Mixed Integer Programming, MIP)來解決這些問題。

員工排班問題

員工排班問題旨在找到滿足特定時間段內人員需求的排班方案,同時最小化成本。該問題涉及決定不同班次中的員工數量,並考慮全職和兼職員工的不同成本和限制。

數學模型建立

首先,定義一個矩陣$M$來表示不同班次在各時間段的排班情況,以及每個時間段所需的最少員工數量。同時,定義變數$x_i$表示在第$i$個班次中的員工數量。

基本約束條件為每個時間段的總人數需滿足需求: $$ \sum_{i} M_{ti} x_i \geq M_{t, \text{最後一列}} \quad \forall t $$

此外,還可以加入其他約束條件,例如:

  1. 最小全職員工數量:$\sum_{i=1}^{nf} x_i \geq P$
  2. 無兼職員工若無全職員工:$B\sum_{i=1}^{nf} M_{ti}x_i \geq \sum_{i=nf+1}^{n} M_{ti}x_i \quad \forall t$

其中,$B$是一個足夠大的常數,以確保當某一時間段內無全職員工時,該時間段內也不會有兼職員工。

程式碼實作

def solve_model(M, nf, Q=None, P=None, no_part=False):
    s = newSolver('Staffing', True)
    nbt, n = len(M)-1, len(M[0])-1
    B = sum(M[t][-1] for t in range(nbt))
    x = [s.IntVar(0, B, "") for i in range(n)]
    
    # 基本約束條件
    for t in range(nbt):
        s.Add(sum([M[t][i] * x[i] for i in range(n)]) >= M[t][-1])
        
    if Q:
        for i in range(n):
            s.Add(x[i] >= Q[i])
            
    if P:
        s.Add(sum(x[i] for i in range(nf)) >= P)
        
    if no_part:
        for t in range(nbt):
            s.Add(B*sum([M[t][i] * x[i] for i in range(nf)]) >= sum([M[t][i] * x[i] for i in range(nf, n)]))
            
    s.Minimize(sum(x[i]*M[-1][i] for i in range(n)))
    rc = s.Solve()
    return rc, ObjVal(s), SolVal(x)

內容解密:

  1. solve_model 函式接收輸入引數,包括排班矩陣 $M$、全職班次數量 $nf$,以及可選的最小班次人數 $Q$、最小全職人數 $P$ 和是否允許無全職員工時有兼職員工的旗標 no_part
  2. 使用 newSolver 建立一個新的求解器例項,並定義整數變數 $x_i$ 表示各班次的員工數量。
  3. 透過迴圈為每個時間段新增基本約束條件,確保總人數滿足需求。
  4. 根據輸入引數新增額外的約束條件,例如最小全職員工數量和無兼職員工若無全職員工的條件。
  5. 設定目標函式為最小化總成本,並呼叫求解器進行求解。

工作坊排程問題

工作坊排程問題涉及將多項任務分配到不同的機器上,並確定每項任務的開始時間,以最小化整體完成時間。

數學模型建立

定義變數$x_{ik}$表示任務$i$在機器$k$上的開始時間。約束條件包括:

  1. 任務順序約束:對於任務$i$,其在機器$p_{i,k}$上的開始時間加上持續時間應小於或等於在機器$p_{i,k+1}$上的開始時間。 $$ x_{i,p_{i,k}} + d_{i,k} \leq x_{i,p_{i,k+1}} $$
  2. 機器衝突約束:同一台機器上不能同時執行多項任務。這通常需要引入二元變數來表示任務之間的先後順序,並使用大$M$法來建模。

程式碼實作與挑戰

工作坊排程問題的程式碼實作比員工排班問題更為複雜,涉及更多的變數和約束條件。特別是在處理機器衝突約束時,需要使用二元變數和大$M$法,這增加了模型的複雜度和求解難度。

任務排程問題的高階排程技術

任務排程的數學模型

任務排程問題是一類別典型的混合整數規劃問題,涉及多台機器和多個任務,需要確定每個任務在各台機器上的執行順序和時間,以最小化整體的完成時間。

變數定義

  • $x_{ik}$:任務 $i$ 在機器 $k$ 上的開始時間
  • $d_{ik}$:任務 $i$ 在機器 $k$ 上的執行時間
  • $z_{ijk}$:二元變數,表示任務 $i$ 是否在任務 $j$ 之前在機器 $k$ 上執行
  • $T$:所有任務的完成時間上限

目標函式

最小化 $T$,即所有任務的最晚完成時間。

約束條件

  1. 任務順序約束:對於每個任務,其在不同機器上的執行順序必須按照預定的順序進行。 [ x_{ip} + d_{ik} \leq x_{i(p+1)} ]

  2. 機器衝突約束:對於在同一台機器上執行的任意兩個任務,必須確保它們不會同時執行。 [ x_{ik} + d_{ik} - Mz_{ijk} \leq x_{jk} ] [ x_{jk} + d_{jk} - M(1-z_{ijk}) \leq x_{ik} ] 其中 $M$ 是一個足夠大的常數。

  3. 完成時間約束:每個任務的完成時間不得超過 $T$。 [ x_{ip} + d_{ip} \leq T ]

可執行模型

以下是以 Python 實作的任務排程模型,使用 Google OR-Tools 的線性規劃求解器。

from ortools.linearsolver import pywraplp

def solve_model(D):
    # 建立求解器
    s = pywraplp.Solver('JobShopScheduling', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
    
    nJ, nM = len(D), len(D[0])
    M = sum([D[i][k][1] for i in range(nJ) for k in range(nM)])
    
    # 定義變數
    x = [[s.NumVar(0, M, '') for _ in range(nM)] for _ in range(nJ)]
    p = [[D[i][k][0] for k in range(nM)] for i in range(nJ)]
    d = [[D[i][k][1] for k in range(nM)] for i in range(nJ)]
    z = [[[s.IntVar(0, 1, '') for _ in range(nM)] for _ in range(nJ)] for _ in range(nJ)]
    T = s.NumVar(0, M, '')
    
    # 新增約束
    for i in range(nJ):
        for k in range(nM):
            s.Add(x[i][p[i][k]] + d[i][k] <= T)
            
            for j in range(nJ):
                if i != j:
                    s.Add(z[i][j][k] + z[j][i][k] == 1)
                    s.Add(x[i][p[i][k]] + d[i][k] - M * z[i][j][p[i][k]] <= x[j][p[i][k]])
                    s.Add(x[j][p[j][k]] + d[j][k] - M * z[j][i][p[j][k]] <= x[i][p[j][k]])
                    
        for k in range(nM - 1):
            s.Add(x[i][p[i][k]] + d[i][k] <= x[i][p[i][k + 1]])
    
    # 設定目標函式
    s.Minimize(T)
    
    # 求解模型
    rc = s.Solve()
    
    return rc, T.solution_value(), [[x[i][k].solution_value() for k in range(nM)] for i in range(nJ)]

#### 內容解密:
此程式碼實作了一個任務排程問題的數學模型使用混合整數規劃來最小化整體完成時間變數 `x`、`z`、`T` 分別表示任務的開始時間任務之間的順序關係以及整體完成時間約束條件確保了任務在不同機器上的正確執行順序以及避免機器之間的衝突

### 高階排程技術

在實際應用中任務排程問題往往涉及更複雜的條件和限制例如資源限制優先順序動態到達時間等為瞭解決這些問題需要採用更先進的技術如切斷法列生成法等

#### 切斷法(Cutting Stock Problem)

切斷法是一種常見的技術用於解決資源分配和排程問題它涉及將大問題分解成較小的子問題並透過迭代求解這些子問題來獲得整體的最優解

```plantuml
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 切斷法Cutting Stock Problem

rectangle "分解" as node1
rectangle "求解" as node2
rectangle "整合" as node3

node1 --> node2
node2 --> node3

@enduml

此圖示展示了切斷法的基本流程,將大問題分解為多個子問題並分別求解,最終整合得到整體最優解。

程式碼實作與解析

# 簡化的切斷法範例
def cutting_stock_problem():
    # 假設的初始資料
    rolls = [100, 200, 300]  # 現有卷材長度
    demands = [50, 75, 100]  # 客戶需求
    
    # 簡化的求解過程
    solution = []
    for demand in demands:
        # 簡化處理,實際上需要更複雜的邏輯
        solution.append(min(rolls, key=lambda x: abs(x - demand)))
    
    return solution

#### 內容解密:
此範例展示了一個簡化的切斷法應用用於滿足客戶對不同尺寸卷材的需求透過遍歷客戶需求並選擇最接近的現有卷材尺寸獲得了一個簡化的解決方案實際應用中需要更複雜的演算法來最佳化資源分配

## 切割庫存問題的數學模型與求解

### 問題描述

在紙張生產中客戶訂單的寬度需求各異如何從大卷紙張中切割出滿足客戶需求的小卷同時最小化浪費是切割庫存問題的核心表7-1展示了一個隨機的訂單例項第一列是訂單數量第二列是所需的寬度以產品卷寬度的百分比表示)。

### 模型建立

最初人們透過指定各種切割模式來解決這一問題但隨著技術的進步我們可以將決定切割模式的任務交給求解器

#### 決策變數

我們定義兩個主要的決策變數
- $x_{i,j}$:表示從第j個大卷中切割出多少個滿足第i個訂單寬度需求的小卷
- $y_j$:表示第j個大卷是否被使用

#### 目標函式

目標是最小化使用的產品卷數量為了避免求解器給出不連續的卷號我們使用一個小技巧使每個新卷的成本比前一個高
\[ \min \sum_{j=1}^{K} j \cdot y_j \]
同時我們引入一個輔助變數$t$,並新增約束$t = \sum_{j=1}^{K} y_j$,以便獲得實際使用的卷數

#### 約束條件

1. **滿足客戶需求**對於每個訂單$i$,我們需要確保從所有使用的卷中切割出的相應寬度的小卷數量不少於訂單需求$b_i$。
\[ \sum_{j=1}^{K} x_{i,j} \geq b_i, \forall i \]
2. **寬度限制**對於每個大卷$j$,從中切割出的所有小卷的總寬度不能超過大卷的寬度假設為100%)。
\[ \sum_{i=1}^{N} w_i \cdot x_{i,j} \leq 100 \cdot y_j, \forall j \]
這同時確保瞭如果$y_j = 0$,$x_{i,j} = 0$,因為沒有被使用的卷不應有任何切割

### 可執行模型

列表7-1展示瞭如何將上述模型轉換為可執行的程式碼使用Python和某個最佳化求解器)。

```python
def solve_model(D):
    s, n = newSolver('CuttingStock', True), len(D)
    k, b = bounds(D)
    y = [s.IntVar(0, 1, '') for _ in range(k[1])]
    x = [[s.IntVar(0, b[i], '') for _ in range(k[1])] for i in range(n)]
    w = [s.NumVar(0, 100, '') for _ in range(k[1])]
    nb = s.IntVar(k[0], k[1], '')
    
    for i in range(n):
        s.Add(sum(x[i][j] for j in range(k[1])) >= D[i][0])
    
    for j in range(k[1]):
        s.Add(sum(D[i][1] * x[i][j] for i in range(n)) <= 100 * y[j])
        s.Add(100 * y[j] - sum(D[i][1] * x[i][j] for i in range(n)) == w[j])
        if j < k[1] - 1:
            s.Add(sum(x[i][j] for i in range(n)) >= sum(x[i][j + 1] for i in range(n)))
    
    Cost = s.Sum((j + 1) * y[j] for j in range(k[1]))
    s.Add(nb == s.Sum(y[j] for j in range(k[1])))
    s.Minimize(Cost)
    
    rc = s.Solve()
    rnb = SolVal(nb)
    return rc, rnb, rolls(rnb, SolVal(x), SolVal(w), D), SolVal(w)

程式碼解析:

  1. 變數初始化:根據訂單資料D,初始化求解器s、變數n(訂單數量)、kb(與卷數相關的界限)。
  2. 決策變數定義:定義整數變數y(表示卷是否被使用)、x(表示從每個卷中切割出滿足每個訂單的小卷數量)和w(與浪費相關的變數)。
  3. 約束新增:新增滿足客戶需求和寬度限制的約束。
  4. 目標函式:定義最小化使用的卷數的目標函式,並新增輔助約束以獲得實際使用的卷數。
  5. 求解與結果:呼叫求解器s.Solve(),並傳回求解狀態、使用的卷數、切割方案等結果。

內容解密:

  • 該程式碼首先根據輸入資料初始化必要的變數和求解器。
  • 然後定義了三類別主要的決策變數:yxw,分別對應於卷的使用狀態、切割方案和浪費情況。
  • 約束條件確保了客戶需求得到滿足,同時也限制了從每個大卷中切割出的小卷總寬度不超過大卷寬度。
  • 目標函式設計為最小化使用的產品卷數量,並透過一個小技巧避免了不連續的卷號。
  • 最後,程式碼呼叫最佳化求解器得出最優解,並傳回相關結果。