在我的优化问题中,单位成本随距离(而不是距离桶)而变化。举个例子:
from to unit_cost total_cost_from total_cost_to
[[0, 174, 25, 0, 4350],
[174, 281, 27, 4350, 7239],
[281, 398, 29, 7239, 10632],
[398, 533, 31, 10632, 14817],
[533, 636, 32, 14817, 18113]]
因此,对于 174 到 281 单位距离,单位成本 = 27
因此,如果在最优解中,距离取值为 180,则
total cost = 174*25 + (180-174)*27 = 4512
我知道许多商业求解器已将分段功能合并到其 API 中,但我如何通过开源求解器实现相同的功能
是的,我们可以在开源求解器中制定分段线性函数。我们将引入额外的变量,一个变量对应函数中的每个断点。假设我们有
n
括号,从概念上讲,我们将这些变量视为括号边界上的权重,告诉我们在括号中的位置。我们希望最多有两个连续变量非零,且它们的总和为一。
这将告诉我们 x 位于何处,从而告诉我们目标函数值是多少。
以下是 Google-ortool 线性求解器中的公式。
from ortools.linear_solver import pywraplp
data = [
[0, 174, 25, 0, 4350],
[174, 281, 27, 4350, 7239],
[281, 398, 29, 7239, 10632],
[398, 533, 31, 10632, 14817],
[533, 636, 32, 14817, 18113],
]
s = pywraplp.Solver("", pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
n = len(data)
# this is the final quantity selected
x = s.NumVar(data[0][0], data[n - 1][0], "x")
# introduce additional variables, one for each break-point in the function
# weights on the bracket boundaries, telling us where we are in the bracket
w = [s.NumVar(0, 1, "w[%i]" % (i,)) for i in range(n)]
# z is the set of binary variables whose only purpose is to
# enforce the SOS2 conditions over w's
z = [s.BoolVar("") for i in range(n)]
# We want the sum of the w's to be one
s.Add(1 == sum(w[i] for i in range(n)))
# calculation of x weighted sum of across brackets
# if say, w2 = 0.25 and w3 = 0.75, then we know that we are in the third bracket,
# x = w2 * point2 + w3 * point3
s.Add(x == sum(w[i] * data[i][0] for i in range(n)))
# put a constraint to determine the value of x
# since we will minimise cost (later) x will be forced to 180
# in optimal solution
# challenge is to identify the correct pair of brackets where 145 will lie
s.Add(x >= 180)
# Below constraints enforce SOS2 condition
# SOS2 : at most two consecutive variables to be non-zero
# There exists an order, in other words the variables can be sorted along one of their attributes
# They are all positive and continuous
# At most 2 of them are non-zero
# The two variables have to be adjacent
# above conditions are translated into code below
for i in range(n):
s.Add(w[i] <= z[i])
s.Add(sum(z[i] for i in range(n)) <= 2)
# only adjacent z's can attain a value of 1
z_sum = {}
for i in range(n - 2):
z_sum[i] = s.IntVar(0, 2, "")
s.Add(z_sum[i] == z[i] + z[i + 1])
z_sum_bool = {i: s.BoolVar("") for i in range(n - 2)}
for i in range(n - 2):
s.Add(z_sum[i] >= -2 * (1 - z_sum_bool[i]) + 2)
s.Add(sum(z_sum_bool[i] for i in range(n - 2)) == 1)
# minimize cost
cost = s.Sum(w[i] * data[i][3] for i in range(n))
s.Minimize(cost)
s.Solve()
R = [w[i].SolutionValue() for i in range(n)]
w[I]
结果为 [0.0, 0.94392523364486, 0.05607476635514003, 0.0, 0.0]
这意味着 180 距离位于
second to third
括号内或 174 - 281
之间。
s.Objective().Value()
= 4512
只不过是174*25 + (180-174)*27
PuLP
中,我们可以为每个间隔创建一个变量并相应地设置它们的界限:
数据:
breakpoints = [0, 174, 281, 398, 533, 636]
slopes = [25, 27, 29, 31, 32]
interval_lengths = [breakpoints[i+1] - breakpoints[i] for i in range(len(breakpoints)-1)]
型号:
from pulp import *
prob = LpProblem("PiecewiseLinearVariables", LpMinimize)
x=LpVariable('x', lowBound=180,upBound=180, cat='Continuous')
variables = {}
# Create a variable for each interval, set its bounds to the respective interval length
for i in range(0,len(interval_lengths)):
segment = f"Segment_{i+1}"
variables[segment] = LpVariable(segment, lowBound=0,upBound=interval_lengths[i], cat='Continuous')
#Force the segments to take on values
prob += x==lpSum(var for var in variables.values())
#Add the weighted cost to the objective
prob + = lpSum(slope * var for var, slope in zip(variables.values(), slopes))
prob.solve()
解决方案:
for var in variables.values():
print(f"{var.name}: {var.varValue}")
print(f"x: {value(x)}")
输出:
Optimal - objective value 4512 # From model.solve() output
Segment_1: 174.0
Segment_2: 6.0
Segment_3: 0.0
Segment_4: 0.0
Segment_5: 0.0
x: 180.0
不需要 SOS 或二进制变量,这通常会产生更简单的模型。
对于许多类别的问题(您尚未指定),不需要二进制赋值变量 - @SebastianWozny 已经认识到这一点,但理解为什么很重要。
您的增量成本单调增加。对于成本最小化问题,本质上它总是先填充下层桶,然后再填充上层桶。如果您的增量成本发生变化,使其不再单调,那么这将不起作用。同样,如果出于某种原因您想要最大化预算支出,这也是行不通的。
简化列的总成本和列的总成本。另外,还不清楚为什么应该有一个最终的最大距离:最后一行的增量价格不应该适用于它上面的所有距离吗?
import numpy as np
import pandas as pd
import pulp
df = pd.DataFrame(
[
[ 0, 25],
[174, 27],
[281, 29],
[398, 31],
[533, 32],
],
columns=['from', 'unit_cost'],
)
def make_distance(row: pd.Series) -> pulp.LpVariable:
return pulp.LpVariable(
name=f'distance_{row.name}',
lowBound=0,
upBound=row.dist_range if np.isfinite(row.dist_range) else None,
)
df['dist_range'] = df['from'].diff().shift(-1, fill_value=np.inf)
df['distance'] = df.apply(make_distance, axis=1)
distance = df.distance.sum()
cost = df.distance.dot(df.unit_cost)
prob = pulp.LpProblem(name='fixed_budget_max_distance', sense=pulp.LpMaximize)
prob.objective = distance + 0
prob.addConstraint(name='cost', constraint=cost <= 10e3)
print(prob)
prob.solve()
assert prob.status == pulp.LpStatusOptimal
print('Can travel up to', distance.value(), 'at cost', cost.value())
print('Distance buckets:')
print(df.distance.apply(pulp.LpVariable.value))
fixed_budget_max_distance:
MAXIMIZE
1*distance_0 + 1*distance_1 + 1*distance_2 + 1*distance_3 + 1*distance_4 + 0
SUBJECT TO
cost: 25 distance_0 + 27 distance_1 + 29 distance_2 + 31 distance_3
+ 32 distance_4 <= 10000
VARIABLES
distance_0 <= 174 Continuous
distance_1 <= 107 Continuous
distance_2 <= 117 Continuous
distance_3 <= 135 Continuous
distance_4 Continuous
Optimal - objective value 376.2069
Optimal objective 376.2068966 - 3 iterations time 0.002
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
Can travel up to 376.206897 at cost 10000.000013
Distance buckets:
0 174.000000
1 107.000000
2 95.206897
3 0.000000
4 0.000000