OR-Tools CP-SAT 操作指南

最近笔者一直在从事整数规划方面的排程编程,主要使用的工具是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有一种特殊的区间变量,它可以用来表示时间间隔。通常配合约束AddNoOverLapAddCumulative使用表示区间不能重叠或者区间累计。有两种创建此变量的方式:

  • model.NewIntervalVar(self, start, size, end, name)
  • model.NewOptionalIntervalVar(self, start, size, end, is_present, name)

相同的是都需要startsizeend参数表示区间的开始的整型变量、尺寸或者时间的长度和区间的结束的整型变量。

不同的是前者表示创建的区间变量在以后的约束建立中一定生效,而后者的方法签名中有个为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))
posted @ 2021-11-14 14:05  zhangbig0  阅读(5115)  评论(0编辑  收藏  举报