案例 077 周末该多跑还是多歇
案例 077 周末该多跑还是多歇
摘要:周六单量是平日的 1.25 倍、单价 5.50 元(比平日高 0.4 元),毛收益 21.31 元/h,看上去周末该把 12 小时跑满。但疲劳不是当天的账:\(F_{t}=0.85F_{t-1}+h_{t}\) 把每一天的小时都带进后面所有天,折扣 \((1-0.02F_{t})\) 在疲劳高位时会把收入砍到三成。把单日 12 小时、一周 66 小时、疲劳上限 40 三条约束一起写进模型,最优排班是 11.3/7.4/5.5/4.7/5.9/10.7/11.7 小时,合计 57.13 小时——66 小时的额度根本没用完;周六只排 10.65 小时而不是 12 小时,周净收益 491.0 元。同一组数据,每天均摊 9.43 小时得 466.5 元、周末堆满得 470.7 元,分别比优化少 24.5 与 20.3 元。
关键词:多期疲劳递推;分段线性化;时长分配;影子价格;边际收益递减
1 问题提出
1.1 问题是什么
一周七天,每天最多在线 12 小时,一周合计不超过 66 小时。单量系数以平日为 1.00:周二周三 0.98,周五 1.08,周六 1.25、周日 1.05;单价平日 5.10 元,周六 5.50 元(+0.4)、周日 5.30 元。折算成无疲劳的小时毛收益,周六 21.31 元/h,平日 15.81 元/h,差 5.50 元/h。
表 1 七天单量系数、单价与最优排班(金额单位:元)
| 日序 | 星期 | 单量系数 | 单价 | 毛收益/h | 最优小时 | 疲劳 F | 折扣因子 | 净收益 |
|---|---|---|---|---|---|---|---|---|
| 1 | 周一 | 1.00 | 5.10 | 15.81 | 11.26 | 11.26 | 0.775 | 137.9 |
| 2 | 周二 | 0.98 | 5.10 | 15.49 | 7.44 | 17.01 | 0.660 | 76.1 |
| 3 | 周三 | 0.98 | 5.10 | 15.49 | 5.49 | 19.95 | 0.601 | 51.1 |
| 4 | 周四 | 1.00 | 5.10 | 15.81 | 4.72 | 21.68 | 0.566 | 42.3 |
| 5 | 周五 | 1.08 | 5.10 | 17.07 | 5.87 | 24.29 | 0.514 | 51.5 |
| 6 | 周六 | 1.25 | 5.50 | 21.31 | 10.65 | 31.30 | 0.374 | 84.9 |
| 7 | 周日 | 1.05 | 5.30 | 17.25 | 11.70 | 38.30 | 0.234 | 47.2 |
直觉会让人把周六、周日各堆满 12 小时,剩下的 42 小时摊给五个工作日。表 1 的模型解否掉了这个直觉:周六只要 10.65 小时,周日 11.70 小时最多,周一 11.26 小时次之,周四最少(4.72 小时)。
1.2 为什么要优化
第一,疲劳是跨期的。今天的小时按 0.85 的比例留到明天,七天下来权重和是 4.671 倍——一周 60 小时不是"每天 8.6 小时",而是"某一天的 8.6 小时会在后面变成 8.6、7.3、6.2、5.3……的连续负担",这个几何级数人脑算不准。第二,边际收益也不是常数。多跑一小时的收入是 \(r_{t}(1-0.02F_{t})\),但它同时把当天的折扣再压低 \(0.02r_{t}h_{t}\)、把后面所有天的折扣压低 \(0.02\times0.85^{s-t}r_{s}h_{s}\)。三项一减,周六的第 12 小时边际是 -1.15 元——负的。第三,约束之间会互相"抵消"。66 小时的额度与 40 的疲劳上限不是两个独立的天花板,它们谁先到谁说了算;本案例里两个都没到,先用完的是"划算的小时"。
1.3 Gurobi 如何求解
决策是 7 个连续变量 \(h_{t}\);疲劳 \(F_{t}\) 由递推等式定义,不是独立决策;折扣因子用 addGenConstrPWL 在 9 个折点上定义为 \(F_{t}\) 的分段线性函数;目标里的 \(h_{t}\cdot g_{t}\) 是一条双线性约束,靠 NonConvex=2 打开空间分支。7 个双线性项、21 个变量,单次求解 0.5 秒。真正花时间的是扫描:疲劳上限 6 点、总时长 8 点、衰减系数 6 点、周末系数 6 点、二维网格 20 点,共 40 余次求解,全部墙钟 25 s。
2 数学模型
\(d_{t}\) 为单量系数,\(p_{t}\) 为单价(元/单),\(h_{t}\) 为在线小时,\(F_{t}\) 为疲劳累积,\(g_{t}\) 为折扣因子,\(e_{t}\) 为疲劳折算后的有效小时,\(r_{t}=3.1d_{t}p_{t}\) 为无疲劳小时毛收益,\(\bar{F}\) 为疲劳上限,\(\bar{T}\) 为总时长额度。
2.1 目标函数
式 (1) 是全案例的骨架:一个等式把七天的决策串成一条链。\(F_{t}\) 没有独立的经济含义,它是历史小时按 \(0.85^{t-s}\) 的加权和;写成递推而不是求和式,是因为约束 \(F_{t}\le40\) 要逐日成立,递推形式天然给出每一天的疲劳水平,也让"今天多跑一小时"的后果自动落到后面每一天上。式 (2) 中 \(3.1d_{t}h_{t}\) 是单量(每小时 3.1 单),乘单价得毛收入,再乘折扣。周六的 \(d\)、\(p\) 同时抬高这个系数,但折扣也按同一系数放大绝对损失——这正是"系数最高的那天未必最该多跑"的根源。
2.2 约束条件
式 (3) 是两条容量,式 (4) 是疲劳上限。式 (5) 把折扣因子写成 \(F\) 的分段线性函数:原函数本身是线性的,9 个折点、8 段,PWL 是精确表示而不是近似——这里的 PWL 不是为了拟合曲率,而是为了让折扣因子成为一个可被约束、可被检验的变量。式 (6) 是双线性项,\(h_{t}\) 与 \(g_{t}\) 都是变量,需要辅助变量 \(e_{t}\) 承载。
式 (7) 定义两个对照排班;若要求均摊又要守住上限,最大日时长是 \(\bar{h}=40/\sum_k0.85^{k}=40/4.671=8.83\) 小时,除数就是一周疲劳的几何权重和——它把 66 小时的额度换算成了 8.83 小时/天。式 (9) 是某一小时的边际账单:自身收入减自身折扣、再减它对后面各天的溢出。式 (8) 是疲劳上限的影子价,用中心差分而不是读对偶值——双线性模型在空间分支下的对偶不稳,差分更可靠。
2.3 规模与性质
7 个连续决策、7 条递推等式、7 条疲劳上限、7 条双线性、2 条容量,共约 30 条约束、21 个变量。双线性让问题非凸,但只有 7 个乘积项、每项两个有界变量,空间分支树极浅;实测单次 0.5 秒、相对间隙 1% 内。值得注意的是:目标在非负象限上是凹的(\(\sum_t r_t h_t F_t\ge0\) 恒成立),所以最优解唯一,不同初值、不同折点密度都收敛到同一组小时——图 6 的 20 个格子与表 1 的排班完全自洽,靠的就是这一点。
3 Gurobi 求解实现
3.1 建模要点
核心五行对应式 (1)(4)(5)(6):
F = m.addVars(7, lb=0, ub=80); g = m.addVars(7, lb=0.2, ub=1.0)
for t in range(1, 7):
m.addConstr(F[t] == DEC * F[t-1] + h[t]) # 递推,本案例的核心
m.addConstr(F[t] <= CAP) # 疲劳上限
for t in range(7):
m.addGenConstrPWL(F[t], g[t], XP, YP) # 折扣因子的分段线性
m.addConstr(e[t] == h[t] * g[t]) # 双线性项
m.setObjective(quicksum(r[t] * e[t] for t in range(7)), GRB.MAXIMIZE)
第一个细节:\(F_{t}\) 的变量上界要放到 80 而不是 40。约束写 \(F_{t}\le40\) 是一回事,ub 是另一回事;若把 ub 也设成 40,PWL 定义域被截断,扫描上限参数时模型直接 infeasible,扫不出一条曲线。第二个细节:NonConvex=2 不能省。默认设置下 Gurobi 拒绝求解含双线性的模型,直接报错;打开之后它用空间分支处理这 7 个乘积,相对间隙压到 1% 只需 0.5 秒。第三个细节:扫描时改用等价的直接写法 \(e_{t}=h_{t}(1-0.02F_{t})\),省掉 7 个 PWL 变量,速度快 4 倍。两种写法的目标值一致到 \(10^{-4}\) 以内(490.9987 对 490.9987),本文所有图用快写法生成,表 1 的排班用 PWL 写法复核。
3.2 求解结果
表 2 三种排班的净收益与疲劳后果(金额单位:元)
| 策略 | 每天小时 | 周总时长 | 周净收益 | 周日疲劳 | 与优化差 |
|---|---|---|---|---|---|
| 每天均摊 | 9.43 | 66.0 | 466.5 | 42.71 | -24.5 |
| 周末堆满 | 8.4/12 | 66.0 | 470.7 | 44.71 | -20.3 |
| 同总时长均摊 | 8.16 | 57.1 | 479.3 | 36.97 | -11.7 |
| 本案例优化 | 见表 1 | 57.1 | 491.0 | 38.30 | 0.0 |
三行基线的差别不在"跑多少小时"(前三行都是 66 小时额度),而在"怎么摊"。均摊 466.5 元、堆满 470.7 元、同总时长均摊 479.3 元、优化 491.0 元。把小时堆到系数最高的那两天,只比均摊多 4.2 元(代价是周日疲劳从 42.71 顶到 44.71);而把总时长从 66 砍到 57.13 小时、仍按均摊跑,反倒多赚 12.7 元——前者是把钱堆在最贵的地方,后者是把最不划算的小时直接丢掉。优化排班只跑 57.13 小时,比前两行各少 8.9 小时,收益反而最高——少跑的那 8.9 小时本来就是负收益的。
4 结果分析

