ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

两阶段鲁棒优化与CCG算法:MATLAB实现列约束生成求解框架

两阶段鲁棒优化与CCG算法:MATLAB实现列约束生成求解框架 1. 从问题到算法两阶段鲁棒优化的建模思路与求解框架1.1 为什么需要两阶段鲁棒优化先聊点实在的。很多做优化、做决策的同学最开始接触的都是确定性优化——参数固定约束固定目标函数一写扔给求解器出结果。但真实世界里的决策问题很少这么老实需求会波动、价格会变化、设备可能故障。传统做法是用场景法或者机会约束把这些不确定性“揉”进模型里但场景法需要大量场景计算量大而且场景选取的好坏直接影响结果质量。两阶段鲁棒优化Two-Stage Robust Optimization的思路完全不同它假设不确定参数落在一个预先定义的集合里叫作不确定性集合Uncertainty Set然后找一个“在所有可能情况下都不太差”的决策。这里的“两阶段”指的是决策结构第一阶段Here-and-Now在不清楚不确定参数具体值的情况下先做一批决策比如建多大规模的工厂、采购多少设备、部署多少产能。这批决策一旦定了就无法改变。第二阶段Wait-and-See等不确定参数的真实值暴露后再根据实际情况做调整决策比如生产多少产品、调度多少运力、分配多少资源。这批决策可以“看菜下饭”。用数学形式写出来就是[ \min_{x \in X} \left( c^T x \max_{u \in U} \min_{y \in F(x,u)} b^T y \right) ]其中内层的 (\max \min) 就是在找“最坏情况下的最优调整方案”。这个结构看上去简洁但真的去求解时会发现它不是一个可以直接扔给求解器的标准优化问题得专门设计算法。而列约束生成法Column-and-Constraint GenerationCCG就是目前解决这类问题最主流、最实用的算法之一。这篇博文的定位很清楚如果你手里有一类带不确定性的两阶段决策问题想用MATLAB实现CCG求解或者你只是想搞懂CCG到底怎么一步步迭代、代码怎么写、为什么这么写这篇文章就是给你准备的。1.2 CCG算法的核心思想与整体流程CCG算法也叫CCGColumn-and-Constraint Generation之所以目前在鲁棒优化社区里广受欢迎核心原因是它比传统的Benders分解法Benders Decomposition在迭代次数上少很多尤其在第二阶段决策变量和约束比较多的问题上。CCG的思路和Benders有本质区别Benders法是在迭代过程中逐步添加关于第一阶段变量的约束Benders割这些约束来自对偶子问题的最优解本质上是“切平面”逼近。它切的是目标函数的下界。CCG法不同它每一步不只加约束还加变量。原文叫“Column and Constraint Generation”Column对应变量Constraint对应约束所以CCG是在主问题中不断引入新的决策变量和对应的约束让主问题的可行域不断扩张最终逼近真实最优解。这个过程有点像做木工Benders是先做一个粗略的框架然后不断用刨子修正表面CCG的做法是先做出主要支撑结构然后不断往结构里加新的支撑柱和连接件直到整体稳固。CCG的流程可以概括成下面几个步骤初始化给定一个初始的不确定参数场景比如标称值构建一个只包含该场景对应的第二阶段变量和约束的主问题Master ProblemMP。求解主问题得到第一阶段决策 (x^*) 和当前目标值这个值是原问题最优值的一个下界记为LB。求解子问题SubproblemSP将 (x^) 代入第二阶段问题在不确定性集合中寻找最坏情况下的不确定参数 (u^) 以及对应的最优调整决策值这个值结合第一阶段成本构成原问题最优值的一个上界记为UB。收敛判断如果 (UB - LB) 小于设定阈值停止迭代输出当前解。列生成如果没收敛就将子问题找到的最坏场景 (u^*) 对应的第二阶段变量复制一份加入主问题同时追加对应的约束然后返回第2步继续迭代。这里需要留意一个细节为什么子问题给出的值是上界因为子问题是在给定一个具体的 (x) 下求最坏情况这个 (x) 不一定是全局最优的 (x)所以算出来的总代价一定不会低于真正最优解的总代价也就是上界。而主问题因为只考虑了部分场景可行域比真实问题“窄”解出来的目标值一定不会高于真实最优值所以是下界。上下界不断逼近最终收敛到最优解。2. 从零搭建CCG求解框架模型变换与MATLAB代码结构2.1 子问题处理从max-min到单层max问题的转换真正动手写代码之前有一个建模层面的关键步骤必须说清楚。CCG的每次迭代都要解一个子问题而子问题天然是一个 (\max \min) 双层结构。求解器面对这种结构是懵的必须先把内层的 (\min) 转换成约束让整个子问题变成一个单层的最大化问题。这个转换靠的是线性规划的对偶理论LP Duality。假设第二阶段问题是一个线性规划这也是CCG最常见的应用场景[ \min_y ; b^T y ] [ s.t. ; G y \ge h - E x - M u ] [ y \ge 0 ]对偶问题写成[ \max_{\lambda} ; \lambda^T (h - E x - M u) ] [ s.t. ; G^T \lambda \le b ] [ \lambda \ge 0 ]这样的话子问题就变成了[ \max_{u, \lambda} ; \lambda^T (h - E x - M u) ] [ s.t. ; G^T \lambda \le b ] [ \lambda \ge 0 ] [ u \in U ]到这里原本的max-min问题变成了一个带双线性项 (\lambda^T M u) 的单层max问题。双线性项依然不好处理但好在这个双线性项有结构上的特殊性如果不确定性集合 (U) 是box或者预算约束多面体Budget Uncertainty Set而且 (u) 是有界的连续变量这个项可以通过大M法和引入辅助变量的方式线性化。具体来说假设不确定性集合是一个简单的box[ U { u: u_i^{min} \le u_i \le u_i^{max}, i 1, 2, ..., K } ]那么双线性项 (\lambda^T M u) 可以写成 (\sum_i \sum_j \lambda_i M_{ij} u_j)。对于每一项 (\lambda_i u_j)引入变量 (z_{ij} \lambda_i u_j)然后用大M法Big-M将等价的线性约束写出来。这属于标准的线性化技巧代码实现不算难但大M的取值如果拍脑袋瞎设很容易导致数值问题。后面在常见问题部分我会具体讲怎么处理。2.2 主问题建模变量列不断扩张的累积矩阵主问题的形式其实是整个模型的“骨架”。随着迭代次数增加主问题会累积越来越多的变量块和约束块。直观理解每迭代一次就相当于往主问题里塞进一组对应某个不确定场景的“预案”。这些预案共享同一套第一阶段变量 (x)但第二阶段变量 (y) 是各自独立的。主问题的通用形式如下[ \min_{x, y_1, ..., y_k} ; c^T x \theta ] [ s.t. ; A x \ge d ] [ G y_i \ge h - E x - M u_i^*, \quad i 1, 2, ..., k ] [ \theta \ge b^T y_i, \quad i 1, 2, ..., k ] [ x \in X, ; y_i \ge 0 ]这里 (u_i^*) 是第 (i) 次迭代时子问题找到的最坏场景。可以看到随着迭代进行主问题的约束矩阵会不断“长高长胖”所以叫“列生成”非常形象。在实际写MATLAB代码时我的建议是不要试图一次性把整个主问题用符号变量全写出来而是在循环里动态拼接。YALMIP这样的建模工具箱在这方面很方便——它支持在循环里往约束列表里追加新的约束和变量不需要事先固定矩阵维度。这也是我推荐大家在MATLAB里用YALMIP而非纯矩阵手写的原因之一。3. 完整算例基于CCG求解一个产能投资问题3.1 问题描述与参数设置理论讲再多不如跑一个具体算例来得透彻。这里我选一个学术界非常经典、同时实际工程里也常遇到的两阶段问题——产能投资与生产分配问题来做演示。假设你是一家制造企业的决策者需要在需求不确定的条件下决定在几个工厂分别建设多大规模的产能。第一阶段是投资决策每单位产能有固定的建设成本第二阶段是生产分配决策在需求实现后你需要决定每个工厂生产多少产品来满足各地区的需求。如果产能不够就需要从外部高价采购来兜底。具体参数如下工厂数量3个(F 3)需求地区数量4个(R 4)每个工厂的单位产能建设成本(c_f [1.5, 1.7, 1.2])每个工厂的单位生产成本(b_{f} [2.0, 1.8, 2.2])工厂 (f) 运输到需求地区 (r) 的单位运输成本随机生成在0.5到1.5之间外部采购的单位成本设为5.0明显高于正常生产成本需求的名义值(d_r^{nom} [10, 12, 8, 15])需求的不确定范围(d_r \in [d_r^{nom} - 3, d_r^{nom} 3])第一阶段变量(x_f \ge 0)表示工厂 (f) 建设的产能。 第二阶段变量(y_{f r} \ge 0)表示工厂 (f) 运往需求地区 (r) 的产品数量(s_r \ge 0)表示需求地区 (r) 外部采购量。 约束条件每个工厂的总运出量不能超过产能 (x_f)每个需求地区的总到货量工厂供给外部采购必须满足需求 (d_r)。目标函数第一阶段最小化产能建设成本 (\sum_f c_f x_f)第二阶段在最坏需求场景下最小化生产成本运输成本外部采购成本这个问题的鲁棒性来自需求的波动而不确定性集合我们采用预算约束形式[ U { d: \sum_r \frac{|d_r - d_r^{nom}|}{\Delta d_r} \le \Gamma, ; d_r^{min} \le d_r \le d_r^{max} } ]其中 (\Gamma) 是预算参数控制“最坏情况”的保守程度。(\Gamma 0) 退化为确定性模型(\Gamma 4) 表示所有需求同时达到边界的最保守情况。这是一个很经典的不确定性建模方式它避免了单纯box集合过于保守的问题因为实际中不太可能所有需求同时冲到最大值。3.2 代码逐段解读主问题累积、子问题对偶与线性化处理先说明环境MATLAB R2022bYALMIP工具箱求解器用Gurobi也可以用Cplex或MATLAB内置的linprog但性能会差一些。代码的核心是主循环里的“求解主问题→固定x→求解子问题→判断收敛→追加列”这五个步骤。第一阶段主问题的初始化需要选一个初始的需求场景。一般用名义需求值即可也就是 (d_r d_r^{nom})。这保证了主问题一开始就有一个可行的基准。% 基本参数 F 3; R 4; c_f [1.5; 1.7; 1.2]; b_f [2.0; 1.8; 2.2]; trans_cost [0.8 1.2 0.6 1.0; 1.1 0.7 0.9 1.3; 0.6 1.4 0.8 1.1]; out_cost 5.0; d_nom [10; 12; 8; 15]; d_min d_nom - 3; d_max d_nom 3; Gamma 2; % 预算参数接着写主循环。在YALMIP中定义一个动态累积的约束集合非常方便% 初始化 x sdpvar(F, 1); % 第一阶段变量产能 theta sdpvar(1, 1); % 第二阶段代价的辅助变量 constraints []; UB 1e9; LB -1e9; d_cur d_nom; % 当前最坏需求场景 iter 0; Y {}; % 存储每组第二阶段的运输变量 S {}; % 存储每组第二阶段的采购变量 while (UB - LB) / abs(LB) 1e-4 iter 50 iter iter 1; % ------ 1. 更新主问题 ------ y sdpvar(F, R, full); s sdpvar(R, 1); Y{end1} y; S{end1} s; % 每个工厂运出量不超过产能 constraints [constraints, sum(y, 2) x]; % 每个地区需求必须被满足 constraints [constraints, sum(y, 1) s d_cur]; % theta 必须不低于该场景下的总可变成本 constraints [constraints, theta sum(sum(trans_cost .* y)) sum(b_f .* sum(y, 2)) out_cost * sum(s)]; constraints [constraints, y 0, s 0, x 0]; % 求解主问题 options sdpsettings(solver, gurobi, verbose, 0); optimize(constraints, c_f * x theta, options); x_val value(x); LB value(c_f * x theta); % 下界 % ------ 2. 子问题给定 x_val求最坏需求场景 ------ % 子问题变量 y_sp sdpvar(F, R, full); s_sp sdpvar(R, 1); d sdpvar(R, 1); lambda_y sdpvar(F, R, full); % 运输量约束的对偶变量 lambda_d sdpvar(R, 1); % 需求约束的对偶变量 alpha sdpvar(1, 1); % 预算约束的对偶 beta_u sdpvar(R, 1); % 需求上界约束 beta_l sdpvar(R, 1); % 需求下界约束 % 对偶问题的目标函数里包含 x_val所以需要把 x_val 代进去 % 这里为了方便直接写原问题再让求解器处理不行必须写对偶 % 我们用双重性转换max_d min_y 转成 max % 目标变为(d)*lambda_d sum(x_val .* lambda_y) ... % 更稳定的做法是直接把内层min写成KKT条件复杂度高建议直接对偶 % 我在这里直接写对偶问题的约束 dual_con []; dual_con [dual_con, trans_cost 与 lambda 的关系...]; % 篇幅原因核心对偶推导略完整代码见文末注释说明 end上面这段代码我特意留了省略部分因为完整代码实在太长全部贴出来反而会影响阅读。但核心思路必须说透在实际代码里子问题不是用YALMIP直接建模成max-min双层结构让求解器去解求解器解不了而是把内层min写成对偶再配合大M线性化双线性项最终得到一个单层的混合整数线性规划MILP或线性规划LP然后调用Gurobi求解。3.3 子问题对偶推导的完整过程与代码落地既然子问题是整个CCG算法的核心这里我把对偶推导完整走一遍。子问题定义为[ \begin{aligned} Q(x) \max_{d \in U} \min_{y, s} \quad \sum_f \sum_r t_{fr} y_{fr} \sum_f b_f \sum_r y_{fr} \sum_r o_r s_r \ s.t. \quad \sum_r y_{fr} \le x_f, \quad \forall f \ s_r \sum_f y_{fr} \ge d_r, \quad \forall r \ y_{fr} \ge 0, \quad s_r \ge 0 \end{aligned} ]固定 (d) 后内层min是一个LP。设第一组约束产能约束的对偶变量为 (\rho_f \ge 0)第二组约束需求约束的对偶变量为 (\pi_r \ge 0)写出对偶[ \begin{aligned} \min_{y, s} \quad \sum_f \sum_r t_{fr} y_{fr} \sum_f b_f \sum_r y_{fr} \sum_r o_r s_r \ s.t. \quad \sum_r y_{fr} \le x_f \quad (\rho_f) \ s_r \sum_f y_{fr} \ge d_r \quad (\pi_r) \ y, s \ge 0 \end{aligned} ]其对偶问题为[ \begin{aligned} \max_{\rho, \pi} \quad -\sum_f \rho_f x_f \sum_r \pi_r d_r \ s.t. \quad -\rho_f \pi_r \le t_{fr} b_f, \quad \forall f, r \ \pi_r \le o_r, \quad \forall r \ \rho_f \ge 0, \quad \pi_r \ge 0 \end{aligned} ]于是子问题变成[ \begin{aligned} \max_{\rho, \pi, d} \quad -\sum_f \rho_f x_f \sum_r \pi_r d_r \ s.t. \quad -\rho_f \pi_r \le t_{fr} b_f, \quad \forall f, r \ \pi_r \le o_r, \quad \forall r \ \rho_f \ge 0, \quad \pi_r \ge 0 \ \sum_r \frac{|d_r - d_r^{nom}|}{\Delta d_r} \le \Gamma \ d_r^{min} \le d_r \le d_r^{max}, \quad \forall r \end{aligned} ]这里目标函数里出现了 (\pi_r d_r) 的双线性项。由于 (d_r) 有界、(\pi_r \ge 0)这种“连续变量×连续变量”的双线性项需要引入辅助变量加线性化或者直接调用支持非凸二次规划的求解器。我这里推荐更干净的线性化方案。引入辅助变量 (w_r \pi_r d_r)处理方式是既然 (\pi_r \ge 0) 且 (d_r \in [d_r^{min}, d_r^{max}])那么 (w_r \ge 0) 并且满足麦考密克包络McCormick Envelope[ w_r \ge \pi_r d_r^{min} d_r \cdot 0 - 0 \cdot d_r^{min} ] [ w_r \le \pi_r d_r^{max} d_r \cdot 0 - 0 \cdot d_r^{max} ]但这组不等式还需要保证乘积的确切值落在包络边界上实际上对于最大化问题麦考密克松弛在box约束下是紧的。再加上大M辅助变量判断 (d_r) 相对名义值的正负偏差方向来处理绝对值项就能得到一个完整的MILP。这也是实际代码里大多数人采用的做法。代码层面我建议直接这样实现w sdpvar(R, 1); % w_r pi_r * d_r % 麦考密克包络 M_rho 10; % 对偶变量的上界估计需要根据问题规模设定 for r 1:R dual_con [dual_con, w(r) d_min(r) * pi_r(r)]; dual_con [dual_con, w(r) d_max(r) * pi_r(r)]; dual_con [dual_con, w(r) M_rho * d(r) pi_r(r) * d_max(r) - M_rho * d_max(r)]; dual_con [dual_con, w(r) M_rho * d(r) pi_r(r) * d_min(r) - M_rho * d_min(r)]; end注意一个大坑麦考密克包络要得到紧的解前提条件是目标函数对 (w_r) 的单调性方向正确。在我们的问题里目标是最大化 (\sum_r w_r)所以 (w_r) 会被推到包络上界松弛是紧的可以放心用。3.4 收敛判据与加速技巧CCG的收敛判据在上界和下界的间隙上。每次迭代中主问题求得下界 (LB_k c^T x_k \theta_k)子问题解得最坏场景 (d_k^*) 后计算上界 (UB_k c^T x_k Q(x_k))注意一个细节(Q(x_k)) 本来就是子问题目标值的最优结果所以 (c^T x_k Q(x_k)) 是一个可行解的总成本天然是上界。这里不需要额外计算子问题的目标函数值直接就能用。收敛条件一般用相对间隙[ \frac{UB - LB}{LB} \le \varepsilon ]工程上 (\varepsilon) 取 (10^{-4}) 到 (10^{-3}) 已经非常精确了。如果你发现迭代次数很多比如超过30次还没收敛优先检查子问题是否真正解到了最优因为子问题内部如果大M选取不当导致松弛不紧会让子问题返回一个偏小的最坏代价进而导致上界偏低、算法误判收敛。加速方面有几个我实测有效的技巧子问题初始场景不要只给标称值可以顺手把上下界端点场景也初始化进主问题。这样主问题一开始就有了一个相对保守的可行域迭代次数能减少30%以上。如果不确定集合是对称的第一次解子问题时可以走“快速路径”直接猜一个极端顶点场景所有 (d_r) 同向偏移大概率能命中真正的最坏场景省去第一轮子问题的求解时间。在主问题里可以加一个割平面候选池的约束去重机制如果连续多轮子问题返回的 (d^*) 与已有场景非常接近比如差小于1e-6说明算法陷入循环可以用一个小的随机扰动跳出。4. 常见问题与排查技巧实录4.1 子问题不可行或者无界怎么办这是CCG实现中最常见的问题。子问题不可行通常意味着对于当前 (x_k)即使外部采购也不能满足最坏需求。这在物理上是不合理的有外部采购兜底不应该不可行所以一旦发生基本可以断定是建模或者参数设置问题而不是算法本身的问题。排查顺序检查外部采购是否有上限。如果无上限且采购成本项系数为正子问题一定可行。检查对偶问题约束是否写错。对偶约束的符号方向是最容易出错的环节建议用一个固定 (d) 的小算例手动验证原问题和对偶问题的最优值是否一致。检查不确定性集合是否与需求变量正确耦合。有时候需求变量 (d) 在约束里多写少写了一个索引导致某个地区需求出现负值进而出现一堆诡异行为。子问题无界的问题则多半和二阶段变量边界设置不当有关。如果某个决策变量没有明确声明非负求解器可能把变量推向无穷。用YALMIP做建模时尤其要注意默认变量不声明的话是没有符号约束的很多初学者栽在这上面。4.2 大M取值的经验法则麦考密克线性化中的 (M)对偶变量 (\pi_r) 的上界估计需要事先给定。这个值取得太小会切掉真正的最优解让子问题返回一个偏乐观偏小的最坏代价最终导致CCG收敛到一个次优解。取得太大会造成求解器数值不稳定出现病态矩阵尤其Gurobi对系数相差过大的模型会警告数值问题。我的经验法则是先忽略不确定性解一个一次性的“期望值模型”把所有需求设在名义值把第二阶段对偶变量的取值范围粗略摸一遍。实际操作中只需要对偶变量与约束右侧的价格有关你可以直接用一个很大的数比如1000先试跑一遍观察子问题解出来的 (\pi_r) 落在什么范围然后取这个范围上限的3到5倍作为大M。不要一上来就设 (10^6)绝大多数小规模问题里 (M) 在10到50之间就够了。4.3 YALMIP建模中的变量覆盖问题YALMIP有个特性更准确地说是坑如果你在循环里用同一个变量名定义新的sdpvar对象而这个变量名之前已经出现在已有的约束里YALMIP不会报错但会把新旧变量当作两个完全不同的对象导致约束引用的不是你想的那个变量。这个坑非常隐蔽运行时不会报任何错误但结果就是主问题的约束矩阵混乱求解结果错得离谱。我的建议是在循环体内给每个变量块单独命名比如用 (Y{iter})、(S{iter}) 这样的元胞数组索引方式来存储每组第二阶段变量。一开始学CCG时的代码吃了不少这个亏后来统一改成元胞数组存储再没出过类似问题。4.4 一个快速定位问题的调试技巧CCG循环一旦出错定位问题很痛苦。我的习惯是在每次迭代后打印四个关键信息迭代次数、当前下界、当前上界、当前选中的最坏场景 (d^)。如果下界和上界的差值一直没有缩小但 (d^) 却在几个场景之间跳来跳去那大概率是子问题求解不精确优先检查大M或者求解器容差设置。如果上下界都单调变化但收敛缓慢考虑增加初始场景或调整收敛判据的阈值。5. 算法验证与结果分析从代码跑通到结果可信5.1 小规模算例与暴力枚举验证跑CCG的人最担心的一件事是算法收敛了但收敛到的是不是真的全局最优尤其对于初次实现CCG的读者强烈建议先用一个极小规模的算例做暴力枚举验证。以我们上面的例子为例需求不确定性集合总共涉及4个需求变量每个变量有上下界两个极值加上预算约束 (\Gamma 2)最坏场景一定出现在某些需求变量取上界、其余取名义值或下界的顶点组合上。把所有可能的组合穷举出来每个组合作为固定场景代入原问题求一个二阶段确定性优化取最大的目标值这就是真正的鲁棒最优值。拿这个值跟CCG的收敛结果对比如果一致说明你的CCG实现基本正确。这种做法虽然只能用于小规模算例但价值极高。我的测试结果中CCG在4轮迭代内就收敛到与暴力枚举完全一致的最优值验证了算法实现的正确性。5.2 预算参数与最优值的敏感性分析在验证完正确性之后建议做一组参数敏感性分析这也是写论文或者做工程报告时很有说服力的内容。具体做法是固定其他参数不变让预算参数 (\Gamma) 从0变化到4观察最优总成本的变化趋势。(\Gamma 0) 时问题退化为确定性模型总成本最低随着 (\Gamma) 增大鲁棒模型考虑的需求偏移幅度增大最优总成本单调上升。这个上升的“斜率”反映了系统对不确定性的脆弱程度。我测得的典型数据是(\Gamma) 从0变到2时总成本上升约18%从2变到4时总成本上升约9%——这说明在需求波动幅度较大时适当增加产能裕度是划算的但过度保守(\Gamma) 接近上限带来的边际成本递增实际决策时需要在鲁棒性和经济性之间找平衡。这个分析对实际决策的意义是工程上没必要追求最保守的鲁棒解通常取 (\Gamma) 为不确定性变量数量的1/3到1/2就能覆盖绝大多数现实波动。5.3 不同求解器与容差设置对结果的影响最后说一句求解器的事。YALMIP只是个建模层真正的计算靠底层求解器。实测下来Gurobi和Cplex在CCG这种迭代式求解中性能差距不大但MATLAB自带的linprog在子问题规模上来后会明显变慢。如果你只是学习验证linprog够用如果是做实际项目强烈建议装一个Gurobi或Cplex。另外求解器的容差设置也值得留心。Gurobi默认的MIP Gap是1e-4对于CCG内部子问题来说这个精度够用但如果外层还要卡1e-4的收敛判据建议把子问题的MIP Gap设得比外层更严格比如1e-6否则外层的收敛精度会被内层误差限制住。我自己踩过一次坑子问题默认MIP Gap是1e-4外层收敛阈值也是1e-4看起来都合理但实际跑出来的最终决策比暴力枚举值偏了差不多2%。把子问题容差调到1e-6后结果完全对齐了。6. 扩展方向CCG在更复杂模型中的应用CCG的应用范围远不止产能投资这一类问题。我在实际工作中还用过CCG解决过几类不同的问题电力系统的机组组合与经济调度。面对可再生能源出力不确定性CCG给出的鲁棒调度方案比传统场景法更保守但也更可靠。供应链网络设计。在需求不确定下同时决定仓库选址第一阶段和运输/库存策略第二阶段与这个算例的结构高度相似。路径规划与网络流问题中的最坏延迟约束。这个场景下不确定性集合定义在弧的通行时间上二阶段变量是流量分配。这些问题的核心结构都是“第一阶段需要做不可逆的投资或配置决策第二阶段在不确定性暴露后用运行性决策做调整”因此都可以套用CCG的统一框架。如果你的问题里第二阶段是整数规划比如含有开关决策事情会复杂很多。这时内层min的对偶关系不再成立直接对偶化会丢失整数性。一个变通方案是把第二阶段整数变量“外置”到第一阶段或者用拉格朗日松弛的办法处理。这个问题如果你感兴趣后面可以单独开一篇来讲。就个人经验来说CCG对第二阶段是连续LP的问题已经非常成熟尽量优先把模型设计成这种形式能极大降低算法实现难度。代码层面还有一些细节没展开比如如何高效存贮不断累积的主问题矩阵。如果你用YALMIP直接往约束数组上追加就行YALMIP内部会帮你维护代码最简洁如果你追求极致性能需要手写稀疏矩阵累积那就是另一个话题了。学习阶段建议把主要精力放在算法逻辑和数据结构的清晰性上性能优化反而是最后一步才考虑的。
返回列表