1.前言

如前文所说,约束规划(CP)指求解满足各项约束的可行解的问题。与线性规划、整数规划不同,约束规划更加关注可行解,没有明确的优化目标。典型的场景包括员工排班问题、N皇后问题。CP问题虽然没有目标函数,但可以通过目标添加到约束的方式缩小到更易于管理的子集,变相解决整数规划问题。

ortools提供了 CP-SAT 求解器,其使用方法与MPSolver类似。接下来,我们看看CP-SAT是如何解决CP问题以及MIP问题的。

2.求解CP问题

问题如下:

有变量x, y,z,取值范围均为为 0, 1, 2,
约束条件: x ≠ y,
求满足条件的x,y,z组合。

代码及讲解如下,这里采用硬编码方式。

#引入cp_model,便于后续构建CP-SAT求解器对应模型
from ortools.sat.python import cp_model


#回调类,每得到一个结果均执行on_solution_callback函数
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


def SearchForAllSolutionsSampleSat():
    """Showcases calling the solver to search for all solutions."""
    # 创建模型
    model = cp_model.CpModel()

    # 创建变量
    num_vals = 3
    x = model.NewIntVar(0, num_vals - 1, 'x')
    y = model.NewIntVar(0, num_vals - 1, 'y')
    z = model.NewIntVar(0, num_vals - 1, 'z')

    # 创建约束.
    model.Add(x != y)

    # 创建求解器并求解.
    solver = cp_model.CpSolver()
    # 定义回调对象
    solution_printer = VarArraySolutionPrinter([x, y, z])
    # 修改求解器参数:枚举所有结果
    solver.parameters.enumerate_all_solutions = True
    # 求解过程
    status = solver.Solve(model, solution_printer)

    print('Status = %s' % solver.StatusName(status))
    print('Number of solutions found: %i' % solution_printer.solution_count())

SearchForAllSolutionsSampleSat()

3.求解MIP问题

问题如下:

最大化 2x + 2y + 3z ,同时满足以下约束:
x + 7⁄2 y + 3⁄2 z ≤ 25
3x - 5y + 7z ≤ 45
5x + 2y - 6z ≤ 37
x, y, z ≥ 0
x, y, z 为整数

代码及讲解如下。需要注意的是:为了提高求解速度,CP-SAT求解器要求所有约束的元素都为整数。实际应用中遇到浮点数时需要对约束条件进行转换,例如,不等式两边分别乘以一个较大的数。

from ortools.sat.python import cp_model


def main():
    model = cp_model.CpModel()

    var_upper_bound = max(50, 45, 37)
    x = model.NewIntVar(0, var_upper_bound, 'x')
    y = model.NewIntVar(0, var_upper_bound, 'y')
    z = model.NewIntVar(0, var_upper_bound, 'z')

    # Creates the constraints.
    model.Add(2 * x + 7 * y + 3 * z <= 50)
    model.Add(3 * x - 5 * y + 7 * z <= 45)
    model.Add(5 * x + 2 * y - 6 * z <= 37)

    model.Maximize(2 * x + 2 * y + 3 * z)

    # Creates a solver and solves the model.
    solver = cp_model.CpSolver()
    status = solver.Solve(model)

    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        print(f'Maximum of objective function: {solver.ObjectiveValue()}\n')
        print(f'x = {solver.Value(x)}')
        print(f'y = {solver.Value(y)}')
        print(f'z = {solver.Value(z)}')
    else:
        print('No solution found.')

    # Statistics.
    print('\nStatistics')
    print(f'  status   : {solver.StatusName(status)}')
    print(f'  conflicts: {solver.NumConflicts()}')
    print(f'  branches : {solver.NumBranches()}')
    print(f'  wall time: {solver.WallTime()} s')


if __name__ == '__main__':
    main()

使用CP-SAT可以解决MIP问题,前文提到使用MPSolver及整数规划求解器同样可以解决MIP问题,另外,后续我们还会提到使用网络流解决MIP问题。使用过程中如何做选型呢?

  • MPSolver:求解问题比较偏向于标准的线性规划问题,部分变量有整数约束
  • CP-SAT:适合变量为0-1取值的情况
  • 网络流方法:问题可以转化为网络关系,进而利用网络关系降低问题求解难度

