简体   繁体   English

如何使用 CP-SAT 求解器找到一个 3D 数组,该数组对行、列和管(代表学校班级、学期和上课日)有约束?

[英]How to use CP-SAT solver to find a 3D array with constraints on rows, columns and tubes (representing school classes, school terms and lesson days)?

I will be grateful to anyone who can help me to write some Python code to create a 21×2×3 array, indexed with i, j and k, and fill it with eighty-four "0" values and the values "Ava", "Bob", "Joe", "Mia", "Sam", "Tom", "Zoe" in such a way that:如果有人能帮助我编写一些 Python 代码来创建一个 21×2×3 数组,索引为 i、j 和 k,并用八十四个“0”值和值“Ava”填充它,我将不胜感激, "Bob", "Joe", "Mia", "Sam", "Tom", "Zoe" 这样:

  1. fixed the index i you have exactly two empty 2-tuples and one 2-tuple with different non-zero values;修复了索引 i 你正好有两个空的 2 元组和一个具有不同非零值的 2 元组;

  2. fixed the index k you have exactly fourteen empty 2-tuples and seven 2-tuple with different non-zero values;修复了索引 k 你正好有 14 个空二元组和七个具有不同非零值的二元组;

  3. fixed the indexes j and k you have a 21-tuple with fourteen zero values and exactly one occurrence of each of the non-zero values, respecting the following constraints:固定索引 j 和 k 你有一个 21 元组,有 14 个零值,每个非零值恰好出现一次,遵守以下约束:

    a) "Ava" can appear only in a row with index 0, 1, 4, 6, 10, 11, 13, 14, 15, 19 or 20; a) "Ava" 只能出现在索引为 0、1、4、6、10、11、13、14、15、19 或 20 的一行中;

    b) "Bob" can appear only in a row with index 2, 3, 5, 7, 8, 9, 12, 16, 17 or 18; b) "Bob" 只能出现在索引为 2、3、5、7、8、9、12、16、17 或 18 的一行中;

    c) "Joe" can appear only in a row with index 2, 4, 5, 7, 8, 10, 14, 15, 18 or 20; c) "Joe" 只能出现在索引为 2、4、5、7、8、10、14、15、18 或 20 的一行中;

    d) "Mia" can appear only in a row with index 0, 1, 3, 6, 9, 12, 13, 16, 17 or 19; d) “Mia”只能出现在索引为 0、1、3、6、9、12、13、16、17 或 19 的一行中;

    e) "Sam" can appear only in a row with index 1, 2, 7, 9, 15, 17 or 20; e) “Sam”只能出现在索引为 1、2、7、9、15、17 或 20 的一行中;

    f) "Tom" can appear only in a row with index 0, 3, 8, 10, 12, 16 or 19; f) "Tom" 只能出现在索引为 0、3、8、10、12、16 或 19 的一行中;

    g) "Zoe" can appear only in a row with index 4, 5, 6, 11, 13, 14 or 18. g) "Zoe" 只能出现在索引为 4、5、6、11、13、14 或 18 的一行中。

As a result I would like to obtain something like this:结果我想获得这样的东西:

[ 0   0       [Tom Mia      [ 0   0
  0   0        Ava Sam        0   0
  0   0        Sam Bob        0   0
  0   0        Bob Tom        0   0
  0   0         0   0        Joe Zoe
  0   0        Joe Zoe        0   0
  0   0         0   0        Zoe Ava
 Joe Sam        0   0         0   0
  0   0         0   0        Tom Bob
  0   0         0   0        Mia Sam
 Tom Ava        0   0         0   0
 Ava Zoe        0   0         0   0
 Bob Mia        0   0         0   0
  0   0        Mia Ava        0   0
  0   0        Zoe Joe        0   0
 Sam Joe        0   0         0   0
  0   0         0   0        Bob Tom
  0   0         0   0        Sam Mia
 Zoe Bob        0   0         0   0
 Mia Tom        0   0         0   0
  0   0 ]       0   0 ]      Ava Joe]

