0 介绍

前面介绍的割平面法和分支定界法都是求解整数规划的常用方法,但是面对大规模整数规划/混合整数规划,往往直接采用割平面法和分支定界法求解是不现实的,这时候就需要对大规模整数规划/混合整数规划问题先进行分解和松弛,然后再进一步采用割平面法和分支定界法来帮助求解。目前我个人总结整数规划问题的分解/松弛的主流的方法有如下三种:
1 Benders decomposition (主要思想是行生成+割平面方法)
2 Dantzig-Wolfe decomposition (主要思想其实就是列生成)
3 Lagrangian decomposition (主要思想是 Lagrangian relaxation)
我们今天主要介绍的是 Benders 分解方法,前两种方法我们已经在之前的笔记中介绍过了。

1 Benders分解的Reformulation

考虑如下的线性混合整数规划问题:
Python实现规划求解 python求解整数规划_动态规划

其中 Python实现规划求解 python求解整数规划_动态规划_02 是离散变量, Python实现规划求解 python求解整数规划_算法_03

在优化问题的求解中,我们直观可以想到的一个方法是先固定住一部分变量,而优化另外一部分变量。例如在上述优化问题(1.1-1.3)中我们可以先将Python实现规划求解 python求解整数规划_算法_03 作为常数,只把 Python实现规划求解 python求解整数规划_动态规划_02 作为决策变量去优化,然后把Python实现规划求解 python求解整数规划_动态规划_02 作为常数,只把 Python实现规划求解 python求解整数规划_算法_03 作为决策变量。在这样一个基本思想的指引下,我们可以将优化问题(1.1-1.3)拆分为2个优化问题:
Python实现规划求解 python求解整数规划_python_08

熟悉 bilevel optimization 的童鞋可能已经看出来就是一个 bilevel optimization 的问题。实际上求解 bilevel optimization 也经常用到行生成的方法,而行生成实际也是 Benders 分解的基本思想。所以 Benders 分解和bilevel optimization有着很密切的联系。在 Benders 分解中我们把优化问题(1.4)称为主问题,把优化问题(1.5)称为子问题。

好了,让我们回到主题上来。可以看到上面的(1.4)就是只包含Python实现规划求解 python求解整数规划_动态规划_02 的优化问题,(1.5)就是只包含Python实现规划求解 python求解整数规划_算法_03 的优化问题。当然需要注意的是优化问题(1.5)是以Python实现规划求解 python求解整数规划_动态规划_02 作为参数的,给一个Python实现规划求解 python求解整数规划_动态规划_02 的值通过求解优化问题(1.5)就可以得到与之对应的Python实现规划求解 python求解整数规划_算法_13。由于Python实现规划求解 python求解整数规划_动态规划_02 的值给的不合适可能会让优化问题(1.5)没有可行解。我们不妨假设 Python实现规划求解 python求解整数规划_python_15,根据 Farkas lemma,优化问题(1.5)有可行解的充要条件为:
Python实现规划求解 python求解整数规划_算法_16
其中Python实现规划求解 python求解整数规划_动态规划_17的极方向。其中极方向集合为
Python实现规划求解 python求解整数规划_python_18
如果大家对 Farkas lemma 不太熟悉的话,那么直接从线性规划的对偶角度来出发也容易得到,优化问题(1.5)有可行解的充要条件。写出优化问题(1.5)的对偶问题为
Python实现规划求解 python求解整数规划_算法_19

我们观察发现对偶问题(1.8)的可行域实际上就是极方向集合(1.7)。这是因为线性规划原问题有可行解的充要条件为对偶问题也有可行解。这样进一步我们可以将式(1.5)中的原问题采用式(1.8)的对偶问题的形式来做等价替换可得:

Python实现规划求解 python求解整数规划_Python实现规划求解_20

这里要说一下,从原问题(1.5)折腾了一大圈到目前的对偶问题(1.9)。难道我们直接求解原问题不好吗?为何要绕个圈子去求解对偶问题(1.9)呢?主要原因有以下两点:

1 我们需要对偶变量Python实现规划求解 python求解整数规划_Python实现规划求解_21的信息。之前也说过了原问题可行不可行的关键就是在于极方向集合(1.7),而极方向实际上就是对偶问题的约束,这样转化到对偶问题之后方便我们在主问题里边利用对偶变量的信息Python实现规划求解 python求解整数规划_Python实现规划求解_21来构造cut,这一点在这里大家有点初步体会,在后边会看得更清楚。

