線性規(guī)劃技巧: Benders Decomposition

Benders分解由Jacques F. Benders在1962年提出[1]. 它是一種把線性規(guī)劃問題分解為小規(guī)模子問題的技巧. 通過迭代求解主問題和子問題, 從而逼近原問題的最優(yōu)解. 與列生成(Column Generation)相比, Benders分解是一種行生成(Row Generation)技巧: 主問題的約束來自子問題的解. 本文介紹的內(nèi)容基于 Georrion和Graves[2].

問題描述

考慮如下的數(shù)學(xué)規(guī)劃問題P(x,y):
\begin{aligned} \min ~ & c^{T} x + f(y) \\ & A x + F(y) = b \\ & x \geq 0 \\ & y \in Y \end{aligned}
其中A\in \mathbb{R}^{m \times n}, c\in \mathbb{R}^n, b\in \mathbb{R}^m, y\in\mathbb{R}^p. f(y)F(y)可以是非線性的. Y可以是離散或連續(xù)的. 給定y, 上述問題則變成了一個標(biāo)準(zhǔn)的線性規(guī)劃問題, 記作P(x|y).

假設(shè). \forall y\in Y, 線性規(guī)劃P(x|y)存在最優(yōu)解. (否則原問題也不存在最優(yōu)解.)

Benders分解

等價形式

先把原問題P(x,y)寫成如下形式:
\min_{y \in Y} \{ f(y) + \min_x \{c^Tx | Ax = b - F(y), x\geq 0\} \}

根據(jù)假設(shè), 問題
P(x|y): \quad \min_x \{c^Tx | Ax=b-F(y), x\geq 0\}
存在最優(yōu)解. 其對偶問題
D(u|y):\quad \max_u \{ [b-F(y)]^Tu | A^T u \leq c \}
的最優(yōu)解也存在. (不會寫對偶? 這篇文章手把手教你: <線性規(guī)劃技巧: 如何寫對偶問題>) .

因此P(x, y)進(jìn)而可以寫成:
\min_{y \in Y} \{ f(y) + \max_u \{ [b-F(y)]^Tu | A^T u \leq c \} \}.

寫成上述形式的好處是D(u|y)中的約束與變量y無關(guān). 因此, 其最優(yōu)解一定是多面體(Polytope) Q=\{u | A^Tu\leq c\}中的一個頂點. 令U代表多面體Q頂點的集合. 因此原問題等價于
P(u,y): \quad \min_{y \in Y} \{ f(y) + \max_{u\in U} [b-F(y)]^T u \}.

再把P(u, y)寫成數(shù)學(xué)規(guī)劃的形式P_1(u,y):
\begin{aligned} \min ~ & f(y) + z \\ \text{ s.t. } & [b-F(y)]^Tu \leq z, \quad u \in U \\ & y \in Y. \end{aligned}

問題分解

對原問題P(x, y), 我們先固定y得到P(x|y), 然后考慮其對偶問題D(u|y). 通過這種方式我們把原問題轉(zhuǎn)化成等價問題P_1(u,y). 在新問題P_1(u,y)中, 每個約束條件對應(yīng)一個對偶問題D(u|y)的基本解, 且目標(biāo)函數(shù)f(y)+zu無關(guān). 這樣一來, 使得我們可以通過迭代的方式進(jìn)行求解: 主問題P_1(u,y)一開始只考慮少量的約束, 然后通過求解對偶問題D(u|y)添加新的約束到P_1(u,y), 直到逼近最優(yōu)解.

主問題M(y, z)
\begin{aligned} \min ~ & f(y) + z \\ \text{s.t. } & [b - F(y)]^T u \leq z, \quad u \in B \\ & y\in Y. \end{aligned}

我們知道u是多面體\{u| A^T u \leq c \}的頂點. 是否需要把所有頂點加入到主問題中? 回顧主問題P(u,y)的目標(biāo)函數(shù)\min_{y \in Y} \{ f(y) + \max_{u\in U} [b-F(y)]^T u \}. 顯然, 我們應(yīng)該找到u使得[b-F(y)]^Tu最大化.

子問題S(u|y)

\begin{aligned} \max ~ [b-F(y)]^T u \\ \text{s.t. } A^T u \leq c. \end{aligned}

求解過程