Rows represent school classes, columns represent school terms (there are 2 of them), tubes represent class days (there are 3 of them: Monday, Wednesday ans Friday).行代表学校班级,列代表学期(有 2 个),管代表 class 天(有 3 个:星期一、星期三和星期五)。 So the first horizontal slice of the above solution means that class 1A has lesson only on Wednesday, in the the first term with teacher Tom and in the second term with teacher Mia.因此,上述解决方案的第一个水平切片意味着 class 1A 仅在星期三上课,第一学期与 Tom 老师上课,第二学期与 Mia 老师上课。 (Teachers can only work in some classes and not in others.) (教师只能在某些班级工作,而不能在其他班级工作。)

Thanks in advance!提前致谢!

PS聚苯乙烯
As a starting point, I tried to attack the following toy problem:作为起点,我试图解决以下玩具问题:
Enumerate all arrays with a given number of rows and 3 columns which are two-thirds filled with "0" and one-third filled with "1" in such a way that summing the values in each row you always get 1 and summing the values in each column you always get rows / 3 .枚举具有给定rows数和3列的所有 arrays,其中三分之二用“0”填充,三分之一用“1”填充,这样对每一行中的值求和总能得到 1 并对值求和在每一列中,您总是得到rows / 3
Finally, after struggling a bit, I think I managed to get a solution with the following code, that I kindly ask you to correct or improve.最后,经过一番努力,我想我设法用下面的代码得到了一个解决方案,我恳请您更正或改进。 (I have set rows = 6 because the number of permutations of the obvious solution is 6,/(2!*2!*2!) = 90, whereas setting rows = 21 I would have got 21,/(7,*7.*7!) = 399,072,960 solutions.) (我设置了rows = 6因为明显解的排列数是 6,/(2!*2!*2!) = 90,而设置rows = 21我会得到 21,/(7,*7 .*7!) = 399,072,960 个解。)

from ortools.sat.python import cp_model

# create model
model = cp_model.CpModel()

# create variables
rows = 6
columns = 3
x = []
for i in range(rows):
  x.append([model.NewBoolVar("x[{}][{}]".format(i, j)) for j in range(columns)])

# add constraints
for i in range(rows):
  model.Add(sum(x[i]) == 1)

for j in range(columns):
  model.Add(sum(x[i][j] for i in range(rows)) == rows//columns)


class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):

    def __init__(self, variables, limit):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self.__variables = variables
        self.__solution_count = 0
        self.__solution_limit = limit

    def solution_count(self):
        return self.__solution_count

    def on_solution_callback(self):
        self.__solution_count += 1
        for v in self.__variables:
          print('%i' % (self.Value(v)), end='')
          #  print('%s=%i' % (v, self.Value(v)), end=' ')
        print()
        if self.__solution_count >= self.__solution_limit:
          print('Stop search after %i solutions' % self.__solution_limit)
          self.StopSearch()


# create solver and solve model
solver = cp_model.CpSolver()
solution_limit = 100000
solution_printer = VarArraySolutionPrinter([*x[0], *x[1], *x[2], *x[3], *x[4], *x[rows-1]], solution_limit)
solver.parameters.enumerate_all_solutions = True
# solver.parameters.log_search_progress = True
# Works better with more workers (at least 8, 16 if enough cores).
# solver.parameters.num_workers = 8
status = solver.Solve(model, solution_printer)

print('Number of solutions found: %i' % solution_printer.solution_count())

Your "toy" problem is definitely going in the right direction.你的“玩具”问题肯定是在朝着正确的方向发展。

For your actual problem, try making a 21×2×3x8 array x indexed with i, j, k and p (for person) of BoolVar's.对于您的实际问题,尝试制作一个 21×2×3x8 数组 x,索引为 BoolVar 的 i、j、k 和 p(代表人)。 The last index represents the person, it will need 0 to represent "nobody" and for the rest Ava = 1, Bob = 2, etc., so its max value will be one more than the number of people.最后一个索引代表人,它需要 0 来代表“没有人”,对于 rest Ava = 1,Bob = 2 等,因此它的最大值将比人数多一。 If the variable X[i,j,k,p] is true (1) it means that the given person p is present at the index i, j, k.如果变量 X[i,j,k,p] 为真 (1),则表示给定的人 p 出现在索引 i、j、k 处。 If X[i,j,k,0] is true, it means a 0 = nobody is present at i, j, k.如果 X[i,j,k,0] 为真,则表示 0 = 没有人出现在 i、j、k 处。

