本文介绍如何在 Gekko 中对非连续时间索引(如跳周的 week = [13, 14, 17, 18, ...])建模同时满足「最小持续时长 d」和「最小间隔 s」的二元决策约束,核心是将问题重构为“起始点选择 + 后处理展开”,避免在稀疏时间轴上直接枚举窗口。
本文介绍如何在 gekko 中对非连续时间索引(如跳周的 `week = [13, 14, 17, 18, ...]`)建模同时满足「最小持续时长 d」和「最小间隔 s」的二元决策约束,核心是将问题重构为“起始点选择 + 后处理展开”,避免在稀疏时间轴上直接枚举窗口。
在实际优化场景中(如促销排期、设备启停计划),我们常需确保某类活动:
持续至少 d 个连续时间单位
(例如连续 2 周促销);
相邻两次活动起始点至少间隔 s+1 个时间单位
(即中间至少空 s 周,如 s=1 表示不能连续两周启动新活动,但允许重叠?不——此处指“启动间隔”)。
难点在于:当时间轴本身存在间隙(如 week = [13, 14, 17, 18, 33, 34, ...]),传统基于 i, i+1, i+d 的数组下标逻辑会失效——因为 week[i] + 1 不一定存在于 week 中,更无法保证 week[i] + d 存在。
✅ 正确解法是
分离建模逻辑
:
仅在合法起始位置定义整数变量 st[i] ∈ {0,1}
,表示“是否在第 i 个时间点(即 week[i])启动一个新周期”;
通过预计算约束确保:
若 st[i] = 1,则后续 d−1 个实际存在的连续周必须可用(即 week[i], week[i+1], ..., week[i+d−1] 必须构成严格递增且无跳跃的子序列);
任意两个 st[i] = 1 和 st[j] = 1 满足 week[j] ≥ week[i] + d + s(即前一周期结束周 week[i]+d−1 与后一周期起始周 week[j] 至少间隔 s 周);
最终执行状态 x7[k] 通过后处理生成
:对每个 i,若 st[i] == 1,则将 x7[i] 至 x7[i+d−1] 置为 1(前提是索引有效)。
以下是适配原始问题中非连续 week 数组的完整实现:
?
关键注意事项
:
✅
不要直接对 x7 施加 d/s 约束
:因时间轴稀疏,x7[i] 与 x7[i+1] 不代表真实时间邻接;
✅
st 是唯一优化变量
,x7 是确定性派生量,不参与求解,大幅降低问题复杂度;
⚠️ 若需 x7 参与其他约束(如资源上限),应改写为 st 的线性表达式(如 sum(x7) == sum_{i} sum_{k=0}^{d-1} st[i]·I[i+k 100),可预计算合法起始索引集 valid_starts 和冲突对 (i,j),用稀疏循环替代全矩阵遍历,提升性能。
该方法将离散组合约束转化为清晰的整数规划结构,在 Gekko 中稳定收敛,适用于任意非均匀时间序列的周期调度场景。
import numpy as np
import pandas as pd
from gekko import GEKKO
# 示例时间轴(含间隙)
week = pd.Series([13, 14, 17, 18, 19, 26, 28, 33, 34, 35, 39, 42, 45, 46, 47, 48, 50, 51, 52])
n = len(week)
d = 2 # 最小持续周数
s = 1 # 最小间隔周数(即两段 promo 起始周至少相差 d + s)
m = GEKKO(remote=False)
m.options.SOLVER = 1
m.options.IMODE = 3
m.options.NODES = 3
# 决策变量:st[i] = 1 表示在 week[i] 启动一个新 promo 周期
st = m.Array(m.Var, n, integer=True, lb=0, ub=1)
# 【约束1】起始位置必须有足够后续周以满足持续时长 d
for i in range(n):
# 检查从 week[i] 开始,是否存在 d 个连续周(即 week[i], week[i+1], ..., week[i+d-1] 都存在且等差为1)
if i + d - 1 >= n:
m.Equation(st[i] == 0) # 越界,禁止启动
else:
# 验证是否连续:week[i+k] == week[i] + k 对所有 k=0..d-1?
is_continuous = all(week.iloc[i+k] == week.iloc[i] + k for k in range(d))
if not is_continuous:
m.Equation(st[i] == 0)
# 【约束2】启动间隔约束:任意两个启动点满足 week[j] >= week[i] + d + s
for i in range(n):
for j in range(i+1, n):
if week.iloc[j] < week.iloc[i] + d + s:
# 若 j 太近,则 st[i] 和 st[j] 不能同时为 1
m.Equation(st[i] + st[j] <= 1)
# 【目标】最大化启动次数(或可替换为其他业务目标,如总销量)
m.Maximize(m.sum(st))
m.solve(disp=True)
# 后处理:生成最终 x7(执行周标记)
x7_sol = np.zeros(n, dtype=int)
for i in range(n):
if st[i].VALUE[0] > 0.5:
# 在 week[i] 启动 → 占用接下来 d 周(索引 i 到 i+d-1)
for k in range(d):
if i + k < n:
x7_sol[i + k] = 1
# 输出结果
result_df = pd.DataFrame({
'week': week,
'start_promo': [int(st[i].VALUE[0]) for i in range(n)],
'in_promo': x7_sol
})
print("Optimal schedule (d=2, s=1):")
print(result_df[result_df['in_promo'] == 1][['week', 'start_promo', 'in_promo']])