三种方法有所侧重,但选型上并不绝对。很多问题也都是可以从不同的角度转化为不同类型的问题,进而使用不同的求解器进行求解的。

4.CP-SAT关键要素

建模过程中需要将数学模型转化为代码,其中最重要的是变量和约束的转化。接下来,我们看一看CP-SAT都提供哪些变量、约束函数和目标函数。了解了提供的功能,编程就变成了“搭积木”。这部分内容主要位于CpModel(object)类中。

变量

  • NewIntVar(self, lb, ub, name):Create an integer variable with domain [lb, ub]
  • NewIntVarFromDomain(self, domain, name):变量取值范围在指定的(未必连续的)域中

    Create an integer variable from a domain.

    A domain is a set of integers specified by a collection of intervals. For example, model.NewIntVarFromDomain(cp_model.Domain.FromIntervals([[1, 2], [4, 6]]), 'x')

  • NewBoolVar(self, name):Creates a 0-1 variable with the given name.
  • NewConstant(self, value):Declares a constant integer
  • NewIntervalVar(self, start, size, end, name):Creates an interval variable from start, size, and end.区间变量,可以表示时间区间,在VRP算法中应该有所使用。

    start、size、 end均可以是线性表达式或常量,但方法内部添加了start + size == end的约束
  • NewFixedSizeIntervalVar(self, start, size, name):区间变量

    start可以是线性表达式或常量,size必须为常量
  • NewOptionalIntervalVar(self, start, size, end, is_present, name):Creates an optional interval var from start, size, end, and is_present.

    is_present: A literal that indicates if the interval is active or not. A inactive interval is simply ignored by all constraints. NewIntervalVar和NewOptionalIntervalVar的不同之处在于,是前者表示创建的区间变量在以后的约束建立中一定生效,而后者的方法签名中有个为is_present的参数表示这个区间变量是否生效。
  • NewOptionalFixedSizeIntervalVar(self, start, size, is_present, name):Creates an interval variable from start, and a fixed size.