思路

  1. 初始化主問題. 令B=\emptyset. 換句話說, 主問題一開始不考慮關(guān)于u的約束. 令z=0, 然后求解M(y, z=0), 從而得到子問題的輸入y_0.
  2. 令OPT(master)代表最優(yōu)解的值. 由于主問題只考慮了部分約束, 因此OPT(master)是原問題P_1(u,y)最優(yōu)解的下界.
  3. 求解子問題S(u|y_0)得到u_1, 然后把約束[b-F(y)]^T u_1 \leq z添加到主問題M(y, z).
  4. 令OPT(sub)代表子問題最優(yōu)解的值. 回顧原問題P(u,y)的目標(biāo)函數(shù)f(y) + \max_u [b-F(y)]^Tu, 因此f(y)+OPT(sub)是原問題最優(yōu)解的上界.
  5. 反復(fù)求解主問題和子問題直到上下界相等(或非常接近).

偽代碼

Init: B = empty; UB = inf; e = -1e-4;
solve master problem M(y, z) by fixing z := 0 and get y;
let LB := f(y) + z;
While UB - LB > e: 
    solve sub problem S(u|y) and get u;
    update UB := min {UB, f(y) + OPT(sub)};
    add constraint [b-F(y)]^T u <= z to the master problem;
    solve master problem M(y, z) and get y;
    update LB := OPT(master);
EndWhile

說明

  1. 迭代過程中LB不會減小, UB不會增大.
  2. 如果子問題的最優(yōu)解滿足唯一性, 這個迭代過程收斂(證明略).

例子: 設(shè)施選址問題(A Facility Location Problem)

某個工廠需要開設(shè)一些倉庫, 用來向它的客戶提供倉儲和配送服務(wù).

  • 考慮m個候選倉庫和n個客戶.
  • 新建一個倉庫有開倉成本. 服務(wù)一個客戶有連接成本, 例如為客戶提供配送服務(wù)和退換貨服務(wù)帶來的成本等.
  • 我們需要決定開設(shè)哪些倉庫, 并指定每個客戶對應(yīng)的服務(wù)倉庫.
  • 目標(biāo)是最小化開倉成本與所有客戶的連接成本之和.
圖片來自網(wǎng)絡(luò).

(如上圖所示, 藍(lán)色方框代表開設(shè)的倉庫, 其它候選倉庫未展示. 紅色圓圈代表客戶. 黑色的線代表服務(wù)關(guān)系.)

整數(shù)線性規(guī)劃

Indices

  • i -- 倉庫
  • j -- 客戶

Parameters

  • f_i -- 倉庫i的開倉成本
  • c_{i,j} -- 倉庫i服務(wù)客戶j的連接成本

Decision Variables

  • x_{i,j}\in \{0,1\} -- 倉庫i是否服務(wù)客戶j
  • y_i\in \{0, 1\} -- 倉庫i是否開設(shè)

\begin{align} \min ~ & \sum_{i=1}^m \sum_{j=1}^n c_{i,j} x_{i,j} + \sum_{i=1}^m f_i y_i \\ \text{ s.t. } & \sum_{i=1}^m x_{i, j} = 1,\quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j}, y_i \in \{ 0, 1 \}, \quad \forall i, j \end{align}

如果開設(shè)倉庫確定, 我們指定離客戶最近的倉庫來服務(wù)這個客戶, 從而達(dá)到連接成本最低. 因此, 設(shè)施選址問題的核心是決定開設(shè)哪些倉庫, 因而上述規(guī)劃的決策變量x_{i,j}的取值范圍可以寫成x_{i,j} \geq 0. 我們得到如下規(guī)劃P(x,y):

\begin{align} \min ~ & \sum_{i=1}^m \sum_{j=1}^n c_{i,j} x_{i,j} + \sum_{i=1}^m f_i y_i \\ \text{ s.t. } & \sum_{i=1}^m x_{i, j} = 1,\quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j} \geq 0, ~ y_i \in \{ 0, 1 \}, \quad \forall i, j \end{align}

Benders分解

