1.前言

前文我们提到,CP-SAT不仅可以解决约束规划问题,还可以解决整数规划、混合整数规划等目标优化问题,并在前文中用车间调度案例做了说明。

本文主要对比两种不同的求解器的差异,我们从一个案例说起。

2.分配问题的两种解法

问题

假设一家出租车公司有四个客户在等车,四个出租车司机(工人)可以接他们,任务是给每个客户分配一名司机去接他们。任务的成本是司机接一个特定客户所花费的时间。问题是,如何将司机分配给客户,以最小化总等待时间。

如下图所示,左边的节点表示worker(司机)。右边的节点表示任务(客户),这些边表示将工作人员分配给任务的所有可能方法。

image.png

用表格形式表示如下:

image.png

MIP问题求解

底层采用SCIP求解器

from ortools.linear_solver import pywraplp

def main():
    # 数据,存储不同work-task匹配时的cost
    costs = [
        [90, 80, 75, 70],
        [35, 85, 55, 65],
        [125, 95, 90, 95],
        [45, 110, 95, 115],
        [50, 100, 90, 100],
    ]
    num_workers = len(costs)
    num_tasks = len(costs[0])

    # SCIP求解器.
    solver = pywraplp.Solver.CreateSolver('SCIP')


    # 决策变量
    # x[i, j] 存储0-1值, 如果将worker i 和 task j进行匹配,则设置为1.
    x = {}
    for i in range(num_workers):
        for j in range(num_tasks):
            x[i, j] = solver.IntVar(0, 1, '')

    # 约束1:一个worker 最多安排一个task.
    for i in range(num_workers):
        #solver自身提供sum等针对var的数值操作
        solver.Add(solver.Sum([x[i, j] for j in range(num_tasks)]) <= 1)

    # 约束2:一个task只安排给1个worker.
    for j in range(num_tasks):
        solver.Add(solver.Sum([x[i, j] for i in range(num_workers)]) == 1)

    # 目标函数
    objective_terms = []
    for i in range(num_workers):
        for j in range(num_tasks):
            objective_terms.append(costs[i][j] * x[i, j])
    solver.Minimize(solver.Sum(objective_terms))

    # Solve
    status = solver.Solve()

    # Print solution.
    if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE:
        print(f'Total cost = {solver.Objective().Value()}\n')
        for i in range(num_workers):
            for j in range(num_tasks):
                # Test if x[i,j] is 1 (with tolerance for floating point arithmetic).
                if x[i, j].solution_value() > 0.5:
                    print(f'Worker {i} assigned to task {j}.' +
                          f' Cost: {costs[i][j]}')
    else:
        print('No solution found.')


if __name__ == '__main__':
    main()

CP-SAT求解

from ortools.sat.python import cp_model


def main():
    # 数据,存储不同work-task匹配时的cost
    costs = [
        [90, 80, 75, 70],
        [35, 85, 55, 65],
        [125, 95, 90, 95],
        [45, 110, 95, 115],
        [50, 100, 90, 100],
    ]
    num_workers = len(costs)
    num_tasks = len(costs[0])

    # CP-SAT
    model = cp_model.CpModel()

    # 变量
    x = []
    for i in range(num_workers):
        t = []
        for j in range(num_tasks):
            t.append(model.NewBoolVar(f'x[{i},{j}]'))
        x.append(t)

    # Constraints
    # Each worker is assigned to at most one task.
    for i in range(num_workers):
        model.AddAtMostOne(x[i][j] for j in range(num_tasks))

    # Each task is assigned to exactly one worker.
    for j in range(num_tasks):
        model.AddExactlyOne(x[i][j] for i in range(num_workers))

    # Objective
    objective_terms = []
    for i in range(num_workers):
        for j in range(num_tasks):
            objective_terms.append(costs[i][j] * x[i][j])
    model.Minimize(sum(objective_terms))

    # Solve
    solver = cp_model.CpSolver()
    status = solver.Solve(model)

    # Print solution.
    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        print(f'Total cost = {solver.ObjectiveValue()}')
        print()
        for i in range(num_workers):
            for j in range(num_tasks):
                if solver.BooleanValue(x[i][j]):
                    print(
                        f'Worker {i} assigned to task {j} Cost = {costs[i][j]}')
    else:
        print('No solution found.')


if __name__ == '__main__':
    main()

3.对比

适用性

理论上,

  • MIP方法更适合线性规划问题的变种,例如仅仅是增加了整数约束;
  • CP-SAT更适合多数变量为布尔型时的情况

代码结构

从代码中可以看到,
CP-SAT提供两个类

  • CpModel类,大量CP-SAT的和信操作,例如变量、约束都在该类中

Methods for building a CP model.
Methods beginning with:

  • New create integer, boolean, or interval variables.
  • Add create new constraints and add them to the model.
  • CpSolver类,求解器主类,搜索solution、生成求解过程的统计数据等
The purpose of this class is to search for a solution to the model provided
to the Solve() method.
Once Solve() is called, this class allows inspecting the solution found
with the Value() and BooleanValue() methods, as well as general statistics
about the solve procedure.

linear_solver只提供一个Solver类,所有核心要素都在该类内。


喜东东
17 声望28 粉丝

不积跬步无以至千里.