当前位置:

首页 > OR-Tools CP-SAT优化:大规模分配性能提升与浮点缩放技巧

OR-Tools CP-SAT优化:大规模分配性能提升与浮点缩放技巧

本文旨在解决使用OR-Toolslinear_solver处理大规模分配问题时遇到的性能瓶颈。针对N值超过40-50的工人-任务分配问题,linear_solver的求解时间显著增加。通过分析问题特性,我们推荐切换至CP-SAT求解器。CP-SAT专为整数规划设计,能显著提升求解速度,并能有效处理浮点系数,通过内部缩放机制将其转换为整数进行优化,从而在保持模型精度的同时,实现更高效的计算。

优化OR-Tools解决大规模分配问题:CP-SAT的性能优势与浮点数缩放

本文旨在解决使用OR-Tools `linear_solver`处理大规模分配问题时遇到的性能瓶颈。针对N值超过40-50的工人-任务分配问题,`linear_solver`的求解时间显著增加。通过分析问题特性,我们推荐切换至`CP-SAT`求解器。`CP-SAT`专为整数规划设计,能显著提升求解速度,并能有效处理浮点系数,通过内部缩放机制将其转换为整数进行优化,从而在保持模型精度的同时,实现更高效的计算。

在复杂的资源分配场景中,例如将工人分配给任务,并要求最小化最高成本与最低成本之间的差异,同时满足多种基于工人ID的约束条件(如特定任务只能由特定ID的工人完成、某些任务组必须由相同ID的工人完成、或某些任务组的工人ID值之和受限),精确建模至关重要。OR-Tools库提供了强大的线性规划和混合整数规划(MIP)求解器,但当问题规模(N值,例如工人/任务数量)增大时,通用MIP求解器(如linear_solver结合SCIP后端)的求解时间可能会迅速变得不切实际。

原始问题与性能瓶颈

原始代码使用ortools.linear_solver.pywraplp模块,并选择SCIP作为后端求解器。该模型成功地构建了分配变量x[i, j]、任务ID变量tasks_ids[j],并设置了以下约束:

  1. 每个工人分配一个任务。
  2. 每个任务分配一个工人。
  3. 特定任务的工人ID限制。
  4. 多组任务必须由相同ID的工人完成。
  5. 多组任务的工人ID值之和限制。

目标函数设定为最小化所有工人分配成本中的最大值与最小值之间的差异。这通过引入辅助变量max_cost和min_cost来实现。

尽管模型逻辑正确,但当工人/任务数量N超过40时,求解时间开始变得难以接受;N超过50时,甚至在10分钟内都无法得到结果。这表明对于此类规模的问题,当前的求解策略存在效率瓶颈。尝试调整求解器参数(如PRESOLVE_ON)或提供初始解,对于通用MIP求解器而言,可能效果有限,且并非所有参数都直接通过Python API暴露。

推荐解决方案:CP-SAT求解器

鉴于该分配问题本质上是一个纯整数模型(尽管成本和目标函数涉及浮点数,但决策变量x[i, j]是二进制的,任务ID也是整数),ortools.sat.python.cp_model模块中的CP-SAT(Constraint Programming - Satisfiability)求解器是更优的选择。CP-SAT专为组合优化和约束满足问题设计,通常在处理整数变量和复杂逻辑约束方面比通用MIP求解器表现出显著的速度优势。

CP-SAT的一个关键特性是它能有效地处理浮点系数。即使模型中存在浮点数(例如成本),CP-SAT也会在内部尝试对它们进行缩放,将其转换为整数,从而利用其高效的整数算术和布尔推理能力。这使得用户无需手动进行复杂的浮点数到整数的转换,同时保证了求解的效率和精度。

使用CP-SAT重构模型

以下是将原问题模型迁移到CP-SAT求解器的示例代码。我们将重点关注模型构建方式的改变以及浮点数处理的策略。

from ortools.sat.python import cp_model
import numpy as np

# number of workers and tasks
N = 40 # 尝试更大的N值,例如70

# cost table for each worker-task pairs
np.random.seed(0)
costs = np.random.rand(N,N)*100

# 为了CP-SAT更好地处理浮点数,我们可以将成本缩放到整数
# 例如,乘以1000,然后进行四舍五入
# CP-SAT内部也会进行类似操作,但明确转换有时能提供更好的控制或性能
SCALING_FACTOR = 1000
scaled_costs = (costs * SCALING_FACTOR).astype(int)

# workers IDs
workers_id = (np.random.rand(N)*4).astype(np.uint32)
id_2_idsrt_dict = {0: 'A', 1: 'B', 2: 'C', 3: 'D'}
workers_id_str = [id_2_idsrt_dict[val] for val in workers_id]
# print(f"Worker IDs: {workers_id_str}") # 打印信息可以帮助调试