2 前面说过了Python实现规划求解 python求解整数规划_算法_13是带有参数Python实现规划求解 python求解整数规划_动态规划_02的一个函数/优化问题,在原问题中 Python实现规划求解 python求解整数规划_动态规划_02出现在约束上,而在对偶问题中 Python实现规划求解 python求解整数规划_动态规划_02出现在了目标函数上。显然出现在目标函数上会让带有参数Python实现规划求解 python求解整数规划_动态规划_02

我们设对偶问题中约束多面体的极点集合为 Python实现规划求解 python求解整数规划_动态规划_28 ,那么优化问题(1.9)进一步可以等价为:
Python实现规划求解 python求解整数规划_优化问题_29

我们将上式带入到(1.11)中可得:
Python实现规划求解 python求解整数规划_动态规划_30

上式中是max套max我们可以把max整体提出来合并可得:
Python实现规划求解 python求解整数规划_动态规划_31

如前所述,我们并不能保证Python实现规划求解 python求解整数规划_算法_13 一定存在可行解,因此还要将保证 Python实现规划求解 python求解整数规划_算法_13存在可行解的充要条件(1.6)加入到上述优化问题中,由此可得:
Python实现规划求解 python求解整数规划_python_34

由于上述约束(1.17-1.18)的数量都是指数级别的,我们是不可能一下子就把所有的约束全部添加进来求解。在实际Benders分解的算法中,我们是逐步逐步将约束(1.17-1.18)添加进来的。
Python实现规划求解 python求解整数规划_算法_35

其中集合 Python实现规划求解 python求解整数规划_Python实现规划求解_36

容易看到一条约束实际上就对应一行,Benders分解也就是行生成(逐步添加行的过程),而约束(1.22)实际上就是一种割平面。因此行生成和割平面与Benders分解有着密不可分的联系。

2 Benders分解算法流程

2.1 算法伪代码

Step 1:初始化 Python实现规划求解 python求解整数规划_Python实现规划求解_37,初始化上下界 Python实现规划求解 python求解整数规划_算法_38

Step 2: 若 Python实现规划求解 python求解整数规划_python_39则进入Step 3,否则直接输出最优解

Step 3: 求解对偶问题(1.8)得到 最优解Python实现规划求解 python求解整数规划_python_40

Step 4: 若Python实现规划求解 python求解整数规划_python_40是 unbounded 则添加约束$ v_{s}^{T}\left( b-By \right) \ge 0$到主问题中

Step 5: 若Python实现规划求解 python求解整数规划_python_40是 bounded 则添加约束Python实现规划求解 python求解整数规划_python_43到主问题中, 更新 Python实现规划求解 python求解整数规划_动态规划_44

Step 6: 求解主问题(1.20-1.23)得到最优解 Python实现规划求解 python求解整数规划_python_45,然后更新Python实现规划求解 python求解整数规划_动态规划_46

2.2 以一个案例进一步说明算法流程

考虑如下的优化问题:
Python实现规划求解 python求解整数规划_算法_47
套用下面的形式:
Python实现规划求解 python求解整数规划_优化问题_48
可得参数为:
Python实现规划求解 python求解整数规划_Python实现规划求解_49
Python实现规划求解 python求解整数规划_动态规划_50

Step 1: 令 Python实现规划求解 python求解整数规划_Python实现规划求解_51,初始化上下界 Python实现规划求解 python求解整数规划_优化问题_52

算法第一次循环:

Step 2: Python实现规划求解 python求解整数规划_Python实现规划求解_53因此进入 Step 3

Step 3: 带入到子问题的对偶问题(1.8)中可得:
Python实现规划求解 python求解整数规划_动态规划_54

最优解为 Python实现规划求解 python求解整数规划_python_55,目标函数为 Python实现规划求解 python求解整数规划_Python实现规划求解_56。因为子问题的对偶问题为unbounded,所以子问题的原问题是 不可行的,可以得到其极方向为 Python实现规划求解 python求解整数规划_算法_57接下来到Step 4。

Step 4: 若 Python实现规划求解 python求解整数规划_python_40 是 unbounded 则添加约束 Python实现规划求解 python求解整数规划_优化问题_59到主问题中,带入可得 Python实现规划求解 python求解整数规划_算法_60