图 1 七天单量系数与单价:周六单量 ×1.25、单价 +0.4 元
4.1 结构:七天的不对称 图 1 把单量系数(棒棒糖)与单价(右轴菱形)画在一起。周六双高:单量 1.25、单价 5.50;周日退到 1.05 与 5.30;周五 1.08 是工作日最高。图 1 只交代收益侧,疲劳侧在下一张。

图 2 在线小时与单位小时净收益:气泡为当日净收益,短横为无疲劳毛收益
4.2 小时—边际收益 图 2 横轴是各天实际排的小时,纵轴是单位小时净收益,短横是无疲劳毛收益。所有点都落在短横下方,越靠右落得越多:周一 11.26 小时还能拿到 12.25 元/h,周日 11.70 小时只剩 4.04 元/h。虚线是周六的全边际曲线(含对周日的溢出),它在 12 小时处已经跌到零以下——周六的第 12 小时是倒贴的,这就是"为什么不是周六堆满"的直接答案。

图 3 三种排班策略的周净收益对比
4.3 三策略对比 图 3 是主图。均摊 466.5 元、堆满 470.7 元、优化 491.0 元。折扣吃掉的钱可以直接读:毛收益合计 982.0 元、到手 491.0 元,折扣吃掉 491.0 元,占 50%。这不是百分比修辞——491.0 元是三种排法之间差距(24.5—20.3 元)的二十倍。排班的胜负,本质是"谁的折扣损失小",而不是"谁跑得多"。

