用数学规划的方式求解优化问题

本文介绍如何用数学语言对实际中的优化问题进行建模. 通过建立数学模型, 我们利用现成的求解器可以便捷地计算出最优解(或可行解).

运输问题

考虑三个粮食储量分别是100, 200, 300的仓库 (单位:吨, 下文省略). 我们需要把粮食运送给4个客户, 其需求分别是: 120, 60, 270, 150.

仓库到客户的单位运输成本用矩阵C描述:
\begin{align} \begin{bmatrix} 350 & 200 & 300 & 250 \\ 220 & 330 & 300 & 270 \\ 215 & 230 & 290 & 240 \\ \end{bmatrix} \end{align}
其中行代表仓库, 列代表客户. 矩阵中的每一个值代表对应的仓库到客户的单位运输成本. 我们的目标最小化总的运输成本.

下面我们用数学语言描述该问题.

输入

  • 仓库的供给量s_i, i=1, 2, ... ,m, 其中m是仓库总数
  • 客户的需求量d_j, j= 1, 2, ..., n, 其中n是客户总数
  • 仓库i到客户j的单位运输成本是c_{i, j}

输出

  • 需要计算仓库i到客户j的运输量x_{i,j}

下面我们写出问题的目标和约束.

目标是最小化总的运输成本, 即
\min \sum_{i,j}c_{ij}x_{ij}.

我们需要满足的约束条件有两个:

  • 每个仓库的出库量不能超过其供给量: \sum_{j} x_{ij} \leq a_i, \forall i
  • 每个客户的需求应该被满足: \sum_{i} x_{ij} = d_j, \forall j

综上所述, 我们可以把运输问题用线性规划(Linear Programming)来表示.

\begin{aligned} \min~& \sum_{ij}c_{ij} x_{ij} \\ \text{s.t. } & \sum_{j} x_{ij} \leq a_i, \forall i \\ & \sum_{i} x_{ij} = d_j, \forall j \\ & x_{ij} \geq 0, \forall i, j. \end{aligned}

标准实践

为了更加直观地写出数学模型, 我们可以总结一份标准的指南. 它包含四个基本步骤:

  • 指标(Indices)
    指标的作用是主要为了简化记号. 以上述运输问题为例, 我们的指标有ij, 其中i代表仓库, j代表客户.

  • 参数(Parameters)
    参数是问题的输入. 以上述运输问题为例, 我们的参数是: 供给量(s_i), 需求量(d_j), 单位运输成本c_{i,j}.

  • 决策变量(Decision Variables)
    决策变量是算法的输出.

  • 优化目标(Objective)
    一般是最小化或最大化一个目标函数. 在某些情况下, 问题只需要找到一个可行解, 因此也可以不指定优化目标.

  • 约束(Constraints)
    用等式或不等式描述解的限制.

求解规划

常用的商用求解器有Gruobi和CPLEX(可申请教育和学术的lisense). 商用求解器功能强大, 能求解多种类型的规划问题, 例如整数规划, 混合整数规划, 二次规划等. 免费的求解器有Google的ORtools, 它把一些开源的求解器做了集成, 求解速度虽然比不上商用求解器, 实际中也能满足很多业务需求.

求解方式有两种:

第一种是直接用商用求解器提供的IDE. 按照求解器的建模语法把模型写出来, 然后求解. 建模语法的好处是非常贴近公式化的描述, 所见即所得.

第二种是调用求解器提供的API, 初始化参数, 约束, 目标, 然后求解.

本文我们使用开源工具ORtools求解(基本的教程请自行google,需要翻墙)

Python实现

模型

# model.py

from ortools.linear_solver import pywraplp
import numpy as np