Python实现规划求解 python求解整数规划_算法_61

由此可得最优目标函数为 Python实现规划求解 python求解整数规划_动态规划_62, 最优解为Python实现规划求解 python求解整数规划_算法_63,进一步更新 Python实现规划求解 python求解整数规划_算法_64

算法第二次循环:
Step 2: Python实现规划求解 python求解整数规划_算法_65因此进入 Step 3

Step 3: 带入 Python实现规划求解 python求解整数规划_动态规划_66到子问题的对偶问题(1.8)中可得:

Python实现规划求解 python求解整数规划_算法_67

最优解为 Python实现规划求解 python求解整数规划_优化问题_68,目标函数为 0 。因为子问题的对偶问题为 bounded,所以子问题的原问题是 可行的,接下来到 Step 5。

Step 5: 若 Python实现规划求解 python求解整数规划_python_40是 bounded 则添加约束 Python实现规划求解 python求解整数规划_python_43到主问题中,带入可得 Python实现规划求解 python求解整数规划_python_71

Python实现规划求解 python求解整数规划_Python实现规划求解_72

由此可得最优目标函数为 Python实现规划求解 python求解整数规划_动态规划_62,最优解为 Python实现规划求解 python求解整数规划_动态规划_74,进一步更新 Python实现规划求解 python求解整数规划_算法_75

算法第三次循环:

Step 2: Python实现规划求解 python求解整数规划_优化问题_76

Step 3: 带入 Python实现规划求解 python求解整数规划_python_77到子问题的对偶问题(1.8)中可得:
Python实现规划求解 python求解整数规划_python_78

最优解为 Python实现规划求解 python求解整数规划_Python实现规划求解_79

Step 5: 若Python实现规划求解 python求解整数规划_python_40 是 bounded 则添加约束Python实现规划求解 python求解整数规划_python_43到主问题中,带入可得 Python实现规划求解 python求解整数规划_python_82
Python实现规划求解 python求解整数规划_优化问题_83

由此可得最优目标函数为 1097.745 最优解为Python实现规划求解 python求解整数规划_python_84,进一步更新Python实现规划求解 python求解整数规划_优化问题_85

重复上述迭代过程,直到满足收敛条件为止。

3 Benders分解代码实现

代码对应上一节的算法流程,首先定义初始的优化问题。

N, M = 10, 1 # 初始化决策变量维数
c, d = np.array([1+0.01*i for i in range(1, N+1)]), 1.045  # 初始化系数
A, B = np.vstack((np.ones((1, N)), np.eye(N))), np.array([1 if i == 0 else 0 for i in range(N+1)]).reshape(N+1,1)
b = np.array([1000 if i == 0 else 100 for i in range(N+1)]).reshape(N+1,1)

进入代码的主循环部分:

ub, lb = np.inf, -np.inf # 初始化上下界
MAX_ITER_TIMES, eps = 10, 0.1 # 初始化最大迭代次数和误差

subproblem = Subproblem(N, M) # 定义子问题
subproblem.add_constrs(A, c)
masterproblem = Master(N, M, d) #定义主问题
masterproblem.set_objective()
y = 1500 # 设置y的初始值

for i in range(MAX_ITER_TIMES):
    if ub - lb <= eps:
        break
    subproblem.set_objective(B, b, y)
    subproblem.solve()
    subproblem.write()
    rays, solution_status = subproblem.get_status()
    
    if solution_status == GRB.Status.UNBOUNDED or solution_status == GRB.Status.INF_OR_UNBD:
        masterproblem.add_cut1(B, b, u = rays) # 添加不可行的cut对应式(1.17)
    if solution_status == GRB.Status.OPTIMAL:
        masterproblem.add_cut2(B, b, u = rays)
        lb = max(lb, subproblem.get_objval() + d*y) # 添加可行的cut对应式(1.18)
 
    masterproblem.solve()
    masterproblem.write()
    y = masterproblem.get_solution()
    ub = masterproblem.get_objval()

以上仅展示了代码的主干部分,完整代码可在GitHub下载:https://github.com/WenYuZhi/EasyIntegerProgramming

参考文献:

【1】孙小玲,李端,整数规划,科学出版社,2010
【2】Laurence A. Wolsey, Integer Programming, Wily, 2021
【3】Benders Decomposition: An Easy Example