idsrt_2_id_dict = {}
for id_val, idstr in id_2_idsrt_dict.items():
   idsrt_2_id_dict[idstr] = id_val
# print(f"ID string to ID dict: {idsrt_2_id_dict}")

num_workers = len(costs)
num_tasks = len(costs[0])

# Solver
# Create the CP-SAT model
model = cp_model.CpModel()

# Variables
# x[i, j] is an array of 0-1 variables, which will be 1
# if worker i is assigned to task j.
x = {}
for i in range(num_workers):
  for j in range(num_tasks):
     x[i, j] = model.NewBoolVar(f"x_{i}_{j}") # 使用NewBoolVar创建布尔变量

# Variables
# tasks_ids[j] is a list of integers variables contains each task's assigned worker id.
# 这里需要NewIntVar,因为workers_id是整数
tasks_ids = []
for j in range(num_tasks):
   # task_id_j = sum([workers_id[i]*x[i, j] for i in range(num_workers)])
   # CP-SAT不能直接对NewBoolVar求和得到NewIntVar,需要使用model.NewIntVar和model.Add
   task_id_var = model.NewIntVar(0, max(workers_id), f"task_id_{j}")
   model.Add(task_id_var == sum([workers_id[i]*x[i, j] for i in range(num_workers)]))
   tasks_ids.append(task_id_var)

# Constraint
# Each worker is assigned to exactly one task.
for i in range(num_workers):
   model.Add(sum(x[i, j] for j in range(num_tasks)) == 1)

# Constraint
# Each task is assigned to exactly one worker.
for j in range(num_tasks):
   model.Add(sum(x[i, j] for i in range(num_workers)) == 1)

# Constraint
# Task 1 can be assigned only with workers that have the id "A"
model.Add(tasks_ids[1] == idsrt_2_id_dict["A"])

# Constraint
# Tasks 2,4,6 must assigned with workers of the same id
model.Add(tasks_ids[2] == tasks_ids[4])
model.Add(tasks_ids[2] == tasks_ids[6])

# Constraint
# Tasks 10,11,12 must assigned with workers of the same id
model.Add(tasks_ids[10] == tasks_ids[11])
model.Add(tasks_ids[11] == tasks_ids[12])

# Constraint
# Tasks 1,2,3 sum of ids <= 4
model.Add((tasks_ids[1] + tasks_ids[2] + tasks_ids[3]) <= 4)

# Constraint
# Tasks 4,5,6 sum of ids <= 4
model.Add((tasks_ids[4] + tasks_ids[5] + tasks_ids[6]) <= 4)

# Constraint
# Tasks 7,8,9 sum of ids <= 3
model.Add((tasks_ids[7] + tasks_ids[8] + tasks_ids[9]) <= 3)

# Objective
# minimize the difference of assignment higher cost worker and lower cost worker

# list of workers costs for an assignment
assignment_workers_costs_list = []
for i in range(num_workers):
   # 注意这里使用 scaled_costs
   worker_cost_var = model.NewIntVar(0, int(np.max(scaled_costs)), f"worker_cost_{i}")
   model.Add(worker_cost_var == sum([scaled_costs[i][j] * x[i, j] for j in range(num_tasks)]))
   assignment_workers_costs_list.append(worker_cost_var)

# Additional variables for max and min costs
# CP-SAT的Max和Min操作需要一个列表的变量
max_cost_var = model.NewIntVar(0, int(np.max(scaled_costs)), 'max_cost')
min_cost_var = model.NewIntVar(0, int(np.max(scaled_costs)), 'min_cost') # min_cost_limit在CP-SAT中通常设为0或最小可能值

# Constraints to update max and min costs
# 使用model.AddMaxEquality和model.AddMinEquality
model.AddMaxEquality(max_cost_var, assignment_workers_costs_list)
model.AddMinEquality(min_cost_var, assignment_workers_costs_list)

# Minimize the difference between max and min costs
model.Minimize(max_cost_var - min_cost_var)

# Create a solver and solve the model.
solver = cp_model.CpSolver()
# 可以设置一些参数来观察求解过程
# solver.parameters.log_search_progress = True
# solver.parameters.num_workers = 8 # 根据CPU核心数调整

print(f"Solving with CP-SAT {solver.parameters.solver_version}")
status = solver.Solve(model)

# Print solution.
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
   # 目标值需要除以缩放因子
   objective_diff = solver.ObjectiveValue() / SCALING_FACTOR
   print(f"Difference (scaled back)= {objective_diff:.2f}\n")
   for i in range(num_workers):
      for j in range(num_tasks):
         if solver.Value(x[i, j]) > 0.5:
            # 成本也需要除以缩放因子
            original_cost = costs[i][j]
            print(f"Worker {i} ({workers_id_str[i]}) assigned to task {j}." + f" Cost: {original_cost:.2f}")