图 4 七天在线小时与疲劳累积的时序(点线为疲劳上限 40)
4.4 时序:小时与疲劳 图 4 左轴柱是排班小时,右轴是疲劳曲线。小时不是单调下台阶,而是两头高、中间低:周一 11.26、周六 10.65、周日 11.70,周三周四只有 5.49 与 4.72。原因在式 (9):周一早,累积疲劳低,自己那小时的折扣损失小;周日是最后一天,它的疲劳没有明天可以溢出;周三周四既不早也不晚,两头的好处都占不到,只能让路。

图 5 疲劳衰减系数扫描:周净收益与周日疲劳
4.5 主灵敏度:疲劳衰减系数 图 5 横轴是 0.85 从 0.70 扫到 0.94。系数越大(恢复越慢),周净收益从 633.6 元掉到 409.1 元,周日疲劳从 30.8 涨到 40.0。0.85 落在曲线最陡的一段:再慢 0.05(到 0.90),收益要再掉 46.8 元。系数到 0.94 时疲劳几乎不衰减,周日疲劳撞上 40,上限才开始真正 binding——"上限是不是紧"取决于体力恢复速度,不取决于上限本身。

图 6 总时长 × 疲劳上限 二维网格上的最优净收益
4.6 二维:总时长 × 疲劳上限 图 6 的 20 个格子里,从左到右(额度 46→70)收益上升后转平,从下到上(上限 30→44)也在上升。上限 40 与 44 两行几乎相同,而与 30 那一行差 18.8 元——40 在本案例是"松的",真正卡住收益的是折扣本身,不是上限;额度过 58 之后同样转平,与图 8 的前沿一致。

图 7 周六单量系数扫描:周净收益与周六小时
4.7 第三维:周六单量系数 图 7 把周六的单量系数从 1.00 拉到 1.50。周收益从 475.8 涨到 509.8 元,周六小时从 8.50 爬到 12.00——系数 1.00 时只排 8.50 小时,一路加到 1.50 才顶到 12 小时的天花板。系数涨五成,收益只涨 7%——多出来的单量主要靠"单位小时更值钱"兑现,不是靠"多干几小时":周六小时涨了 41%。

图 8 总时长额度—净收益前沿与超出 66 h 的不可行区
4.8 前沿与不可行区 图 8 横轴是一周总时长额度,纵轴是最优净收益。曲线在 58 小时之前一路向上,之后转平;绿点线标出实际用满的 57.13 小时,它在平台的起点之前一点点——这说明 66 小时的额度是宽裕的,把额度从 66 砍到 60 收益也几乎不变,砍到 50 才开始疼。斜率从 46 小时处的 3.76 元/h 掉到 70 小时处的 0.00 元/h,这条曲线的斜率就是"多跑一小时值多少钱"。

图 9 单量 ±12% 波动下的周净收益分布
4.9 分布:单量波动 图 9 给每天的单量系数加 ±12% 的正态扰动,跑 4000 次。周净收益均值 491 元、P5 451 元、P95 530 元,变异系数 0.049。均值与确定性解 491.0 元几乎重合,因为折扣因子对 \(h\) 是线性的、扰动只影响毛收益,期望不被扭曲;但 530 与 451 之间拉开 79 元,这才是"这周能赚多少"的真实口径。

图 10 各天收益分解:毛收益、疲劳折扣损失与累计份额
4.10 各天贡献分解 图 10 每根柱由两段组成:深色是到手净收益,浅色是被折扣吃掉的部分,柱顶是毛收益。七天毛收益 982.0 元,周六一天 227.0 元、占 23%,是最大的一块;但周六被吃掉的也有 142.1 元。被吃掉最多的是周日(154.6 元)与周六(142.1 元)——正是系数最高的两天。优化做的事,就是把这两天的小时压到"折扣还不算太狠"的位置,把省下的小时挪给周一。
5 结论
- 周六不该堆满 12 小时。 最优排班 11.3/7.4/5.5/4.7/5.9/10.7/11.7 小时,周六 10.65 小时。原因不是周六不划算(21.31 元/h 最高),而是周六每一小时都按 0.85 折到周日,而周日自己也排了 11.70 小时;周六第 12 小时的边际是 -1.15 元,倒贴。
- 66 小时的额度用不完。 最优只跑 57.13 小时,比额度少 8.9 小时;把额度从 66 砍到 60,收益几乎不变(图 8)。"能跑 66 小时"和"该跑 66 小时"是两件事。
- 疲劳折扣吃掉 491.0 元(毛收益的 50%)。 毛收益 982.0 元、到手 491.0 元。三种排法的差距是 24.5—20.3 元量级,折扣损失是 491.0 元量级——选排法就是选折扣损失最小的那条路径。
- 疲劳上限 40 不 binding,余量 1.7 单位,影子价 0.00 元。 分界点就是解出来的 \(F_{7}=38.30\):上限压在 32—40 之间时每单位只值 1.35 元,压到 28 附近涨到 4.53 元/单位——上限越紧,代价涨得越快。上限是安全阀,不是瓶颈;真正的瓶颈是折扣本身。
- 连续第 7 天值得跑,但只值 37.6 元。 强制周日休息的排班(11.6/8.0/6.4/6.2/8.2/12.0/0.0)收益 453.4 元,比最优低 37.6 元。周日的 11.70 小时是"没有明天"的小时,不必为溢出买单,所以仍然划算;但这一天的边际已经薄到 3.2 元/小时的量级——再累一点、再热一点,它就翻负了。
参考文献
[1] Gurobi Optimization LLC. Gurobi Optimizer Reference Manual[Z]. 2024.
[2] Kellerer H, Pferschy U, Pisinger D. Knapsack Problems[M]. Berlin: Springer, 2004.
[3] Martello S, Toth P. Knapsack Problems: Algorithms and Computer Implementations[M]. Chichester: John Wiley & Sons, 1990.
[4] 司守奎, 孙玺菁. 数学建模算法与应用[M]. 3 版. 北京: 国防工业出版社, 2021.

浙公网安备 33010602011771号