约束

  • AddLinearConstraint(self, linear_expr, lb, ub):Adds the constraint: lb <= linear_expr <= ub.
  • AddLinearExpressionInDomain(self, linear_expr, domain):Adds the constraint: linear_expr in domain.
  • Add(self, ct):Adds a BoundedLinearExpression to the model.

    示例:
    model.Add(5 x + 2 y - 6 * z <= 37)
  • AddAllDifferent(self, *expressions):This constraint forces all expressions to have different values.

    Adds AllDifferent(expressions).
    This constraint forces all expressions to have different values.
    Args:
    expressions: simple expressions of the form a var + constant.
    Returns:
    An instance of the Constraint class.
    示例:
    queens = [model.NewIntVar(0, board_size - 1, 'x%i' % i) for i in range(board_size)
    ]
    model.AddAllDifferent(queens)
  • AddElement(self, index, variables, target):等值约束

    Adds the element constraint: variables[index] == target.
  • AddCircuit(self, arcs):arcs组成的路径集合构成哈密顿路径,TSP约束.
  • AddMultipleCircuit(self, arcs):Adds a multiple circuit constraint, aka the "VRP" constraint.形成的多条链路,需要保证形成的各链路内arc首位连接。推测ortools的routing模块使用了AddCircuit、AddMultipleCircuit两种方法。
  • AddAllowedAssignments(self, variables, tuples_list): 固定匹配约束,variables的取值,属于tuples_list中的一种

    An AllowedAssignments constraint is a constraint on an array of variables,
    which requires that when all variables are assigned values, the resulting
    array equals one of the tuples in tuple_list.
  • AddForbiddenAssignments(self, variables, tuples_list):禁止约束

    A ForbiddenAssignments constraint is a constraint on an array of variables
    where the list of impossible combinations is provided in the tuples list.
  • AddAutomaton(self, transition_variables, starting_state, final_states, transition_triples): 状态转移约束 (状态之间存在转移关系)

    transition_variables 代表了需要求解的变量,starting_state为起始状态,final\_states为可接受的最终状态,transition_triples为转移关系
  • AddInverse(self, variables, inverse_variables):关联约束

    An inverse constraint enforces that if variables[i] is assigned a value j, then inverse_variables[j] is assigned a value i. And vice versa.
  • AddReservoirConstraint(self, times, level_changes, min_level,max_level):储水池约束

    sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
  • AddReservoirConstraintWithActive(self, times, level_changes, actives, min_level, max_level):时间开关的储水池约束,actives表示是否动作是否生效

    sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, max_level]
  • AddMapDomain(self, var, bool_var_array, offset=0):Adds var == i + offset <=> bool_var_array[i] == true for all i.
  • AddImplication(self, a, b):Adds a => b (a implies b).单向约束,如果a,则b
  • AddBoolOr(self, *literals):Adds Or(literals) == true: Sum(literals) >= 1.
  • AddAtLeastOne(self, *literals):Same as AddBoolOr: Sum(literals) >= 1.
  • AddAtMostOne(self, *literals):Adds AtMostOne(literals): Sum(literals) <= 1.
  • AddExactlyOne(self, *literals):Adds ExactlyOne(literals): Sum(literals) == 1.
  • AddBoolAnd(self, *literals):Adds And(literals) == true.
  • AddBoolXOr(self, *literals):Adds XOr(literals) == true.异或运算
  • AddMinEquality(self, target, exprs):Adds target == Min(exprs).
  • AddMaxEquality(self, target, exprs):Adds target == Max(exprs).
  • AddDivisionEquality(self, target, num, denom):Adds target == num // denom (integer division rounded towards 0).取整操作,向0舍入。
  • AddAbsEquality(self, target, expr):Adds target == Abs(var).
  • AddModuloEquality(self, target, var, mod):Adds target = var % mod. 取余操作
  • AddMultiplicationEquality(self, target, *expressions):Adds target == expressions[0] * .. * expressions[n].
  • AddNoOverlap(self, interval_vars):区间不重叠约束.例如,区间变量表示时间间隔时,AddNoOverlap会强制所有的时间间隔变量不发生重叠,不过它们可以使用相同的开始/结束的时间点。在VRP算法中会进行使用。
  • AddNoOverlap2D(self, x_intervals, y_intervals):所有矩形不重叠约束,x_intervals、 y_intervals分别存储了不同矩形的x、y坐标
  • AddCumulative(self, intervals, demands, capacity):需求量小于能力上限的约束,VRP中会使用。

    for all t:
    sum(demands[i] if (start(intervals[i]) <= t < end(intervals[i])) and (intervals[i] is present)) <= capacity

以上方法返回结果是一个Constraint(object)的对象。Constraint(object)包含的关键方法如下:

  • OnlyEnforceIf(self, *boolvar):如果boolvar为true,则增加约束。

    例如,model.Add(d == 1).OnlyEnforceIf(a),表示如果a为true,则d必须为1。与AddImplication(self, a, b)有方法等价。
    再比如,
    b = model.NewBoolVar('b')
    x = model.NewIntVar(0, 10, 'x')
    y = model.NewIntVar(0, 10, 'y')
    model.Add(x + 2 * y == 5).OnlyEnforceIf(b.Not())
  • Index(self):Returns the index of the constraint in the model.
  • Proto(self):Returns the constraint protobuf.

其他

  • AddHint(self, var, value):Adds 'var == value' as a hint to the solver. 给变量添加初始值。

目标

  • Minimize(self, obj):最小化
  • Maximize(self, obj):最大化

5.结语

本篇文章主要讲解了ortools使用CP-SAT求解器解决CP、MIP问题的方法,并详细解读了CP可以使用的变量、约束函数、目标函数等信息。


喜东东
17 声望28 粉丝

不积跬步无以至千里.