else:
   print("No solution found.")

CP-SAT模型构建要点:

  1. 导入模块: 使用 from ortools.sat.python import cp_model。
  2. 创建模型: model = cp_model.CpModel()。
  3. 变量声明:
    • 布尔变量(0或1)使用 model.NewBoolVar(name)。
    • 整数变量使用 model.NewIntVar(lower_bound, upper_bound, name)。
    • 注意,tasks_ids 和 assignment_workers_costs_list 中的变量必须是model.NewIntVar创建的变量,而不是直接的Python整数或浮点数。
  4. 添加约束: 所有约束都通过 model.Add(...) 方法添加。
    • 等式约束:model.Add(expr == value)。
    • 不等式约束:model.Add(expr <= value) 或 model.Add(expr >= value)。
    • 对于最大/最小值的辅助变量,CP-SAT提供了 model.AddMaxEquality(target_var, list_of_vars) 和 model.AddMinEquality(target_var, list_of_vars),这比手动添加循环约束更简洁高效。
  5. 目标函数: 使用 model.Minimize(expression) 或 model.Maximize(expression)。表达式必须是CP-SAT变量或其线性组合。
  6. 求解: 创建 solver = cp_model.CpSolver() 实例,然后调用 solver.Solve(model)。
  7. 获取结果: 使用 solver.Value(variable) 获取变量的解。

浮点数处理与缩放

在CP-SAT中,所有变量和约束通常都期望是整数。当原始问题包含浮点成本时,有两种主要处理方式:

  1. CP-SAT内部自动缩放: CP-SAT在内部会尝试检测浮点系数并对其进行缩放以转换为整数。这通常是默认且推荐的方式,因为它能自动处理精度问题。在日志中,可以看到CP-SAT关于缩放的报告。
  2. 手动缩放: 如示例代码所示,可以手动将所有浮点成本乘以一个足够大的整数(SCALING_FACTOR),然后四舍五入转换为整数。这样做可以确保所有输入都是整数,并且可能在某些情况下提供更可控的精度或性能。在获取最终结果时,需要将目标值和相关成本除以相同的缩放因子还原。

对于本问题,由于目标是最小化成本差异,而成本本身是浮点数,通过手动缩放,我们可以将所有成本转换为整数,使CP-SAT能更直接地处理整数目标函数。

性能调优与注意事项

  1. CP-SAT参数: CP-SAT拥有丰富的参数集,可以通过 solver.parameters 进行访问和调整。例如,solver.parameters.log_search_progress = True 可以打印详细的求解日志,帮助理解求解器的行为。solver.parameters.num_workers 可以设置并行求解的线程数,充分利用多核CPU。
  2. 问题规模: 即使CP-SAT效率更高,对于极其庞大的问题(N远超70甚至上百),求解时间仍然可能很长。此时,可能需要考虑问题分解、启发式算法或近似求解策略。
  3. 约束紧密性: 在CP-SAT中,更紧密、更强的约束通常能帮助求解器更快地剪枝搜索空间。尽量利用CP-SAT提供的各种约束类型(如AddAllDifferent、AddCircuit等)来精确表达问题逻辑。
  4. 初始解: CP-SAT通常不直接接受外部提供的初始解来“热启动”求解过程。然而,对于某些特定问题,可以通过自定义搜索策略或利用启发式方法生成一个好的初始解,然后将其作为模型的一个约束(例如,将某些变量固定为初始解的值),从而引导求解器。但对于一般分配问题,直接切换到CP-SAT通常已是最大的性能提升。

总结

当使用OR-Tools解决大规模分配问题,特别是当问题本质上是整数规划时,从linear_solver切换到CP-SAT求解器是提升性能的关键策略。CP-SAT凭借其在整数和布尔变量处理上的优势,以及对浮点系数的内部缩放能力,能够显著缩短求解时间,使原本不切实际的问题规模变得可解。在实际应用中,理解CP-SAT的模型构建范式并适当考虑浮点数缩放,将有助于充分发挥其强大性能。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
相关文章 更多
using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

一个 memwatch 实战案例:定位野指针问题
一个 memwatch 实战案例:定位野指针问题

内存监控工具的价值与挑战在软件开发,尤其是使用C/C++这类手动管理内存的语言时,内存错误是程序员最常遭遇的难题之一。其中,野指针问题因其隐蔽性和破坏性,往往成为最难定位的“幽灵”缺陷。它可能潜伏在代码中,在特定条件下才被触发,导致程序崩溃、数据损坏或难以预测的行为。传统的调试手段,如打印日志或使用

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。