class TransportModel(object):

    def __init__(self, a, d, C):
        """
        :param a: 供给量(m维向量), m代表仓库数量
        :param d: 需求量(n维向量), n代表客户数量
        :param C: 单位运输成本(m*n维矩阵), C[i][j]代表仓库i到客户j的单位运输成本
        """
        self._solver = pywraplp.Solver('TransportModel',
                                       pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        self._a = a
        self._d = d
        self._C = C
        self._m = len(self._a)  # 仓库数量
        self._n = len(self._d)  # 客户数量
        self._x = None  # 决策变量
        self._solution_x = None  # 计算结果
        self._obj_val = None  # 目标函数值

    def _init_decision_variables(self):
        self._x = [
            # 0 <= x[i][j] <= infinity
            [self._solver.NumVar(0, self._solver.infinity(), "x[%d][%d]" % (i, j))
             for j in range(self._n)] for i in range(self._m)
        ]

    def _init_constraints(self):
        # 每个仓库的出库量不能超过其供给量
        # sum(x[i][j]) <= a[i], over j
        for i in range(self._m):
            ct = self._solver.Constraint(0, self._a[i])
            for j in range(self._n):
                ct.SetCoefficient(self._x[i][j], 1)
        # 每个客户的需求应该被满足
        # sum(x[i][j]) == b[j], over i
        for j in range(self._n):
            ct = self._solver.Constraint(self._d[j], self._d[j])
            for i in range(self._m):
                ct.SetCoefficient(self._x[i][j], 1)

    def _init_objective(self):
        obj = self._solver.Objective()
        for i in range(self._m):
            for j in range(self._n):
                obj.SetCoefficient(self._x[i][j], self._C[i][j])
        obj.SetMinimization()

    def solve(self):
        self._init_decision_variables()
        self._init_constraints()
        self._init_objective()
        self._solver.Solve()
        # 求解器返回的解
        self._solution_x = [
            [self._x[i][j].solution_value() for j in range(self._n)]
            for i in range(self._m)
        ]
        # sum(C[i][i] * x[i][j]) over i,j
        self._obj_val = np.sum(np.array(self._C) * np.array(self._solution_x))

    def print_result(self):
        print("最优值 = ", self._obj_val)
        print("最优解 x = ")
        print(np.array(self._solution_x))

主函数

# main.py

from data import a, d, C  # 运输问题实例
from model import TransportModel


if __name__ == '__main__':
    tm = TransportModel(a, d, C)
    tm.solve()
    tm.print_result()

完整代码: 运输问题

数独(Sudoku)

把数字1-9填入下图的空格子中, 且满足如下三个条件:

  1. 每个区块 (图中灰色方框包含的3\times3小格子)包含数字1-9
  2. 每行包含数字1-9
  3. 每列包含数字1-9

我们通过数学规划的方式求解该问题.

指标

  • n -- 填入的数字, n \in \{1, 2, ..., 9 \}
  • i -- 第i行区块, 区块一共三行, 因此i \in \{1, 2, 3\}
  • j -- 第j行区块, 区块一共三列, 因此j \in \{1, 2, 3\}
  • p -- 区块中元素的行, 每个区块包含三行, 因此p \in \{1,2,3 \}
  • q -- 区块中元素的列, 每个区块包含三列q \in \{ 1, 2, 3\}

参数

  • a_{i,j,p,q,n} \in \{ 0, 1\} -- 考虑第ij列的区块, 它的ij列是否数字n

决策变量

  • x_{i,j,p,q,n} \in \{ 0, 1\} -- 考虑第ij列的区块, 它的ij列是填入否数字n

约束

  1. 已经存在的值不能修改.
    x_{i,j,p,q, n} \geq a_{i,j,p, q, n}, \forall i,j,p,q, n
  2. 一个单元格同时只允许填入一个数字.
    \sum_n x_{i,j,p,q,n} = 1, \forall i,j,p,q
  3. 每个区块包含数字1-9.
    \sum_{p, q} x_{i,j, p, q, n} = 1, \forall i, j, n
  4. 每行包含数字1-9.
    \sum_{j, q} x_{i,j,p,q,n} = 1, \forall i,p, n
  5. 每列包含数字1-9.
    \sum_{i, p} x_{i,j,p,q,n} = 1, \forall j,q, n

综上所述, 我们的规划可以写成下面的整数规划(Integer Programming). 注意: 无优化目标.

\begin{aligned} \min~& 0 \\ \text{s.t. } & x_{i,j,p,q,n} \geq a_{i,j,p,q,n}, \forall i, j,p,q, n \\ & \sum_n x_{i,j,p,q,n} = 1, \forall i,j,p,q \\ & \sum_{p, q} x_{i,j, p, q, n} = 1, \forall i, j, n \\ & \sum_{j, q} x_{i,j,p,q,n} = 1, \forall i,p, n \\ & \sum_{i, p} x_{i,j,p,q,n} = 1, \forall j,q, n \\ & x_{i,j,p,q} \in \{ 0,1\} . \end{aligned}

Python实现

模型

# model.py

from ortools.linear_solver import pywraplp
import numpy as np


class SudokuModel(object):

    def __init__(self, a):
        """
        :param a: Sudoku实例 
        """
        self._solver = pywraplp.Solver('SudokuModel',
                                       pywraplp.Solver.BOP_INTEGER_PROGRAMMING)
        self._a = a
        self._x = None  # 决策变量
        self._solution_x = None  # 计算结果

    def __init_decision_variables(self):
        self._x = np.empty((3, 3, 3, 3, 9)).tolist()
        for i in range(3):
            for j in range(3):
                for p in range(3):
                    for q in range(3):
                        for n in range(9):
                            # 已知数字不允许修改
                            # x[i][j][p][q][n] >= a[i][j][p][q][n]
                            self._x[i][j][p][q][n] \
                                = self._solver.IntVar(self._a[i][j][p][q][n], 1,
                                                      'x[%d][%d][%d][%d][%d]' % (i, j, p, q, n))

    def __init_constraints(self):
        # 一个单元格同时只允许填入一个数字
        # sum(x[i][j][p][q][n]) = 1, over n
        for i in range(3):
            for j in range(3):
                for p in range(3):
                    for q in range(3):
                        ct = self._solver.Constraint(1, 1)
                        for n in range(9):
                            ct.SetCoefficient(self._x[i][j][p][q][n], 1)
        # 每个区块包含数字1-9
        # sum(x[i][j][p][q][n]) = 1, over p, q
        for i in range(3):
            for j in range(3):
                for n in range(9):
                    ct = self._solver.Constraint(1, 1)
                    for p in range(3):
                        for q in range(3):
                            ct.SetCoefficient(self._x[i][j][p][q][n], 1)
        # 每行包含数字1-9
        # sum(x[i][j][p][q][n]) = 1, over j, q
        for i in range(3):
            for p in range(3):
                for n in range(9):
                    ct = self._solver.Constraint(1, 1)
                    for j in range(3):
                        for q in range(3):
                            ct.SetCoefficient(self._x[i][j][p][q][n], 1)
        # 每列包含数字1-9
        # sum(x[i][j][p][q][n]) = 1, over i, p
        for j in range(3):
            for q in range(3):
                for n in range(9):
                    ct = self._solver.Constraint(1, 1)
                    for i in range(3):
                        for p in range(3):
                            ct.SetCoefficient(self._x[i][j][p][q][n], 1)

    def solve(self):
        self.__init_decision_variables()
        self.__init_constraints()
        self._solver.Solve()
        self._get_solution_x()

    def _get_solution_x(self):
        self._solution_x = np.empty((3, 3, 3, 3))
        for i in range(3):
            for j in range(3):
                for p in range(3):
                    for q in range(3):
                        for n in range(9):
                            if self._x[i][j][p][q][n].solution_value() == 1:
                                self._solution_x[i][j][p][q] = n + 1

    def print_result(self):
        res = np.empty((9, 9))
        for i in range(3):
            for p in range(3):
                for j in range(3):
                    for q in range(3):
                        res[i*3+p][j*3+q] = self._solution_x[i][j][p][q]
        print(res)

主函数

# main.py

from model import SudokuModel
from data import a


if __name__ == '__main__':
    sm = SudokuModel(a)
    sm.solve()
    sm.print_result()

完整代码: Sudoku

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 今天我们将用几个综合实例来实践Lingo。 例1 求解非线性方程组 规划问题本来就是给出优化条件和限制条件,之后得...
    热爱学习的高老板阅读 1,409评论 0 3
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,475评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,713评论 1 1
  • 在妖界我有个名头叫胡百晓,无论是何事,只要找到胡百晓即可有解决的办法。因为是只狐狸大家以讹传讹叫我“倾城百晓”,...
    猫九0110阅读 3,255评论 7 3