最近筆者一直在從事整數規划方面的排程編程,主要使用的工具是Google的OR-tools,因此筆者在互聯網上收集了很多有意思的技巧與知識想分享一下。首先這是OR-Tools的官網,里面有許許多多的例子,感興趣的朋友可以自己去學習一下,筆者這里不再贅述了。
下面筆者先對Or-Tools的建模語言cp_model及其求解器CP-SAT Solver的使用技巧做一下簡單介紹,隨后對一些復雜的建模語法通過幾個例子對大家詳細講解,以下素材均來自於互聯網。這里的部分介紹引用自-派神-大大的博客線性規划之Google OR-Tools 簡介與實戰,筆者也會對部分約束進行解釋。
CP-SAT 簡介
在or-tools中內置了一些第三方的開源求解器(SCIP, GLPK) 及商用求解器( Gurobi , CPLEX),同時還有google自己的求解器Google's GLOP以及獲得過多項金獎的CP-SAT。
下面筆者將對CP-SAT中使用頻率較高的建模語法和使用技巧進行介紹,cp_model是一種很奇妙的建模語法,它主要使用整型變量對優化問題進行建模。
Or-tools 支持C#、python、java、c++建模。其實筆者都是使用C#作為建模語言的,但是此文為了普適性所以用python作為此文的建模與語言
求解器變量
非連續整形變量(Intvar)
# List of values
model.NewIntVarFromDomain(
cp_model.Domain.FromValues([1, 3, 4, 6]), 'x'
)
# List of intervals
model.NewIntVarFromDomain(
cp_model.Domain.FromIntervals([[1, 2], [4, 6]]), 'x'
)
# Exclude [-1, 1]
model.NewIntVarFromDomain(
cp_model.Domain.FromIntervals([[cp_model.INT_MIN, -2], [2, cp_model.INT_MAX]]), 'x'
)
# Constant
model.NewConstant(154)
model.NewIntVarFromDomain
從域區間創建整數變量。
域區間是由區間集合所指定的一組整數。 例如:
model.NewIntVarFromDomain(cp_model.Domain.FromIntervals([[1, 2], [4, 6]]), 'x')
Constant
Constant表示一個特殊的IntVal,其上界等於其下界所以其IntVal所表示的值也是一個常量。
BoolVar
表示一個特殊的IntVal,其下界為0,上界為1。所以其變量的值只能為 0 或者 1 。
IntervalVar
CP-SAT有一種特殊的區間變量,它可以用來表示時間間隔。通常配合約束AddNoOverLap與AddCumulative使用表示區間不能重疊或者區間累計。有兩種創建此變量的方式:
model.NewIntervalVar(self, start, size, end, name)model.NewOptionalIntervalVar(self, start, size, end, is_present, name)
相同的是都需要start、size和end參數表示區間的開始的整型變量、尺寸或者時間的長度和區間的結束的整型變量。
不同的是前者表示創建的區間變量在以后的約束建立中一定生效,而后者的方法簽名中有個為is_present的參數表示這個區間變量是否生效。
求解器約束
Implications(離散數學里的蘊涵)
Implications方法是一種單向的約束操作:a = b (a,b均為布爾型變量) ,a為True則b也為True,反之則不成立。
其真值表為:
| p | q | p-q |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 1 | 1 |
| 0 | 0 | 1 |
| 1 | 0 | 0 |
# a = b (both booleans)
model.AddImplication(a, b)
# a <= b (better remove one of them)
model.Add(a == b)
# a or b or c = d
model.AddImplication(a, d) # model.Add(d == 1).OnlyEnforceIf(a)
model.AddImplication(b, d)
model.AddImplication(c, d)
# a and b and c = d
model.Add(d == 1).OnlyEnforceIf([a, b, c])
or
model.AddBoolOr([a.Not(), b.Not(), c.Not(), d])
Linear Constraints(線性約束)
常用的線性約束有Add,AddLinearConstraint,AddLinearExpressionInDomain等幾種:
#布爾數組work的和<=6
model.Add(sum(work[(i)] for i in range(10))<=6)
#布爾數組work的和=2 and <=6
model.AddLinearConstraint(sum(work[(i)] for i in range(10)), 2, 6)
#布爾數組work的和 in [0,2,3,5]
model.AddLinearExpressionInDomain(sum(work[(i)] for i in range(10)) ,[0,2,3,5])
Nonlinear Constraints(非線性約束)
常用的幾種非線性約束如:絕對值約束、乘法約束、最大最小值約束
# Adds constraint: target == |var|
model.AddAbsEquality(target, var)
#Adds constraint: target == v1 * v2
model.AddMultiplicationEquality(target, [v1,v2])
#Adds constraint: target == Max(var_arr)
model.AddMaxEquality(target, var_arr)
#Adds constraint: target == Min(var_arr)
model.AddMinEquality(target, var_arr)
遺憾的是:沒有一步到位的非線性表達式; 必須建立復雜的使用中間變量逐段地生成數學表達式。
The AllDifferent Constraints(強制所有變量都不相同約束)
#Forces all vars in the array to take on different values
model.AddAddAllDifferent(var_arr)
The Element Constraint(元素約束)
# Adds constraint: target == var_arr[index]
# Useful because index can be a variable
# The var_arr can also contain constants!
model.AddElement(index, var_arr, target)
The Inverse Constraint(逆約束)
# The arrays should have the same size 𝑛 (can’t use dicts)
# The vars in both arrays can only take values from 0 to 𝑛 − 1
# Adds the following constraints:
# If var_arr[i] == j, then inv_arr[j] == i
# If inv_arr[j] == i, then var_arr[i] == j
# Intuition: sets up a “perfect matching” between the two sets of variables
model.AddInverse(var_arr, inv_arr)
使不同的兩個列表中的所有元素
NoOverlap Constraint (不重疊約束)
當筆者們創建了一組區間變量以后,有時候筆者們希望區間變量之間的時間間隔不能發生重疊,這時筆者們可以使用AddNoOverlap約束。AddNoOverlap會強制所有的時間間隔變量不發生重疊,不過它們可以使用相同的開始/結束的時間點。
# Note: there is no way to access start, end, duration of an interval by default
# Recommended: directly add them as fields of the interval, e.g.
# interval.start = start
model.AddNoOverlap(interval_arr)
# Powerful constraint: enforces that all intervals in the array do not overlap with each other!
# It’s OK to have shared start/endpoints
AllowedAssignments
Adds AllowedAssignments(variables, tuples_list).
AllowedAssignments 約束是對變量數組的約束,它要求當所有變量都被賦值時,其結果的數組內容等於 tuple_list 中的某一個元組的內容。
搜索最優解
單個最優化目標
使用model.Maximize()或者model.Minimize()設置模型的最優化目標。再通過 CpSolver 對象下的 solve(model) 方法求解模型即可開始求解,求解完成后方法會返回此次求解的狀態:
| Status | Description |
|---|---|
| OPTIMAL | An optimal feasible solution was found. |
| FEASIBLE | A feasible solution was found, but we don't know if it's optimal. |
| INFEASIBLE | The problem was proven infeasible. |
| MODEL_INVALID | The given CpModelProto didn't pass the validation step. You can get a detailed error by calling ValidateCpModel(model_proto). |
| UNKNOWN | The status of the model is unknown because no solution was found (or the problem was not proven INFEASIBLE) before something caused the solver to stop, such as a time limit, a memory limit, or a custom limit set by the user. |
求解完成后可以通過solver.ObjectiveValue()取得優化目標, 以及使用 solve.Value(IntVar) 方法取得模型中求解器變量的值。
求解所有可行解
求解所有可行解需要用到def SolveWithSolutionCallback(self, model, callback)方法。而且需要用到一個額外的回調處理每一個解決方案中的解。具體參考or-tools文檔
class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""
def __init__(self, variables):
cp_model.CpSolverSolutionCallback.__init__(self)
self.__variables = variables
self.__solution_count = 0
def on_solution_callback(self):
self.__solution_count += 1
for v in self.__variables:
print('%s=%i' % (v, self.Value(v)), end=' ')
print()
def solution_count(self):
return self.__solution_count
小技巧1 如何搜索所有的最優解
如果你在求解一個優化問題的時候設置了一個目標如:model.Maximize或者model.Minimize。因為最優化目標設置后只會得出一個解,所以你將不能使用SearchForAllSolutions方法來搜索跟最優解優化目標相同大小的解。但用一個小技巧,可以使用以下兩個步驟來搜索所有優化目標達到最優的解:
# Get the optimal objective value
model.Maximize(objective)
solver.Solve(model)
# Set the objective to a fixed value
# use round() instead of int()
model.Add(objective == round(solver.ObjectiveValue()))
model.Proto().ClearField('objective')
# Search for all solutions
solver.SearchForAllSolutions(model, cp_model.VarArraySolutionPrinter([x, y, z]))
小技巧2 如何設置前后多個最優化目標
用求解目標得出解決方案后,再使用之前的求解方案的最優化目標的值作為下一個最優化目標的一個約束,並對模型AddHint提示上個解決方案的值再用求解目標求解。
from ortools.sat.python import cp_model
model = cp_model.CpModel()
solver = cp_model.CpSolver()
x = model.NewIntVar(0, 10, "x")
y = model.NewIntVar(0, 10, "y")
# Maximize x
model.Maximize(x)
solver.Solve(model)
print("x", solver.Value(x))
print("y", solver.Value(y))
print()
# Hint (speed up solving)
model.AddHint(x, solver.Value(x))
model.AddHint(y, solver.Value(y))
# Maximize y (and constraint prev objective)
model.Add(x == round(solver.ObjectiveValue())) # use <= or >= if not optimal
model.Maximize(y)
solver.Solve(model)
print("x", solver.Value(x))
