本文介绍的内容,均参考自Zeng Bo 于2013 年在Operations Research Letters 上发表的论文,引用信息如下:
Bo Zeng, Long Zhao,Solving two-stage robust optimization problems using a column-and-constraint generation method,Operations Research Letters,Volume 41, Issue 5,2013,Pages 457-461,ISSN0167-6377,https://doi.org/10.1016/j.orl.2013.05.003.
原文链接如下:
(https://www.sciencedirect.com/science/article/pii/S0167637713000618)
此外,零基础细致讲解请参阅:鲁棒优化| C&CG算法求解两阶段鲁棒优化:全网最完整、最详细的【入门-完整推导-代码实现】笔记 - 运小筹的文章 - 知乎
https://zhuanlan.zhihu.com/p/534285185
本文只对算例进行说明。
文中的算例是一个location-transportation问题。
即:货物存放在3个仓库(SUPPLY)中,有3个客户(DEMAND)。现在需要通过优化方法来安排货物的配送情况。其中第一阶段确定仓库启用情况和存储的货物量,第二阶段确定运输情况。涉及如下参数和变量:
仓库的固定成本: f i f_i fi, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
仓库的单位容量使用成本: a i a_i ai, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
仓库最大容量: k i k_i ki, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
客户基础需求: d ‾ j \underline{d}_j dj, \quad j = 0 , 1 , 2 j=0,1,2 j=0,1,2
客户需求的最大误差: d ~ j \tilde{d}_j d~j, \quad j = 0 , 1 , 2 j=0,1,2 j=0,1,2
货物从仓库 i i i到客户 j j j的单位运输成本: c i j c_{ij} cij, \quad i = 0 , 1 , 2 j = 0 , 1 , 2 i=0,1,2\quad j=0,1,2 i=0,1,2j=0,1,2
仓库状态变量(布尔型): y i y_i yi, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
仓库存储的货物量: z i z_i zi, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
货物运输计划: x i j x_{ij} xij, \quad i = 0 , 1 , 2 j = 0 , 1 , 2 i=0,1,2\quad j=0,1,2 i=0,1,2j=0,1,2
π i \pi_i πi, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
θ j \theta_j θj, \quad j = 0 , 1 , 2 j=0,1,2 j=0,1,2
主问题目标函数的辅助变量: η \eta η
线性化过程中的辅助变量:
v i v_i vi, \quad i = 0 , 1 , 2 i=0,1,2 i=0,1,2
w j w_j wj, \quad j = 0 , 1 , 2 j=0,1,2 j=0,1,2
h i j h_{ij} hij, \quad i = 0 , 1 , 2 j = 0 , 1 , 2 i=0,1,2\quad j=0,1,2 i=0,1,2j=0,1,2
D = { d : d j = d ‾ j + g j d ~ j , g j ∈ [ 0 , 1 ] , j = 0 , 1 , 2 ; g 0 + g 1 ≤ 1.2 , g 0 + g 1 + g 2 ≤ 1.8 } D={\{d:d_j=\underline{d}_j+g_j\tilde{d}_j},g_j\in [0,1],j=0,1,2;g_0+g_1\leq1.2,g_0+g_1+g_2\leq1.8\} D={d:dj=dj+gjd~j,gj∈[0,1],j=0,1,2;g0+g1≤1.2,g0+g1+g2≤1.8}
min y , z ∑ i ( f i y i + a i z i ) + max d ∈ D min x ∑ i ∑ j c i j x i j \min_{y,z}\sum_i{(f_iy_i+a_iz_i)}+\max_{d\in D}\min_{x}\sum_i\sum_j{c_{ij}x_{ij}} y,zmini∑(fiyi+aizi)+d∈Dmaxxmini∑j∑cijxij
z i ≤ k i y i , ∀ i z_i \leq k_iy_i, \quad \forall i zi≤kiyi,∀i
注意到论文中4.2节上面一段倒数第二行提到,为了保证存在可行解(即:货物量大于需求量),需要添加一条约束,这里我们采用比论文里( ∑ i k i ≥ max { ∑ j d j : d ∈ D } \sum_{i}k_i\geq\max\{\sum_{j}d_j:d\in D\} ∑iki≥max{∑jdj:d∈D})更强的约束: ∑ i z i ≥ max { ∑ j d j : d ∈ D } \sum_{i}z_i\geq\max\{\sum_{j}d_j:d\in D\} i∑zi≥max{j∑dj:d∈D}上式不等号右侧很容易计算出具体值为: 206 + 274 + 220 + 1.8 ∗ 40 = 772 206+274+220+1.8*40=772 206+274+220+1.8∗40=772
y i ∈ { 0 , 1 } , z i ≥ 0 ∀ i y_i\in \{0,1\},z_i\geq 0 \quad \forall i yi∈{0,1},zi≥0∀i
∑ j x i j ≤ z i ∀ i \sum_{j}x_{ij}\leq z_i \quad \forall i j∑xij≤zi∀i ∑ i x i j ≥ d j ∀ j \sum_{i}x_{ij}\geq d_j \quad \forall j i∑xij≥dj∀j x i j ≥ 0 , ∀ i , j x_{ij}\geq 0, \quad \forall i,j xij≥0,∀i,j
由于第二阶段目标函数是一个max-min双层优化问题,所以需要使用对偶理论将内层的min问题转化为max问题,这样第二阶段目标函数就可以转化为一个单层max问题。下面使用拉格朗日对偶推导KKT条件:
1.列写内层原问题(约束写成 ≤ 0 \leq0 ≤0的形式): min x ∑ i ∑ j c i j x i j \min_{x}\sum_i\sum_j{c_{ij}x_{ij}} xmini∑j∑cijxij s . t . s.t. s.t. ∑ j x i j − z i ≤ 0 ∀ i → π i ≥ 0 \sum_{j}x_{ij}-z_i\leq 0 \quad \forall i \quad \quad \rightarrow \pi_i \geq0 j∑xij−zi≤0∀i→πi≥0 ∑ i − x i j + d j ≤ 0 ∀ j → θ j ≥ 0 \sum_{i}-x_{ij}+d_j\leq 0 \quad \forall j \quad \quad \rightarrow \theta_j \geq0 i∑−xij+dj≤0∀j→θj≥0 − x i j ≤ 0 , ∀ i , j → λ i j ≥ 0 -x_{ij}\leq 0, \quad \forall i,j \quad \quad \rightarrow \lambda_{ij} \geq0 −xij≤0,∀i,j→λij≥0
2.写出拉格朗日函数: L = ∑ i ∑ j c i j x i j + ∑ i π i ( ∑ j x i j − z i ) + ∑ j θ j ( ∑ i − x i j + d j ) + ∑ i ∑ j λ i j ( − x i j ) L=\sum_{i}\sum_{j}{c_{ij}x_{ij}}+\sum_{i}\pi_i(\sum_{j}x_{ij}- z_i)+\sum_{j}\theta_j(\sum_{i}-x_{ij}+d_j)+\sum_{i}\sum_{j}{\lambda_{ij}(-x_{ij})} L=i∑j∑cijxij+i∑πi(j∑xij−zi)+j∑θj(i∑−xij+dj)+i∑j∑λij(−xij)
3.根据变量合并“同类项”: L = ∑ i ∑ j ( c i j + π i − θ j − λ i j ) x i j − ∑ i π i z i + ∑ j θ j d j L=\sum_{i}\sum_{j}(c_{ij}+\pi_i-\theta_j-\lambda_{ij})x_{ij}-\sum_{i}\pi_iz_i+\sum_{j}\theta_jd_j L=i∑j∑(cij+πi−θj−λij)xij−i∑πizi+j∑θjdj
4.求拉格朗日函数的下确界: inf x ∑ i ∑ j ( c i j + π i − θ j − λ i j ) x i j − ∑ i π i z i + ∑ j θ j d j \inf_{x}\sum_{i}\sum_{j}(c_{ij}+\pi_i-\theta_j-\lambda_{ij})x_{ij}-\sum_{i}\pi_iz_i+\sum_{j}\theta_jd_j xinfi∑j∑(cij+πi−θj−λij)xij−i∑πizi+j∑θjdj
5.获得对偶目标函数和对偶约束:
(1)上式后面两项与原问题变量 x x x无关,所以为对偶问题的目标函数,即 max π , θ − ∑ i π i z i + ∑ j θ j d j \max_{\pi, \theta}-\sum_{i}\pi_iz_i+\sum_{j}\theta_jd_j π,θmax−i∑πizi+j∑θjdj
(2)要想上一步4中的式子可以取得最小值,只有 c i j + π i − θ j − λ i j = 0 c_{ij}+\pi_i-\theta_j-\lambda_{ij}=0 cij+πi−θj−λij=0注意到 λ i j ≥ 0 \lambda_{ij} \geq 0 λij≥0, 所以 c i j + π i − θ j ≥ 0 c_{ij}+\pi_i-\theta_j \geq 0 cij+πi−θj≥0
6.列写内层对偶问题: max π , θ − ∑ i π i z i + ∑ j θ j d j \max_{\pi, \theta}-\sum_{i}\pi_iz_i+\sum_{j}\theta_jd_j π,θmax−i∑πizi+j∑θjdj s . t . s.t. s.t. c i j + π i − θ j ≥ 0 ∀ i , j c_{ij}+\pi_i-\theta_j \geq 0 \quad \forall i,j cij+πi−θj≥0∀i,j π i ≥ 0 ∀ i \pi_i \geq0 \quad \forall i πi≥0∀i θ j ≥ 0 ∀ j \theta_j \geq0 \quad \forall j θj≥0∀j
可以发现,对偶问题目标函数存在连续变量乘积项,不方便使用求解器求解,我们使用论文中提到的KKT条件进行求解,这样便可规避非线性项对求解效率的影响。根据Stephen Boyd的凸优化教材《Convex Optimization》第243页5.5.3节内容,我们可以轻松得到该问题的KKT条件:
①原问题可行性: ∑ j x i j ≤ z i ∀ i \sum_{j}x_{ij}\leq z_i \quad \forall i j∑xij≤zi∀i ∑ i x i j ≥ d j ∀ j \sum_{i}x_{ij}\geq d_j \quad \forall j i∑xij≥dj∀j x i j ≥ 0 , ∀ i , j x_{ij}\geq 0, \quad \forall i,j xij≥0,∀i,j
②对偶问题可行性: c i j + π i − θ j ≥ 0 ∀ i , j c_{ij}+\pi_i-\theta_j \geq 0 \quad \forall i,j cij+πi−θj≥0∀i,j π i ≥ 0 ∀ i \pi_i \geq0 \quad \forall i πi≥0∀i θ j ≥ 0 ∀ j \theta_j \geq0 \quad \forall j θj≥0∀j
③互补松弛条件: ( ∑ j x i j − z i ) π i = 0 (\sum_{j}x_{ij}- z_i)\pi_i=0 (j∑xij−zi)πi=0 ( ∑ i − x i j + d j ) θ j = 0 (\sum_{i}-x_{ij}+d_j)\theta_j=0 (i∑−xij+dj)θj=0 ( − x i j ) λ i j = 0 (-x_{ij})\lambda_{ij}=0 (−xij)λij=0注意到 c i j + π i − θ j − λ i j = 0 c_{ij}+\pi_i-\theta_j-\lambda_{ij}=0 cij+πi−θj−λij=0代入上式,即: ( c i j + π i − θ j ) ( − x i j ) = 0 (c_{ij}+\pi_i-\theta_j)(-x_{ij})=0 (cij+πi−θj)(−xij)=0可以看到这3个互补松弛条件都是非线性的,所以论文里采用了式(21)所示的线性化方法(同时也应注意式(21)第二项的左边是大于等于零的,而我们推导的互补松弛条件括号内是小于等于零的,所以要加一个负号),即:
π i ≤ M v i \pi_i\leq Mv_i πi≤Mvi z i − ∑ j x i j ≤ M ( 1 − v i ) z_i-\sum_{j}x_{ij}\leq M(1-v_i) zi−j∑xij≤M(1−vi) θ j ≤ M w j \theta_j\leq Mw_j θj≤Mwj ∑ i x i j − d j ≤ M ( 1 − w j ) \sum_{i}x_{ij}-d_j\leq M(1-w_j) i∑xij−dj≤M(1−wj) x i j ≤ M h i j x_{ij}\leq Mh_{ij} xij≤Mhij c i j + π i − θ j ≤ M ( 1 − h i j ) c_{ij}+\pi_i-\theta_j\leq M(1-h_{ij}) cij+πi−θj≤M(1−hij)
④稳定性条件和对偶约束相同
综上,我们得到了所有KKT条件。
根据论文中的算法伪代码,主问题可表述为:
M P = min y , z , η , x ∑ i ( f i y i + a i z i ) + η MP=\min_{y,z,\eta,x}\sum_i{(f_iy_i+a_iz_i)}+\eta MP=y,z,η,xmini∑(fiyi+aizi)+η s . t . s.t. s.t. z i ≤ k i y i , ∀ i z_i \leq k_iy_i, \quad \forall i zi≤kiyi,∀i ∑ i z i ≥ 772 \sum_{i}z_i\geq772 i∑zi≥772 η ∈ R \eta \in R η∈R y i ∈ { 0 , 1 } , z i ≥ 0 ∀ i y_i\in \{0,1\},z_i\geq 0 \quad \forall i yi∈{0,1},zi≥0∀i η ≥ ∑ i ∑ j c i j x i j n ∀ n = 1 , . . . N \eta\geq\sum_{i}\sum_{j}c_{ij}{x_{ij}}^n \quad \forall n=1,...N η≥i∑j∑cijxijn∀n=1,...N ∑ j x i j n ≤ z i ∀ i , n = 1 , . . . N \sum_{j}{x_{ij}}^n\leq z_i \quad \forall i,n=1,...N j∑xijn≤zi∀i,n=1,...N ∑ i x i j n ≥ d j ∗ n ∀ j , n = 1 , . . . N \sum_{i}{x_{ij}}^n\geq{d^\ast_j}^n \quad \forall j,n=1,...N i∑xijn≥dj∗n∀j,n=1,...N最后三个约束表示的是每一次迭代生成的约束(即Constraint),其中 N N N为迭代次数, x i j n {x_{ij}}^n xijn为每次迭代生成的变量(即Column)(这也是Column-and-constraint generation (C&CG) algorithm名字的由来), d j ∗ n {d^\ast_j}^n dj∗n为子问题的最优解(相当于论文中伪代码里的 u k + 1 ∗ u^\ast_{k+1} uk+1∗),传递到主问题中被当做参数。
根据前文KKT条件的推导结果,以及不确定集的定义,我们可以得到如下子问题:
S P = max d , x , g , θ , π , v , w , h ∑ i ∑ j c i j x i j SP=\max_{d,x,g,\theta,\pi,v,w,h}\sum_i\sum_j{c_{ij}x_{ij}} SP=d,x,g,θ,π,v,w,hmaxi∑j∑cijxij s . t . s.t. s.t. ∑ j x i j ≤ z i ∗ ∀ i \sum_{j}x_{ij}\leq z^\ast_i \quad \forall i j∑xij≤zi∗∀i ∑ i x i j ≥ d j ∀ j \sum_{i}x_{ij}\geq d_j \quad \forall j i∑xij≥dj∀j x i j ≥ 0 , ∀ i , j x_{ij}\geq 0, \quad \forall i,j xij≥0,∀i,j c i j + π i − θ j ≥ 0 ∀ i , j c_{ij}+\pi_i-\theta_j \geq 0 \quad \forall i,j cij+πi−θj≥0∀i,j π i ≥ 0 ∀ i \pi_i \geq0 \quad \forall i πi≥0∀i θ j ≥ 0 ∀ j \theta_j \geq0 \quad \forall j θj≥0∀j d j = d ‾ j + g j d ~ j ∀ j d_j=\underline{d}_j+g_j\tilde{d}_j \quad \forall j dj=dj+gjd~j∀j g j ∈ [ 0 , 1 ] ∀ j g_j\in [0,1] \quad \forall j gj∈[0,1]∀j g 0 + g 1 ≤ 1.2 g_0+g_1\leq1.2 g0+g1≤1.2 g 0 + g 1 + g 2 ≤ 1.8 g_0+g_1+g_2\leq1.8 g0+g1+g2≤1.8 π i ≤ M v i ∀ i \pi_i\leq Mv_i\quad \forall i πi≤Mvi∀i z i ∗ − ∑ j x i j ≤ M ( 1 − v i ) ∀ i z^\ast_i-\sum_{j}x_{ij}\leq M(1-v_i)\quad \forall i zi∗−j∑xij≤M(1−vi)∀i θ j ≤ M w j ∀ j \theta_j\leq Mw_j\quad \forall j θj≤Mwj∀j ∑ i x i j − d j ≤ M ( 1 − w j ) ∀ j \sum_{i}x_{ij}-d_j\leq M(1-w_j)\quad \forall j i∑xij−dj≤M(1−wj)∀j x i j ≤ M h i j ∀ i , j x_{ij}\leq Mh_{ij}\quad \forall i,j xij≤Mhij∀i,j c i j + π i − θ j ≤ M ( 1 − h i j ) ∀ i , j c_{ij}+\pi_i-\theta_j\leq M(1-h_{ij})\quad \forall i,j cij+πi−θj≤M(1−hij)∀i,j其中, z i ∗ z^\ast_i zi∗为主问题的最优解,传递到子问题中被当做参数。
本文对Zeng Bo论文中的算例进行了细致的说明,通过使用AMPL软件调用CPLEX求解器,得到了与论文中相同的结果。
论文中的结果:
我的结果:
具体的代码可以访问我的Github:https://github.com/jysw980/Paper-reproduced-Zeng-Bo-2013-paper-about-CCG-Algorithm.git
本文的撰写和程序的编写过程中,得到了师兄、同门的大力支持,在此向他们表示感谢!
最后,希望这篇文章能够帮助到大家!
20240702 更新 python代码,使用的是gurobipy
from gurobipy import Model, GRB, quicksum
import numpy as np
# Data
supply_data = {
"S1": {"f": 400, "a": 18, "k": 800},
"S2": {"f": 414, "a": 25, "k": 800},
"S3": {"f": 326, "a": 20, "k": 800}
}
demand_data = {
"D1": {"d_L": 206, "d_T": 40},
"D2": {"d_L": 274, "d_T": 40},
"D3": {"d_L": 220, "d_T": 40}
}
costs = {
("S1", "D1"): 22, ("S1", "D2"): 33, ("S1", "D3"): 24,
("S2", "D1"): 33, ("S2", "D2"): 23, ("S2", "D3"): 30,
("S3", "D1"): 20, ("S3", "D2"): 25, ("S3", "D3"): 27
}
# Big-M parameter
M = 10000
# Initialize model
master = Model("Master")
sub = Model("Sub")
# Sets
supply = list(supply_data.keys())
demand = list(demand_data.keys())
iter = np.arange(1,4,1)
# Global Parameters
f = {i: supply_data[i]["f"] for i in supply}
a = {i: supply_data[i]["a"] for i in supply}
k = {i: supply_data[i]["k"] for i in supply}
d_L = {j: demand_data[j]["d_L"] for j in demand}
d_T = {j: demand_data[j]["d_T"] for j in demand}
c = costs
# Master problem variables
y = master.addVars(supply, vtype=GRB.BINARY, name="y")
z = master.addVars(supply, vtype=GRB.CONTINUOUS, name="z", lb=0.0)
eta = master.addVar(vtype=GRB.CONTINUOUS, name="eta")
x_gen = master.addVars(supply, demand, iter, vtype=GRB.CONTINUOUS, name="x_gen", lb=0.0)
# Subproblem variables
x = sub.addVars(supply, demand, vtype=GRB.CONTINUOUS, name="x", lb=0.0)
d = sub.addVars(demand, vtype=GRB.CONTINUOUS, name="d")
g = sub.addVars(demand, vtype=GRB.CONTINUOUS, name="g", lb=0.0, ub=1.0)
theta = sub.addVars(demand, vtype=GRB.CONTINUOUS, name="theta", lb=0.0)
Pi = sub.addVars(supply, vtype=GRB.CONTINUOUS, name="pi", lb=0.0)
v = sub.addVars(supply, vtype=GRB.BINARY, name="v")
w = sub.addVars(demand, vtype=GRB.BINARY, name="w")
h = sub.addVars(supply, demand, vtype=GRB.BINARY, name="h")
# Objective for master problem
master.setObjective(quicksum(f[i]*y[i] + a[i]*z[i] for i in supply) + eta, GRB.MINIMIZE)
# Constraints for master problem
master.addConstrs((z[i] <= k[i]*y[i] for i in supply), name="C1")
master.addConstr(quicksum(z[i] for i in supply) >= 772, name="C2")
master.update()
# Objective for subproblem
sub.setObjective(quicksum(c[i,j]*x[i,j] for i in supply for j in demand), GRB.MAXIMIZE)
# Constraints for subproblem
# sub.addConstrs((quicksum(x[i,j] for j in demand) <= z_star[i] for i in supply), name="C3")
sub.addConstrs((quicksum(x[i,j] for i in supply) >= d[j] for j in demand), name="C4")
sub.addConstrs((theta[j] - Pi[i] <= c[i,j] for i in supply for j in demand), name="C5")
sub.addConstrs((d[j] == d_L[j] + d_T[j]*g[j] for j in demand), name="C6")
sub.addConstr(quicksum(g[j] for j in demand if j != "D3") <= 1.2, name="C7")
sub.addConstr(quicksum(g[j] for j in demand) <= 1.8, name="C8")
sub.addConstrs((Pi[i] <= M*v[i] for i in supply), name="C9")
# sub.addConstrs((z_star[i] - quicksum(x[i,j] for j in demand) <= M*(1-v[i]) for i in supply), name="C10")
sub.addConstrs((theta[j] <= M*w[j] for j in demand), name="C11")
sub.addConstrs((quicksum(x[i,j] for i in supply) - d[j] <= M*(1-w[j]) for j in demand), name="C12")
sub.addConstrs((x[i,j] <= M*h[i,j] for i in supply for j in demand), name="C13")
sub.addConstrs((c[i,j] - theta[j] + Pi[i] <= M*(1-h[i,j]) for i in supply for j in demand), name="C14")
sub.update()
#Cancel Log To Console
master.setParam('LogToConsole', 0)
sub.setParam('LogToConsole', 0)
# Initialization
LB = -float('inf')
UB = float('inf')
N = 0
print("\n----------------------Main Loop of C&CG------------------------\n")
while UB - LB > 1e-4:
print(f"\n-------------------------Iteration {N+1}---------------------------\n")
# Solve master problem
master.optimize()
LB = master.ObjVal
sub.addConstrs((quicksum(x[i,j] for j in demand) <= z[i].X for i in supply), name="C3")
sub.addConstrs((z[i].X - quicksum(x[i,j] for j in demand) <= M*(1-v[i]) for i in supply), name="C10")
sub.update()
# Solve subproblem
sub.optimize()
UB = min(UB, LB - eta.X + sub.ObjVal)
print(f"\niter: {N+1} LB: {LB:.1f} UB: {UB:.1f} Gap: {100 * (UB - LB) / UB:.2f}%\n")
if UB - LB <= 1e-4:
break
N += 1
master.addConstr(eta >= quicksum(c[i,j]*x_gen[i,j,N] for i in supply for j in demand), name=f"gen_C1_{N}")
master.addConstrs((quicksum(x_gen[i,j,N] for j in demand) <= z[i] for i in supply), name=f"gen_C2_{N}")
master.addConstrs((quicksum(x_gen[i,j,N] for i in supply) >= d[j].X for j in demand), name=f"gen_C3_{N}")
master.update()
for i in supply:
sub.remove(sub.getConstrByName(f'C3[{i}]'))
sub.remove(sub.getConstrByName(f'C10[{i}]'))
sub.update()
print("\n-------------------------The optimal solution has been found!---------------------------\n")
print(f"Optimal value is {LB:.1f}\n")
print(f"Optimal solution:")
for v in master.getVars():
print(f"{v.varName}: {v.x}")