第一次在 macOS 版 Excel 里找规划求解,我在“数据”选项卡上从左到右翻了三遍也没找到按钮,最后才发现它根本不在那儿——得先去“工具”菜单里的“Excel 加载项”把勾打上,回来才会出现。这件事挺能说明问题:用 Excel 求解规划问题(Windows 系统、macOS 系统)本身并不难,真正让人卡住的是两个系统里入口位置不一样、术语翻译不一样、报错信息也不一样,甚至连“能不能生成敏感性报告”都跟你的约束类型有关。这篇内容就是把我在 Windows 和 macOS 两台机器上反复折腾出来的那套流程完整摊开:从一个排产模型怎么落地到单元格,到求解参数里每个开关的真实作用,再到“找不到可行解”和“解出来了但数字不对”这两类问题的排查顺序。适合手里有资源分配、配料、排班、预算取舍这类问题、又不想为一次性的小模型去装专业优化软件的人。看完你应该能直接在任意一个系统上把模型跑通,并且知道结果为什么是这样。
1. 规划求解到底在替我们做什么
1.1 从“手动凑数”到“求解器给答案”
大部分人第一次遇到资源分配问题,用的方法是凑。比如两条产线、三种原料,想凑出一个利润尽量高的组合,就在单元格里改改数字,看着总利润涨没涨,改十几轮觉得差不多了就停手。这个做法的问题不在于慢,而在于你永远不知道自己离真正的最优解差多远——可能你手里那个方案只发挥了六成产能,但因为没有参照物,你根本感觉不到。
规划求解(Solver)做的事,本质上是把这个“改数字—看结果”的循环交给算法去跑。你把三类信息告诉它:哪个单元格是你要最大化的目标、哪些单元格是你可以自由调整的、以及这些单元格必须满足什么限制。剩下的搜索工作由内置的求解引擎完成,它会在满足所有约束的可行域里找到让目标函数取到极值的那个点。对线性模型,它用的是单纯形法,能在很短时间内穷尽所有顶点;对非线性或者带整数约束的模型,它会切换成另外的引擎,用迭代和启发式的方式逼近。
1.2 它和“单变量求解”不是一回事
Excel 里有两个容易混淆的工具。**单变量求解(Goal Seek)**解决的是“反推问题”:我知道结果要是 100,公式是 y = 3x + 7,那 x 应该是多少。它只能反推一个变量,而且必须给定一个确切的目标值,没有“尽量大”“尽量小”的概念。规划求解解决的是“优化问题”:在约束范围内,让目标尽量大或者尽量小,可以同时调整几十上百个变量,可以带不等式约束、整数约束、0-1 约束。
所以判断标准很简单:如果你的问题里出现了“最多不超过”“至少要达到”“在……条件下利润最高”这类表述,那多半就是规划求解的活;如果只是“结果要达到某个具体数字,反推输入是多少”,单变量求解就够了。我见过有人拿单变量求解去硬凑一个多变量的排班表,改一个变量另一个就崩,来回折腾一下午——这种场景换规划求解,模型搭好之后点击一次就出结果。
1.3 Windows 和 macOS 上会立刻撞到的差异
先把差异摆出来,省得你在另一个系统上重新懵一遍。核心求解算法两个平台是一样的,因为都是同一套第三方引擎的精简版,但界面外壳差别不小。
| 对比项 | Windows 版 | macOS 版 |
|---|---|---|
| 加载项启用入口 | 文件 → 选项 → 加载项 → 转到 | 工具 → Excel 加载项(部分版本在“数据”选项卡) |
| 求解按钮位置 | “数据”选项卡最右侧 | “数据”选项卡,或“工具”菜单下 |
| 任务窗格外观 | 右侧对话框 | 浮动对话框,可拖动 |
| 报告生成 | 三种报告齐全 | 支持,但个别版本输出格式有差异 |
| VBA 调用 | 引用 SOLVER.XLAM 即可 | 需手动添加引用,路径与文件名不同 |
| 大模型速度 | 明显更快 | 公式多时体感慢一截 |
这张表里最容易被忽略的是最后两行。同一份带 VBA 的模型文件,在 Windows 上跑得好好的,拿到 Mac 上很可能直接报“找不到宏”,因为 Mac 的加载项文件和引用路径跟 Windows 不是一个位置。这个坑我后面会在批量跑参数那一节展开说。
2. 两个系统里的启用路径与“找不到入口”的处理
2.1 Windows 版:三个层级点进去
Windows 上的路径固定,只是层级有点深,第一次操作容易点到别的地方。完整顺序是:文件 → 选项 → 加载项,然后看对话框底部那个“管理”下拉框,默认可能显示的是“Excel 加载项”,如果不是就手动选成它,再点右边的“转到”。弹出来的列表里勾上“规划求解加载项”,确定。回到主界面,“数据”选项卡最右边就会出现“规划求解”按钮。
如果你用的是精简安装或者从别处拷来的便携版,列表里可能只有“分析工具库”没有“规划求解加载项”,这种情况看 2.3 节的处理办法。
注意:勾选加载项之后建议重启一次 Excel。我有几次勾了之后按钮没出现,重启才生效,原因通常是加载项注册表项写得比较慢。
2.2 macOS 版:菜单名随版本变,三条路都试
Mac 这边麻烦一点,因为微软在不同版本里改过加载项的入口位置。我手上这台 365 版是这样:菜单栏“工具” → “Excel 加载项”,勾选“规划求解加载项”后确定,之后按钮出现在“数据”选项卡上。而在一些较早的版本里,加载项入口在“工具”菜单下叫“加载项”,按钮本身也可能挂在“工具”菜单里而不是“数据”选项卡。
最稳的做法是三条路都试一遍:先在“数据”选项卡最右侧找;找不到就去“工具”菜单找“Excel 加载项”或“加载项”;再找不到就打开“插入 → 加载项”看看当前账户下有没有可用的加载项列表。我实际体验下来,Mac 版加载项开关的可见性比 Windows 更依赖版本,同一家公司两台 Mac 装的同一个版本号,菜单项位置也可能因为语言包不同而不一样。
2.3 列表里压根没有“规划求解”怎么办
这种情况通常有三个原因。第一是安装时没勾选完整组件,Office 安装程序里有个自定义安装选项,默认是全装的,但如果装机的人做过精简,加载项文件就不会被复制到本地。解决办法是从控制面板里对 Office 做一次“更改 → 添加或删除功能”,把 Excel 的加载项部分补上。
第二是企业受控环境限制了加载项。有些公司的 Office 会通过组策略把加载项列表锁掉,这时候你在界面上操作是没有任何反应的,勾了也白勾。这种情况问一下 IT 比自己在网上翻半天有用。
第三是文件本身的问题。如果你打开的是从别人那儿拿来的 .xls 老格式文件,加载项逻辑可能受兼容模式影响。我遇到过打开的 .xls 文件里“数据”选项卡是灰色的,把文件另存为 .xlsx 之后一切正常。
3. 把业务语言翻译成单元格:建模的三件套
3.1 目标单元格、可变单元格、约束区域各放什么
规划求解的对话框里只有三个必填项,对应模型的三要素。**“设置目标”填那个你要最大化或最小化的单元格,注意它必须是一个包含公式的单元格,而且这个公式要间接或直接引用到可变单元格——如果你填了一个纯数字,求解器会直接无视你。“通过更改可变单元格”填决策变量的区域,保持连续最好,虽然也支持多个不连续区域,但那样后期维护会很痛苦。“遵守约束”**就是你把每个限制写成一条条单元格引用加关系符。
这里有个细节值得强调:约束的两侧都可以是单元格引用。写$B$22 <= $B$13表示“实际消耗不超过可用量”,这种写法比直接把 100 硬写进约束里好太多,因为你后面想改数据只需要改单元格,不用重新进对话框改每一条约束。做可复用的模型,这一点是分水岭。
3.2 SUMPRODUCT 是最稳的线性表达式写法
线性表达式我强烈建议统一用 SUMPRODUCT,不要用=B5*B18+C5*C18这种展开写法。原因有两个:一是改动系数数量时 SUMPRODUCT 只需要调整区域引用,展开式得一个个加;二是 SUMPRODUCT 天然是线性结构,求解器识别线性模型时更省事。
=SUMPRODUCT($B$5:$C$5, $B$18:$C$18)这行的意思是:把两个同样尺寸的一维区域按位置相乘再求和。两个区域的尺寸和方向必须完全一致,一个横着一个竖着会直接返回#VALUE!。这是我见过最高频的建模错误,尤其是从别处复制数据过来的时候,粘贴选项把行转成了列,公式里的区域还是原来的方向。
3.3 整数、0-1 变量的表达方式
默认情况下,规划求解把变量当连续数处理,也就是可以出现 37.56 个产品这种现实里不成立的结果。要限制成整数,在约束列表里选好变量区域,关系符选int。要表达“选或不选”,就选bin。这两种约束在对话框里是独立的选项,不在<=那一类里,很多人第一次找不到就是因为一直在找“等于 0 或 1”的写法。
还有一个容易被忽略的复选框叫**“使无约束变量为非负数”**,默认是勾上的。如果你的模型里变量确实允许取负值(比如调拨量可以是负数表示反向调拨),那就得手动把它取消掉,否则求解器会偷偷加上一层>= 0,你可能一直想不明白为什么负解的方案永远出不来。
4. 参数对话框里每个开关在干什么
4.1 三种求解方法怎么选
下拉框里有三个引擎,选错的话轻则慢,重则直接告诉你“找不到解”。判断方法看模型里有没有非线性的东西。
**单纯形线性规划(Simplex LP)**用于目标和约束都是线性的模型。什么叫线性?所有变量只以一次方出现,变量之间只做加减和常数倍的乘法,没有乘积项、没有除法、没有 IF、MAX、ABS、ROUND 这类函数。这类模型它能给出全局最优解,而且速度快,是首选。
**广义递减梯度(GRG 非线性)**用于目标或约束里含光滑非线性函数的模型,比如涉及指数、对数、幂函数的情况。它的特点是会沿着梯度方向迭代,容易停在局部最优,也就是说同样的模型换个初始值可能得到不同的答案。所以用它的时候一定要多做几次不同初始值的尝试,或者打开“多初始点”选项。
**演化(Evolutionary)**用于目标函数不平滑、含有 IF、查表、离散判断之类情况。它靠随机变异加选择来搜,不依赖梯度,但代价是慢,而且不保证收敛到全局最优。我一般把它当最后手段,能用线性改写表达的逻辑,就尽量改写成线性,比如用 0-1 变量加一个大常数来替代 IF 分支。
4.2 选项里的精度、缩放、整数最优性
点“选项”按钮进去,里面的参数比主对话框重要得多。约束精确度默认是 0.000001,表示求解器认为“约束满足”的容差。这个值不能随便调大,调大到 0.01 的话,一个应该满足<= 100的约束解出来 100.008,求解器也会认为没问题。反过来调得太小,浮点误差会让它误判可行域为空。
使用自动缩放这个勾在小模型里无所谓,但在系数数量级差异很大的模型里必须打开。举个例子,一个约束里某个变量的系数是 0.0003,另一个是 850000,求解器在做数值运算时小系数那部分的信息会被大系数淹没。自动缩放会内部把量级拉平,代价是极小的精度损失。
整数最优性这个参数是最容易吃亏的地方。它默认不是 0,而是留了一个百分之一的容差(具体数值不同版本略有差别),意思是只要当前解和理论上界的差距在这个范围内,求解器就认为“够好了”直接停手。做预算取舍、选址、排班这类问题,百分之一的差距可能就是几万块的差别。要精确最优就把它改成 0,代价是求解时间变长。
另外两个默认值也要注意:最长求解时间默认 100 秒,迭代次数默认 100 次。模型一复杂,100 次迭代根本不够,求解器会停下来告诉你“未收敛”,这时候第一件事就是去把这两个数字加大。
4.3 三种报告怎么读
求解结束后会弹出一个“报告”列表,可以选运算结果、敏感性、极限值三种。运算结果报告是最基础的,它列出目标单元格和每个可变单元格的初值、终值,以及每条约束的状态。约束状态里“到达限制值”表示这条约束卡住了,“未到限制值”表示还有余量,后面那列松弛量就是余量的大小。
敏感性报告是真正有价值的一份。它给出两个方向的信息:一是目标函数系数的允许增量、允许减量,告诉你单个产品的单位利润在多大范围内波动,当前的最优组合不会变;二是约束右端值的影子价格和允许变动范围,告诉你每多一个单位的资源,目标值能提升多少。
注意:整数约束一加上,敏感性报告通常就不生成了。这不是 bug,是因为整数模型的目标函数是阶梯状的,没有连续可导的斜率概念。需要敏感性分析时,先取消整数约束跑一次连续模型看看趋势,再决定最终方案。
极限值报告给的是某个变量取不同值时目标和约束的变化,做单因素敏感性分析时比手动做数据表快。
5. 完整跑一遍:两种产品的排产模型
5.1 数据结构与公式搭建
用一个小规模但数字可手工验算的例子,方便你确认求解器没算错。假设工厂生产甲、乙两种产品,单位利润分别是 40 元和 30 元。两种产品都要消耗原料 A 和原料 B,工时消耗只有甲产品需要。
| 单元格 | 内容 | 数值或公式 |
|---|---|---|
| B5:C5 | 单位利润 | 40 / 30 |
| B8:C8 | 原料 A 单耗 | 2 / 1 |
| B9:C9 | 原料 B 单耗 | 1 / 1 |
| B10:C10 | 工时单耗 | 1 / 0 |
| B13 | 原料 A 可用量 | 100 |
| B14 | 原料 B 可用量 | 80 |
| B15 | 可用工时 | 40 |
| B18:C18 | 决策变量 x1、x2 | 初始填 0 |
| B22 | 原料 A 实际消耗 | =SUMPRODUCT($B$8:$C$8,$B$18:$C$18) |
| B23 | 原料 B 实际消耗 | =SUMPRODUCT($B$9:$C$9,$B$18:$C$18) |
| B24 | 工时实际消耗 | =SUMPRODUCT($B$10:$C$10,$B$18:$C$18) |
| B27 | 总利润(目标) | =SUMPRODUCT($B$5:$C$5,$B$18:$C$18) |
摆好之后,在规划求解对话框里设目标为$B$27,选“最大值”,可变单元格填$B$18:$C$18,约束加四条:$B$22 <= $B$13、$B$23 <= $B$14、$B$24 <= $B$15、$B$18:$C$18 >= 0。求解方法选单纯形线性规划。
5.2 第一次求解与手工验算
点求解,结果应该是甲 20 件、乙 60 件,总利润 2600。手工验算一下:2×20 + 60 = 100,原料 A 刚好用尽;20 + 60 = 80,原料 B 也刚好用尽;工时用了 20,还有 20 的富余。利润 40×20 + 30×60 = 2600。
再对比几个边界方案确认这是最优:如果全做乙,受原料 B 限制最多 80 件,利润 2400;如果甲做到工时的上限 40 件,剩下的原料只能做 20 件乙,利润 2200。三个顶点比下来 2600 确实最高。这一步手工校验很值得做,因为后面你改参数改到模型变形的时候,需要一个可信的基准来对照。
5.3 用敏感性报告做产能决策
求解完成后选“敏感性”报告,会得到两张表。这里把关键数字列出来,你可以直接对照。
| 变量 | 最优值 | 目标系数 | 允许增量 | 允许减量 |
|---|---|---|---|---|
| x1(甲) | 20 | 40 | 20 | 10 |
| x2(乙) | 60 | 30 | 10 | 10 |
| 约束 | 右端值 | 影子价格 | 允许增量 | 允许减量 |
|---|---|---|---|---|
| 原料 A | 100 | 10 | 20 | 20 |
| 原料 B | 80 | 20 | 20 | 20 |
| 工时 | 40 | 0 | 1E+30 | 20 |
这张表怎么用?看第二张表。原料 B 的影子价格是 20,意思是在允许范围内,每多拿到一个单位的原料 B,总利润增加 20 元;原料 A 的影子价格是 10,增加一个单位只值 10 元。如果采购部门问“多花钱买哪个原料划算”,答案很直接:原料 B 的边际价值是 A 的两倍。允许增量是 20,说明这个结论在原料 B 从 80 加到 100 的区间内都成立,超过 100 之后影子价格会变,因为那时候瓶颈可能转移到别的约束上了。
工时的影子价格是 0,说明它现在有富余,加产能一分钱利润都不涨。允许减量是 20,意味着工时哪怕砍到 20 小时,最优方案和总利润都不变——这个结论在砍预算的时候非常有用,能帮你挡住“一刀切按比例砍”的做法。
第一张表同样有价值。甲产品的单位利润允许减量是 10,也就是说即使市场压价到 30 元一件,最优组合还是甲 20 件、乙 60 件;但一旦跌到 30 元以下,组合就要变了。这种判断在定价谈判前做一次,心里会踏实很多。
6. 0-1 变量的实战:预算内的项目取舍
6.1 模型搭建与约束写法
再上一个典型场景:手上有一笔预算,要在五个项目里挑几个做,每个项目的投入和预期收益已知,总投入不能超过上限。
| 项目 | A | B | C | D | E |
|---|---|---|---|---|---|
| 成本 | 8 | 5 | 6 | 4 | 7 |
| 收益 | 12 | 9 | 8 | 5 | 11 |
预算上限 20 个单位。可变单元格就是五个 0-1 变量,中间放两行 SUMPRODUCT:一行算总成本,一行算总收益。目标单元格设为总收益最大,约束是“总成本 <= 20”,再加上五个变量的bin约束。
求解结果是选 A、B、E,总成本 8+5+7=20,总收益 12+9+11=32。手工验证一下其他组合:A+B+C 成本 19 收益 29;A+C+E 成本 21 超预算;B+C+D+E 成本 22 超预算;A+D+E 成本 19 收益 28。32 确实是能取到的最高收益,而且预算刚好用满。
这个模型看着简单,但它是我实际用得最多的模板之一。凡是“选或不选”“做或不做”“上或不上”的决策,本质都是这个结构:一堆 0-1 变量、一条资源总量约束、一个求和型目标。多加几条约束就能处理“A 和 B 必须同时选”“C 和 D 互斥”这类逻辑,后者写成xC + xD <= 1就行。
6.2 整数容差带来的“0.9999”问题
0-1 模型跑完,你可能看到变量值显示成 0.999999987 或者 0.000000412,而不是干净的 1 和 0。这不是求解器犯错,而是浮点运算加上整数容差的正常表现。处理办法有两个:一是把“整数最优性”改成 0 减少这种残留;二是在读取结果的地方套一层 ROUND,比如另起一列写=ROUND(B18,0)专门用于展示和后续计算。
千万别拿带小数尾巴的结果去做后续的乘法或判断。我踩过一次,把 0.999999987 直接当成选中数量去算成本,结果总额差了零点零几,后面用这个数去比对预算时逻辑判断出错,排查了半个多小时才发现是这里的问题。
6.3 逻辑约束怎么写才不会把模型变非线性
“如果选了 A 就必须选 B”这类条件,很多人第一反应是用 IF 函数写,写完求解器就告诉你线性条件不满足。正确做法是用大 M 法:写成xA - xB <= 0,意思是选了 A(x A=1)就必须让 xB 也等于 1。至于“选 A 则成本增加某个固定值”这种情况,可以引入辅助 0-1 变量配合一个大常数,把分段逻辑摊平成线性的不等式组。
改写成线性的好处不只是求解器能用单纯形,更实际的是你能拿到敏感性报告。用 IF 函数搭的模型只能走演化引擎,跑得慢还没有影子价格可看,做决策分析时等于自断一条腿。
7. 踩坑排查链路:从“找不到解”到“结果不对”
7.1 报“找不到可行解”时的排查顺序
这个报错信息本身信息量很低,它只是说在你给的约束下找不到任何一个点。我的排查顺序是这样的。
第一步,先把所有整数和 0-1 约束删掉,只留连续变量重跑一次。如果能跑出解,说明可行域本身没问题,是整数组合太苛刻——比如预算约束卡得太死,任何整数组合都超一点点。这时候要么放宽预算,要么接受一个近似解。
第二步,如果去掉整数约束还是无解,把约束按“最可能矛盾”的顺序一条条加回去。先只加一条跑一次,再加第二条,直到某次加上去之后突然无解,那条就是矛盾源。这个方法笨但非常有效,因为约束写多了之后肉眼是看不出冲突的。
第三步,检查约束关系符的方向有没有写反。>=和<=在对话框里是下拉选择,中文界面上显示的是“>=”和“<=”,很容易在复制粘贴的时候看错。还有一种是两侧区域尺寸不一致被自动扩展成了笛卡尔积约束,加了十条你只想要一条的约束,可行域瞬间被压成空集。
第四步,检查变量的非负设置。如果模型里某个变量本该可以为负,但“使无约束变量为非负数”是勾上的,那它实际被强行加了>= 0,某些本来有解的模型会因此变成无解。
第五步,放宽约束精确度试一次。默认 1E-06 在数值条件差的模型里可能太严,改成 1E-04 跑一下看是否有解,如果有解说明是数值精度问题而不是模型问题,那就需要开自动缩放,而不是长期用宽松的精度。
7.2 结果和手工算的不一致
这类问题的根源统计下来,排第一的是目标单元格里没有引用可变单元格。很多人的模型里目标是通过一串中间公式算出来的,中间某一步写成了引用常量,整条链条断掉,求解器发现改可变单元格对目标毫无影响,就什么也不做,返回一个“当前值即为最优”的结果。验证方法是改一下某个变量的值,看目标单元格有没有跟着变,不变就是引用断了。
第二个原因是存在易失函数。像 OFFSET、INDIRECT、TODAY、NOW、RAND 这几个,每次重算都会触发全表重算。放在求解模型里,求解器每一轮迭代都要重新计算整个工作簿,不仅慢,而且如果模型里有 RAND 这类随机函数,等于每次迭代看到的都是不同的模型,结果自然不可能稳定。用 INDEX 替代 OFFSET 是最常见的改法。
第三个原因是精度和缩放。系数跨度大的模型没开自动缩放,求解器算出来的解会有可见偏差,比如本该是 100 的约束解出来 100.0004。这种情况先开自动缩放,再把精度调到合理值,最后用 ROUND 做展示层处理。
7.3 macOS 上特有的几个坑
Mac 上的问题主要集中在宏和文件格式两个方面。加载项引用路径不一样是第一个坎:Windows 上你在 VBA 编辑器里引用 SOLVER.XLAM 就行,Mac 上加载项文件的位置和名称都可能不同,有时候需要把加载项从加载项文件夹里找出来重新引用。如果引用加不上,退而求其次的办法是用Application.Run带上完整的加载项宏名去调用,兼容性更好但代码可读性差。
文件格式是第二个坎。老版 .xls 格式在 Mac 上打开时可能触发兼容模式,规划求解的某些选项会灰掉。顺手另存为 .xlsx 能省掉很多莫名的问题。
第三个是性能。同一个模型,公式量上万的时候,Mac 版跑求解的耗时明显比 Windows 长。如果模型确实大,可以考虑把中间过程改成数值粘贴、把易失函数清掉、把不必要的条件格式和数组公式删掉,这些对速度的影响比换机器更直接。
8. 把模型沉淀成模板:命名区域与批量跑
8.1 命名区域 + 数据与模型分层
模型只要能跑通一次,就值得花十分钟做一层结构整理。我习惯把它分成三层:最上面是数据层,放各种系数、上限、单价,这一层用 Excel 表格(Ctrl+T)做结构化,方便以后追加数据;中间是模型层,放可变单元格、中间计算和目标单元格,这一层用普通区域配命名区域,因为命名区域在约束对话框里显示成名字而不是$B$18:$C$18,可读性完全不是一个级别。
命名的方法是在“公式”选项卡里打开名称管理器,把决策变量区域命名成类似Vars、把系数区域命名成Coeff。之后在规划求解对话框里直接输入Vars就能引用,约束里写CostUsed <= Budget这种自解释的表达式,几个月后回来看也不会一头雾水。这一点对需要交给别人维护的模型尤其重要,我见过太多因为一堆绝对引用而没人敢动的表格。
注意:结构化引用(表格的
表名[列名]语法)不要直接用在规划求解的约束里。求解器对结构化引用的解析在不同版本上表现不一致,保险的做法是在表格外面用普通区域加 INDEX 引用一次,再把命名区域指向这块普通区域。
8.2 用 VBA 批量改参数并记录结果
做产能规划的时候,经常需要看“预算从 10 变到 30,每种情况下最优收益是多少”。手动改二十次再抄二十次结果,既慢又容易抄错。这时候用一段循环就够了。
Sub RunSolverBatch() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("Model") Dim i As Long For i = 1 To 20 ' 把第 i 个预算值写进约束单元格 ws.Range("B2").Value = ws.Range("F" & i + 1).Value SolverReset SolverOk SetCell:=ws.Range("C10"), MaxMinVal:=1, _ ByChange:=ws.Range("C5:D5"), Engine:=1 SolverAdd CellRef:=ws.Range("C15"), Relation:=1, _ FormulaText:=ws.Range("B2") SolverAdd CellRef:=ws.Range("C5:D5"), Relation:=3, FormulaText:=0 SolverOptions Precision:=0.000001, Convergence:=0.0001, _ AssumeLinear:=True, IntegerOptimality:=0 SolverSolve UserFinish:=True SolverFinish KeepFinal:=1 ws.Range("H" & i + 1).Value = ws.Range("C10").Value Next i End Sub几个参数解释一下。MaxMinVal里 1 表示求最大、2 表示求最小、3 表示求特定值。Relation里 1 是<=、2 是=、3 是>=,跟直觉的顺序不一样,很容易写错,写完一定要用一个已知答案的小例子验证一次。AssumeLinear设成 True 等价于在界面上选单纯形线性规划,速度差异非常大。
UserFinish:=True表示求解完不弹结果对话框,KeepFinal:=1表示保留最终解。这两个设置是批量跑循环的关键,少了它们每一轮都会卡在对话框上等你点确定。另外循环开头一定要有SolverReset,否则上一轮的约束会累积到这一轮,跑出来的结果莫名其妙。
在 Mac 上跑这段代码,大概率需要额外处理加载项引用,或者在调用前先用Application.Run绑定一次求解器的宏名。我的经验是先在 Mac 上用一个两变量的最小模型把宏跑通,再往里加复杂度,不然调试信息会很混乱。
8.3 交给别人之前的自查清单
模型做完,在发给同事之前我会走一遍这几条。第一,把所有约束的右端值改成从单元格读取,不留硬编码的数字。第二,把目标单元格和可变单元格都命名,并在工作表上写一行说明文字,写清楚哪个区域是输入、哪个是输出。第三,用一组已知答案的小数据做回归测试,确认模型在别人机器上复现的结果跟你的完全一致。第四,说明平台差异,如果文件里带 VBA,一定要注明在 macOS 上需要额外配置,不然对方打开就是一堆报错。
还有一条容易被忽略的:把求解器的参数设置截图或者写成注释放在表里。求解方法、精度、整数最优性这些参数不在工作簿里保存(除了通过 VBA),换个人打开文件重新点一次求解,参数就是默认值。如果默认值和你的设置不一样,结果就可能不同,而对方完全不知道问题出在哪。
9. 关于模型规模的一点实际感受
内置规划求解是第三方引擎的精简版本,变量和约束的数量上限比专业优化工具低不少,具体数字随版本变化。我在实际使用中的体会是,决策变量在几十到一两百个这个量级、约束在几十条这个量级,它跑起来很舒服,秒级出结果;上到几百个变量加几百条约束,尤其是带整数约束的,等待时间就开始变得难熬,有时候还会提示找不到解,其实不是模型有问题,是它搜不动了。
规模到那个程度,我的做法是分两步:先用 Excel 里的一个小规模版本(比如把时间段聚合成周而不是天)把逻辑和量级验证清楚,确认模型结构没问题,再考虑把数据导出去用别的工具跑。这样既不用一开始就上重型工具,也不会在一个跑不动的模型上反复怀疑自己公式写错了。
另外分享一个小技巧:求解之前先手动给可变单元格填一组可行的初值,而不是留着全 0。对线性模型影响不大,但对非线性模型,一组接近答案的初值能显著减少陷入局部最优的概率,也能让求解器更快判断出可行域在哪里。这个习惯是从一次配料模型上养成的,那个模型用全 0 初值跑出来的结果总比实际最优低一截,换了初值之后每次都能稳定收敛到同一个更好的解上。