Dantzig&Wolfe分解(簡稱DW分解)[1]是一種列生成技巧,可以把一類特殊形式線性規劃問題分解成若干子問題進行求解.
問題描述
我們考慮如下形式的線性規劃問題:
其中,
,
.
說明
- 上述規劃共
組約束. 第一組約束包含了所有的決策變量, 稱為連接約束(Linking constraints), 接下來是
組獨立的約束.
-
都是向量.
- 上述規劃如果寫成標準形式
, 其系數矩陣
的維度是
. 我們發現
實際上是相對稀疏的. 當
,
的非常大時, 矩陣的規模可能大到無法直接求解上述規劃. 在這種情況下, 我們考慮把它分解成多個子問題進行求解(DW分解).
應用場景
例1 一個公司有
個部門, 各部門有獨立的約束, 部門之間也有約束. 目標是最小化所有部門的總成本.
例2 一個零售公司有
個倉庫, 需要決定每個倉庫存放的商品. 每個倉庫中對商品有一些約束. 商品關于各倉庫也需要滿足一些約束. 目標是最小化總的成本(例如配送成本/時效等).
主問題
令. 如果
是有界的, 那么
可以表示成
頂點的凸組合. 設
代表
的頂點(Extreme point), 則存在
且
使得
在一般情況下(無界或有界時), 它可以用頂點和極射線的線性組合來表示(參考 Minkowski表示定理). 具體來說, 令
代表極射線(Extreme ray). 則存在
且
,
使得
假設我們枚舉所有的頂點和極射線. 把上式代入原問題得到主問題(Master problem):
主問題
說明
-
,
是決策變量.
- 約束的數量為
(第一個等式包含了
個約束). 原問題的約束數量是
. 因此主問題的約束數量明顯減少了.
- 但是主問題的變量顯著增加了(
對應所有的頂點和極射線). 與列生成技巧相似, 主問題一開始只考慮兩個可行解(對應頂點和極射線). 通過求解子問題得到新的頂點或極射線.
- 在實際問題中, 一般情況下
都是有界的. 此時主問題可以簡化成如下形式.
子問題
定義主問題的對偶變量,
,
. 我們計算變量
的Reduced Cost:
其中,
分別代表
的reduced cost. 當
,
時得到原問題的最優解. 因此在子問題中, 我們需要計算
(或
)使得
的值盡可能小.
子問題 -
求解子問題時考慮三種情況.
- 最優解的目標值為
. 此時
, 子問題的解是極射線
. 我們把
加入到主問題進行求解.
- 最優解的目標值有界且
. 此時
, 子問題的解是頂點
. 我們把
加入到主問題進行求解.
- 最優解的目標值有界且
. 此時
(注意到實際上
), 得到最優解.
例子: 一個選品問題
考慮個零售品牌商, 每個零售品牌商有自己的商品(SKU), 例如可口可樂, 雪碧和芬達對應同一家品牌商. 一家電商平臺需要從
個品牌中選擇一些商品做營銷活動. 已知每個商品的營銷成本, 商品預期的收益和營銷的預算. 在總營銷費用不超過預算且每個品牌選中商品數量有限制的前提下, 如何選擇商品使得預期的收益最大化?
我們先考慮一個直觀的數學模型.
indices
-
-- 品牌
-
-- 商品
parameters
-
-- 商品
是否屬于品牌
-
-- 商品
的預期收益
-
-- 商品
的營銷成本
-
-- 選中品牌
的商品數量上限
-
-- 營銷費用的總預算
decision variables
-
-- 是否選擇商品
模型1
當品牌和商品數量較大時, 例如1000品牌和10萬商品, 這時參數的規模是1億, 因此直接求解非常困難. 注意到每個品牌的商品是不一樣的, 因而矩陣
非常稀疏, 我們可以把每個品牌中的商品分開考慮, 得到如下模型.
indices
-
-- 品牌
-
-- 商品
parameters
-
-- 品牌
的商品數量
-
-- 品牌
中商品
的預期收益,
-
-- 品牌
中商品
的營銷成本,
-
-- 品牌
可以被選中的商品數量上限
-
-- 營銷費用的總預算
decision variables
-
-- 是否選擇品牌
中的商品
,
模型2
模型1和模型2本質上是相同的, 因此當品牌數和商品數量非常大時直接求解模型2依然非常困難. 下面我們使用DW分解進行求解.
令,
. 注意到
是有界的, 我們用
代表的頂點, 因此
可以被表示成如下形式:
把它代入模型2中我們得到主問題的形式.
主問題
定義對偶變量和
,
. 計算
對應的reduced cost:
注意: 主問題是最大化問題, 因此意味著可以提升主問題的目標函數值. 我們有:
- 子問題是最大化問題.
- 當所有為
時主問題達到最優.
子問題 -
初始化
對任意的, 定義向量:
顯然是每個約束
的可行解, 即
,
. 我們用
作為主問題初始化的頂點.
Remark. 前文的推導過程要求
是多面體的頂點, 但上面
的定義并不滿足此條件. 這么做可行的原因是任意可行解本身就是多面體頂點的凸組合.
求解
求解的基本步驟如下:
- 初始化主問題, 求解子問題的輸入參數
- 求解
個子問題,分別計算
對應的Reduced Cost
. 如果
, 則把對應的解
加入到主問題. (
可以理解為迭代的次數)
- 如果所有的
, 則停止迭代;否則迭代求解主問題和子問題直到滿足停止條件.
Python實現
主問題模型
# master_model.py
from ortools.linear_solver import pywraplp
class MasterModel(object):
def __init__(self, p, v, c, d):
"""
:param p: p[i][j]代表品牌i中商品j的預期收益
:param v: v[i]代表第i個子問題的解
:param c: c[i][j]代表品牌i中商品j的營銷成本
:param d: scalar, 總預算
"""
self._solver = pywraplp.Solver('MasterModel',
pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
self._p = p
self._v = v
self._c = c
self._d = d
self._la = None # 決策變量lambda
self._constraint_y = None # 約束
self._constraint_z = [] # 約束
self._solution_la = None # 計算結果
def _init_decision_variables(self):
self._la = [[]] * len(self._v)
self._solution_la = [[]] * len(self._v) # 初始化保存結果的變量
for i in range(len(self._v)):
self._la[i] = [[]] * len(self._v[i])
self._solution_la[i] = [[]] * len(self._v[i]) # 初始化保存結果的變量
for k in range(len(self._v[i])):
self._la[i][k] = self._solver.NumVar(0, 1, 'la[%d][%d]' % (i, k))
def _init_constraints(self):
self._constraint_y = self._solver.Constraint(0, self._d)
for i in range(len(self._v)):
for k in range(len(self._v[i])):
f = 0
for j in range(len(self._v[i][k])):
f += self._c[i][j] * self._v[i][k][j]
self._constraint_y.SetCoefficient(self._la[i][k], f)
self._constraint_z = [None] * len(self._v)
for i in range(len(self._v)):
self._constraint_z[i] = self._solver.Constraint(1, 1)
for k in range(len(self._la[i])):
self._constraint_z[i].SetCoefficient(self._la[i][k], 1)
def _init_objective(self):
obj = self._solver.Objective()
for i in range(len(self._v)):
for k in range(len(self._v[i])):
f = 0
for j in range(len(self._v[i][k])):
f += self._p[i][j] * self._v[i][k][j]
obj.SetCoefficient(self._la[i][k], f)
obj.SetMaximization()
def solve(self):
self._init_decision_variables()
self._init_constraints()
self._init_objective()
self._solver.Solve()
# 保存計算結果
for i in range(len(self._v)):
for k in range(len(self._v[i])):
self._solution_la[i][k] = self._la[i][k].solution_value()
def get_solution_value(self):
return self._solution_la
def get_y(self):
""" 獲取對偶變量y的值
"""
return self._constraint_y.dual_value()
def get_zi(self, i):
""" 獲取對偶變量z[i]的值
"""
return self._constraint_z[i].dual_value()
def get_obj_value(self):
res = 0
for i in range(len(self._p)):
for k in range(len(self._v[i])):
for j in range(len(self._p[i])):
res += self._solution_la[i][k] * self._p[i][j] * self._v[i][k][j]
return res
def get_solution_x(self):
""" 得到原問題的解. x[i][j] = sum(la[i][k] * v[i][k][j]) over k.
"""
x = [[]] * len(self._v)
for i in range(len(self._v)):
x[i] = [0] * len(self._v[i][0])
for i in range(len(self._v)):
for k in range(len(self._v[i])):
for j in range(len(self._v[i][k])):
x[i][j] += self._solution_la[i][k] * self._v[i][k][j]
return x
子問題模型
# sub_model.py
from ortools.linear_solver import pywraplp
import numpy as np
class SubModel(object):
""" 子問題i
"""
def __init__(self, pi, ci, y, bi):
""" 下標i忽略
:param pi: list, pi := p[i] = [p1, p2, ..., ]
:param ci: list, ci := c[i] = [c1, c2, ..., ]
:param y: scalar
:param bi: scalar
"""
self._solver = pywraplp.Solver('MasterModel',
pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
self._pi = pi
self._ci = ci
self._y = y
self._bi = bi
self._x = None # 決策變量
self._solution_x = None # 計算結果
def _init_decision_variables(self):
self._x = [None] * len(self._pi)
for j in range(len(self._pi)):
self._x[j] = self._solver.IntVar(0, 1, 'x[%d]' % j)
def _init_constraints(self):
ct = self._solver.Constraint(0, self._bi)
for j in range(len(self._pi)):
ct.SetCoefficient(self._x[j], 1)
def _init_objective(self):
obj = self._solver.Objective()
for j in range(len(self._pi)):
obj.SetCoefficient(self._x[j], self._pi[j] - self._y * self._ci[j])
obj.SetMaximization()
def solve(self):
self._init_decision_variables()
self._init_constraints()
self._init_objective()
self._solver.Solve()
self._solution_x = [s.solution_value() for s in self._x]
def get_solution_x(self):
return self._solution_x
def get_obj_value(self):
p = np.array(self._pi)
c = np.array(self._ci)
x = np.array(self._solution_x)
return sum((p - self._y * c) * x)
DW分解的求解過程
# dw_proc.py
from master_model import MasterModel
from sub_model import SubModel
class DWProc(object):
def __init__(self, p, c, d, b, max_iter=1000):
"""
:param p: p[i][j]代表品牌i中商品j的預期收益
:param c: c[i][j]代表品牌i中商品j的營銷成本
:param d: 總營銷成本, int
:param b: b[i]代表選中品牌i的商品數量限制
"""
self._p = p
self._c = c
self._d = d
self._b = b
self._v = None # 待初始化
self._max_iter = max_iter
self._iter_times = 0
self._status = -1
self._reduced_costs = [1] * len(self._p)
self._solution_x = None # 計算結果
self._obj_value = 0 # 目標函數值
def _stop_criteria_is_satisfied(self):
""" 根據reduced cost判斷是否應該停止迭代.
注意: 這是最大化問題, 因此所有子問題對應的reduced cost <= 0時停止.
"""
st = [0] * len(self._reduced_costs)
for i in range(len(self._reduced_costs)):
if self._reduced_costs[i] < 1e-6:
st[i] = 1
if sum(st) == len(st):
self._status = 0
return True
if self._iter_times >= self._max_iter:
if self._status == -1:
self._status = 1
return True
return False
def _init_v(self):
""" 初始化主問題的輸入
"""
self._v = [[]] * len(self._p)
for i in range(len(self._p)):
self._v[i] = [[0] * len(self._p[i])]
def _append_v(self, i, x):
""" 把子問題i的解加入到主問題中
:param x: 子問題i的解
"""
self._v[i].append(x)
def solve(self):
# 初始化主問題并求解
self._init_v()
mp = MasterModel(self._p, self._v, self._c, self._d)
mp.solve()
self._iter_times += 1
# 迭代求解主問題和子問題直到滿足停止條件
while not self._stop_criteria_is_satisfied():
# 求解子問題
print("==== iter %d ====" % self._iter_times)
for i in range(len(self._p)):
# 求解子問題
sm = SubModel(self._p[i], self._c[i], mp.get_y(), self._b[i])
sm.solve()
# 更新reduced cost
self._reduced_costs[i] = sm.get_obj_value() - mp.get_zi(i)
# 把子問題中滿足條件的解加入到主問題中
if self._reduced_costs[i] > 0:
self._append_v(i, sm.get_solution_x())
print(">>> Solve sub problem %d, reduced cost = %f" % (i, self._reduced_costs[i]))
# 求解主問題
mp = MasterModel(self._p, self._v, self._c, self._d)
mp.solve()
self._iter_times += 1
self._solution_x = mp.get_solution_x()
self._obj_value = mp.get_obj_value()
status_str = {-1: "error", 0: "optimal", 1: "attain max iteration"}
print(">>> Terminated. Status:", status_str[self._status])
def print_info(self):
print("==== Result Info ====")
print(">>> objective value =", self._obj_value)
print(">>> Solution")
sku_list = [[]] * len(self._solution_x)
for i in range(len(self._solution_x)):
sku_list[i] = [j for j in range(len(self._solution_x[i])) if self._solution_x[i][j] > 0]
for i in range(len(self._solution_x)):
print("brand %d, sku list:" % i, sku_list[i])
主函數
# main.py
from data import p, c, b, d # instance data
from dw_proc import DWProc
if __name__ == '__main__':
dw = DWProc(p, c, d, b)
dw.solve()
dw.print_info()
參考文獻
-
George B. Dantzig; Philip Wolfe. Decomposition Principle for Linear Programs. Operations Research. Vol 8: 101–111, 1960. ?