For all i, j, k, constrain the sum of x[i, j, k, p] for p in the range of people to be equal to 1, so exactly nobody or one person is selected at a given slot.对于所有 i, j, k,约束 p 在人范围内的 x[i, j, k, p] 的总和等于 1,因此在给定的槽位中没有人或一个人被选中。

For point 1: fixed the index i you have exactly two empty 2-tuples and one 2-tuple with different non-zero values:对于第 1 点:固定索引 i 你正好有两个空 2 元组和一个具有不同非零值的 2 元组:

For all i constrain the sum of x[i, j, k, p] for all j, k, p in their respective ranges (except p = 0) to be exactly equal to 2, so exactly two people are in a given row.对于所有 i 约束 x[i, j, k, p] 的总和对于所有 j, k, p 在它们各自的范围内(除了 p = 0)正好等于 2,所以恰好有两个人在给定的行中.

For all i, k, and for p = 0, add the implications对于所有 i、k 和 p = 0,添加含义

x[i, 0, k, 0] == x[i, 1, k, 0] x[i, 0, k, 0] == x[i, 1, k, 0]

This will ensure that if one of the pair is 0, so is the other.这将确保如果一对中的一个为 0,另一个也为 0。

For all i, k and p except p = 0, add the implications对于除 p = 0 之外的所有 i、k 和 p,添加含义

x[i, 0, k, p] implies x[i, 1, k, p].Not and x[i, 0, k, p] 表示 x[i, 1, k, p].Not and

x[i, 1, k, p] implies x[i, 0, k, p].Not x[i, 1, k, p] 表示 x[i, 0, k, p].Not

This will ensure a tuple will consist of two different people.这将确保一个元组将由两个不同的人组成。

Alternative formulation of the last part:最后一部分的替代表述:

For all i and p except p = 0, constraint the sum of x[i, j, k, p] for all j and k to be exactly equal to 1.对于除 p = 0 之外的所有 i 和 p,约束所有 j 和 k 的 x[i, j, k, p] 之和恰好等于 1。

For point 2: fixed the index k you have exactly fourteen empty 2-tuples and seven 2-tuple with different non-zero values;对于第 2 点:固定索引 k 你正好有 14 个空 2 元组和 7 个具有不同非零值的 2 元组;

For all k constrain the sum of x[i, j, k, p] for all i, j, p in their respective ranges to be exactly equal to 7, so exactly seven people are in a given column.对于所有 k,将 x[i, j, k, p] 的总和约束在各自范围内的所有 i, j, p 恰好等于 7,因此给定列中恰好有 7 个人。

For all k and p except p = 0, constraint the sum of x[i, j, k, p] for all i and j to be exactly equal to 1, so each person appears exactly once in the column.对于除 p = 0 之外的所有 k 和 p,约束所有 i 和 j 的 x[i, j, k, p] 之和正好等于 1,因此每个人在列中只出现一次。

For point 3:对于第 3 点:

For all j and k, Constrain x[i, j, k, p] == 0 for the row i in which each person p can't appear.对于所有的 j 和 k,约束 x[i, j, k, p] == 0 对于行 i 其中每个人 p 不能出现。

Let us know how it works.让我们知道它是如何工作的。

You're taking a pretty big swing if you are new to the trifecta of python , linear programming, and pulp , but the problem you describe is very doable...perhaps the below will get you started.如果您不python 、线性规划和pulp的三重奏,那么您将获得一个相当大的 swing ,但是您描述的问题非常可行……也许下面的内容会让您入门。 It is a smaller example that should work just fine for the data you have, I just didn't type it all in.这是一个较小的示例,应该可以很好地处理您拥有的数据,我只是没有全部输入。