給定y_i, P(x|y)的最優(yōu)解等價于如下問題的最優(yōu)解.
\begin{align} \min ~ & \sum_{ i=1}^{m} \sum_{j=1}^{n} c_{i,j} x_{i,j} \\ \text{ s.t. } & \sum_{i=1}^{m} x_{i, j} = 1, \quad \forall j \\ & x_{i, j} \leq y_i, \quad \forall i, j \\ & x_{i, j} \geq 0, \quad \forall i, j \end{align}
它的對偶問題則是我們要求解的子問題S(\alpha, \beta|y):
\begin{align} \max ~ & \sum_{ j=1}^n \alpha_j - \sum_{i=1}^m \sum_{j=1}^{n} y_i \beta_{i, j} \\ \text{s.t. } & \alpha_j - \beta_{i, j} \leq c_{i,j}, \quad \forall i, j \\ & \beta_{i, j} \geq 0 \end{align}

結(jié)合原問題P(x,y)的目標(biāo), 我們得到主問題M_0(y, z):
\begin{align} \min ~ & \sum_{i=1}^{m} f_i y_i + z \\ \text{ s.t. } & \sum_{ j=1}^{n } \alpha_j - \sum_{i=1}^{m} \sum_{j=1}^{n} y_i \beta_{i, j} \leq z, \quad \forall (\alpha, \beta) \in B \\ & y_i \in \{ 0,1\}, \quad \forall i \end{align}

注意: 我們需要檢查本文開頭提到的假設(shè)是否成立. 即, 對任意可行的y_i, 子問題S(\alpha, \beta|y)是否存在最優(yōu)解. 事實上, 當(dāng)y_i=0, \forall i時, 子問題的目標(biāo)函數(shù)的最大值是+\infty, 因而不存在最優(yōu)解! 因此我們需要為主問題添加約束來保證假設(shè)條件成立. 注意到原問題P(x,y)的最優(yōu)解至少要保證開設(shè)1個倉庫, 把它加入主問題的約束從而保證了子問題最優(yōu)解的存在(想想為什么).

主問題M(y,z)
\begin{align} \min ~ & \sum_{i=1}^{m} f_i y_i + z \\ \text{ s.t. } & \sum_{ j=1}^{n} \alpha_j - \sum_{i=1}^{m} \sum_{j=1}^{n} y_i \beta_{i, j} \leq z, \quad \forall (\alpha, \beta) \in B \\ & \sum_i {y_i} \geq 1 \\ & y_i \in \{ 0,1\}, \quad \forall i \end{align}

說明 主問題M(y,z)的約束條件一開始為空, 即B=\emptyset. 它的約束條件通過求解子問題得到.

Python實現(xiàn)

主問題模型

# master_model.py

from ortools.linear_solver import pywraplp

import numpy as np


class MasterModel(object):

    def __init__(self, f, fix_z=None):
        """
        :param f: 開倉成本(m維向量)
        :param fix_z: 限定z的值. 目的是初始化的時候令z=0,然后求解主問題得到y(tǒng)的值.
        """
        self._solver = pywraplp.Solver('MasterModel',
                                       pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        self._f = f
        self._fix_z = fix_z
        self._m = len(self._f)
        self._y = None  # 決策變量
        self._z = None  # 決策變量
        self._solution_y = None  # 計算結(jié)果
        self._solution_z = None  # 計算結(jié)果
        # 迭代求解之前會調(diào)用add_constraint
        # 所以決策變量的初始化要放在前面
        self._init_decision_variables()
        self._init_constraints()
        self._init_objective()

    def _init_decision_variables(self):
        self._y = [self._solver.NumVar(0, 1, 'y[%d]' % i)
                   for i in range(self._m)]
        self._z = self._solver.NumVar(-self._solver.Infinity(), self._solver.Infinity(), 'z')

    def _init_objective(self):
        obj = self._solver.Objective()
        for i in range(self._m):
            obj.SetCoefficient(self._y[i], self._f[i])
        obj.SetCoefficient(self._z, 1)
        obj.SetMinimization()

    def add_constraint(self, alpha, beta):
        """ Benders主流程會調(diào)用此方法為主問題增加新的約束.

        :param alpha: 子問題的解
        :param beta: 子問題的解
        """
        ct = self._solver.Constraint(sum(alpha), self._solver.Infinity())
        for i in range(self._m):
            ct.SetCoefficient(self._y[i], sum(beta[i]))
        ct.SetCoefficient(self._z, 1)

    def _init_constraints(self):
        if self._fix_z is not None:
            ct = self._solver.Constraint(self._fix_z, self._fix_z)
            ct.SetCoefficient(self._z, 1)

        ct = self._solver.Constraint(1, self._solver.infinity())
        for i in range(self._m):
            ct.SetCoefficient(self._y[i], 1)

    def solve(self):
        self._solver.Solve()
        self._solution_y = [self._y[i].solution_value() for i in range(self._m)]
        self._solution_z = self._z.solution_value()

    def get_solution(self):
        return self._solution_y, self._solution_z

    def get_objective_value(self):
        return sum(np.array(self._solution_y) * np.array(self._f)) + self._solution_z

子問題模型

# sub_model.py

from ortools.linear_solver import pywraplp
import numpy as np


class SubModel(object):

    def __init__(self, y, C):
        """
        :param y: 主問題的解(代表y_i=1代表開設(shè)倉i)
        :param C: 連接成本(m*n維矩陣)
        """
        self._solver = pywraplp.Solver('SubModel',
                                       pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        self._y = y
        self._c = C
        self._m = len(self._c)
        self._n = len(self._c[0])
        self._alpha = None  # 決策變量
        self._beta = None  # 決策變量
        self._constraints = []  # 所有約束(后續(xù)獲取對偶變量, 得到原始問題的解)
        self._solution_alpha = None  # 計算結(jié)果
        self._solution_beta = None  # 計算結(jié)果

    def _init_decision_variables(self):
        self._alpha = [self._solver.NumVar(-self._solver.Infinity(),
                                           self._solver.Infinity(),
                                           'alpha[%d]' % j)
                       for j in range(self._n)]

        self._beta = [[self._solver.NumVar(0, self._solver.Infinity(), 'beta[%d][%d]' % (i, j))
                      for j in range(self._n)] for i in range(self._m)]

    def _init_constraints(self):
        for i in range(self._m):
            self._constraints.append([])
            for j in range(self._n):
                ct = self._solver.Constraint(-self._solver.Infinity(), self._c[i][j])
                ct.SetCoefficient(self._alpha[j], 1)
                ct.SetCoefficient(self._beta[i][j], -1)
                self._constraints[i].append(ct)

    def _init_objective(self):
        obj = self._solver.Objective()
        for j in range(self._n):
            obj.SetCoefficient(self._alpha[j], 1)
        for i in range(self._m):
            for j in range(self._n):
                obj.SetCoefficient(self._beta[i][j], -self._y[i])

        obj.SetMaximization()

    def solve(self):
        self._init_decision_variables()
        self._init_constraints()
        self._init_objective()
        self._solver.Solve()
        self._solution_alpha = [self._alpha[j].solution_value() for j in range(self._n)]
        self._solution_beta = [[self._beta[i][j].solution_value()
                                for j in range(self._n)]
                               for i in range(self._m)]

    def get_solution(self):
        return self._solution_alpha, self._solution_beta

    def get_objective_value(self):
        return sum(self._solution_alpha) - sum(np.array(self._y) * np.sum(self._solution_beta, axis=1))

    def get_dual_values(self):
        """ 得到原問題的解: x[i][j]
        """
        return [[self._constraints[i][j].dual_value()
                for j in range(self._n)]
                for i in range(self._m)]

Benders分解流程

# benders_proc.py

import numpy as np

from master_model import MasterModel
from sub_model import SubModel


class BendersProc(object):
    """ Benders分解流程
    """

    def __init__(self, f, C, max_iter=10000):
        """
        :param f: 開倉成本(m維向量)
        :param C: 連接成本(m*n矩陣), 其中m是候選設(shè)施的數(shù)量, n是客戶的數(shù)量
        :param max_iter: 最大循環(huán)次數(shù)
        """
        self._f = f
        self._c = C
        self._iter_times = 0
        self._max_iter = max_iter
        self._status = -1  # -1:執(zhí)行錯誤; 0:最優(yōu)解; 1: 達(dá)到最大循環(huán)次數(shù)
        self._ub = np.inf
        self._lb = -np.inf
        self._solution_x = None  # 計算結(jié)果
        self._solution_y = None  # 計算結(jié)果

    def _stop_criteria_is_satisfied(self):
        """ 根據(jù)上下界判斷是否停止迭代
        """
        if self._ub - self._lb < 0.0001:
            self._status = 0
            return True
        if self._iter_times >= self._max_iter:
            if self._status == -1:
                self._status = 1
            return True
        return False

    def solve(self):
        # 初始令z=0. 求解主問題得到y(tǒng)
        master0 = MasterModel(self._f, fix_z=0)
        master0.solve()
        y, z = master0.get_solution()
        # 下面的迭代需要重新生成master對象
        # 因為master0中z=0是約束條件
        master = MasterModel(self._f)
        sub = None
        # 迭代過程
        while not self._stop_criteria_is_satisfied():
            # 求解子問題
            sub = SubModel(y, self._c)
            sub.solve()
            # 更新上界
            fy = sum(np.array(self._f) * np.array(y))
            self._ub = min(self._ub, fy + sub.get_objective_value())
            # 生成主問題的約束
            alpha, beta = sub.get_solution()
            master.add_constraint(alpha, beta)
            master.solve()
            # 更新y和下界
            y, z = master.get_solution()
            self._lb = master.get_objective_value()

            print(">>> iter %d: lb = %.4f, ub = %.4f" % (self._iter_times, self._lb, self._ub))
            self._iter_times += 1

        # 保存結(jié)果
        self._solution_x = sub.get_dual_values()
        self._solution_y = y

        status_str = {-1: "error", 0: "optimal", 1: "attain max iteration"}
        print(">>> Terminated. Status:", status_str[self._status])

    def print_info(self):
        print("---- Solution ----")
        res = {}
        for i in range(len(self._f)):
            if self._solution_y[i] == 0:
                continue
            connected_cities = [str(j) for j in range(len(self._c[0]))
                                if self._solution_x[i][j] > 0]
            res[i] = ', '.join(connected_cities)

        for f, c in res.items():
            print("open facility: %d, connected cities: %s" % (f, c))

主函數(shù)

# main.py

from benders_proc import BendersProc
from data import f, C  # data.py包含了一個實例


if __name__ == '__main__':
    bp = BendersProc(f, C)
    bp.solve()
    bp.print_info()

完整代碼

參考文獻(xiàn)


  1. J.F. Benders. Partitioning Procedures for Solving Mixed-Variables Programming Problems. Numerische Mathematik, Vol. 4, 1962. ?

  2. A.M. Geoffrion and G.W. Graves. Multicommodity distribution system design by benders decomposition. Management Science 20, no. 5, 822–844, 22, 1974. ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市悦污,隨后出現(xiàn)的幾起案子榴啸,更是在濱河造成了極大的恐慌必孤,老刑警劉巖畜疾,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茶没,居然都是意外死亡筒占,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來团赁,“玉大人育拨,你說我怎么就攤上這事』渡悖” “怎么了熬丧?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵乍楚,是天一觀的道長胡岔。 經(jīng)常有香客問我贱迟,道長伐债,這世上最難降的妖魔是什么茶凳? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任孵睬,我火速辦了婚禮殃恒,結(jié)果婚禮上模蜡,老公的妹妹穿的比我還像新娘吞滞。我一直安慰自己佑菩,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布裁赠。 她就那樣靜靜地躺著殿漠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佩捞。 梳的紋絲不亂的頭發(fā)上绞幌,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機(jī)與錄音一忱,去河邊找鬼啊奄。 笑死,一個胖子當(dāng)著我的面吹牛掀潮,可吹牛的內(nèi)容都是我干的菇夸。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼仪吧,長吁一口氣:“原來是場噩夢啊……” “哼庄新!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薯鼠,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤择诈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后出皇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羞芍,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年郊艘,在試婚紗的時候發(fā)現(xiàn)自己被綠了荷科。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唯咬。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖畏浆,靈堂內(nèi)的尸體忽然破棺而出胆胰,到底是詐尸還是另有隱情,我是刑警寧澤刻获,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布蜀涨,位于F島的核電站,受9級特大地震影響蝎毡,放射性物質(zhì)發(fā)生泄漏厚柳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一沐兵、第九天 我趴在偏房一處隱蔽的房頂上張望别垮。 院中可真熱鬧,春花似錦痒筒、人聲如沸宰闰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽移袍。三九已至,卻和暖如春老充,著一層夾襖步出監(jiān)牢的瞬間葡盗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工啡浊, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留觅够,地道東北人。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓巷嚣,卻偏偏與公主長得像喘先,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子廷粒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內(nèi)容