A couple notes:一些注意事项:

  • The below is a linear program.下面是一个线性程序。 It is "naturally integer" as coded, preventing the need to restrict the domain of the variables to integers, so it is much easier to solve.它是编码的“自然整数”,无需将变量的域限制为整数,因此更容易解决。 (A topic for you to research, perhaps). (也许是您要研究的主题)。
  • You could certainly code this up as a constraint problem as well, I'm just not as familiar.您当然也可以将其编码为约束问题,我只是不太熟悉。 You could also code this up like a matrix as you are doing with i, j, k, but most frameworks allow more readable names for the sets.您也可以像使用 i、j、k 一样将其编码为矩阵,但大多数框架允许为集合命名更具可读性。
  • The teaching day M/W/F is arbitrary and not linked to anything else in the problem, so you can (externally to the problem), just pick 1/3 of the assignments per day from the solution for each course & term.教学日 M/W/F 是任意的,与问题中的任何其他内容无关,因此您可以(在问题外部)每天从每门课程和学期的解决方案中选择 1/3 的作业。
  • The transition from the verbiage to the constraint formulation is most of the magic in linear programming and you'd be well suited with an introductory text if you continue along!从冗长的文字到约束公式的过渡是线性规划中的大部分魔法,如果您继续下去,您将非常适合阅读介绍性文字!

Code:代码:

# teacher assignment

import pulp
from itertools import chain

# some data...
teach_days = {'M', 'W', 'F'}
terms = {'Spring', 'Fall'}
courses = {'Math 101', 'English 203', 'Physics 201'}
legal_asmts = { 'Bob': {'Math 101', 'Physics 201'},
                'Ann': {'Math 101', 'English 203'},
                'Tim': {'English 203'},
                'Joe': {'Physics 201'}}

# quick sanity check
assert courses == set.union(*chain(legal_asmts.values())), 'course mismatch'

# set up the problem

prob = pulp.LpProblem('teacher_assignment', pulp.LpMaximize)

# make a 3-tuple index of the term, class, teacher
idx = [(term, course, teacher) for term in terms for course in courses for teacher in legal_asmts.keys()]

assn = pulp.LpVariable.dicts('assign', idx, cat=pulp.LpContinuous, lowBound=0)

# OBJECTIVE:  teach as many courses as possible within constraints...
prob += pulp.lpSum(assn)

# CONSTRAINTS
# teach each class no more than once per term
for term in terms:
    for course in courses:
        prob += pulp.lpSum(assn[term, course, teacher] for teacher in legal_asmts.keys()) <= 1

# each teacher no more than 1 course per term
for term in terms:
    for teacher in legal_asmts.keys():
        prob += pulp.lpSum(assn[term, course, teacher] for course in courses) <= 1

# each teacher can only teach within legal assmts, and if legal, only teach it once
for teacher in legal_asmts.keys():
    for course in courses:
        if course in legal_asmts.get(teacher):
            prob += pulp.lpSum(assn[term, course, teacher] for term in terms) <= 1
        else:  # it is not legal assignment
            prob += pulp.lpSum(assn[term, course, teacher] for term in terms) <= 0


prob.solve()

#print(prob)

# Inspect results...
for i in idx:
    if assn[i].varValue:  # will be true if value is non-zero
        print(i, assn[i].varValue)

Output: Output:

Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Presolve 16 (-10) rows, 12 (-12) columns and 32 (-40) elements
Perturbing problem by 0.001% of 1 - largest nonzero change 0.00010234913 ( 0.010234913%) - largest zero change 0
0  Obj -0 Dual inf 11.99913 (12)
10  Obj 5.9995988
Optimal - objective value 6
After Postsolve, objective 6, infeasibilities - dual 0 (0), primal 0 (0)
Optimal objective 6 - 10 iterations time 0.002, Presolve 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.00   (Wallclock seconds):       0.00

('Spring', 'Math 101', 'Bob') 1.0
('Spring', 'Physics 201', 'Joe') 1.0
('Spring', 'English 203', 'Ann') 1.0
('Fall', 'Math 101', 'Ann') 1.0
('Fall', 'Physics 201', 'Bob') 1.0
('Fall', 'English 203', 'Tim') 1.0

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM