180 lines
6.1 KiB
Python
180 lines
6.1 KiB
Python
# Copyright 2010-2022 Google LLC
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""We are trying to group items in equal sized groups.
|
|
|
|
Each item has a color and a value. We want the sum of values of each group to
|
|
be as close to the average as possible.
|
|
Furthermore, if one color is an a group, at least k items with this color must
|
|
be in that group.
|
|
"""
|
|
|
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
# Create a solution printer.
|
|
class SolutionPrinter(cp_model.CpSolverSolutionCallback):
|
|
"""Print intermediate solutions."""
|
|
|
|
def __init__(self, values, colors, all_groups, all_items, item_in_group):
|
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
|
self.__solution_count = 0
|
|
self.__values = values
|
|
self.__colors = colors
|
|
self.__all_groups = all_groups
|
|
self.__all_items = all_items
|
|
self.__item_in_group = item_in_group
|
|
|
|
def on_solution_callback(self):
|
|
print('Solution %i' % self.__solution_count)
|
|
self.__solution_count += 1
|
|
|
|
print(' objective value = %i' % self.ObjectiveValue())
|
|
groups = {}
|
|
sums = {}
|
|
for g in self.__all_groups:
|
|
groups[g] = []
|
|
sums[g] = 0
|
|
for item in self.__all_items:
|
|
if self.BooleanValue(self.__item_in_group[(item, g)]):
|
|
groups[g].append(item)
|
|
sums[g] += self.__values[item]
|
|
|
|
for g in self.__all_groups:
|
|
group = groups[g]
|
|
print('group %i: sum = %0.2f [' % (g, sums[g]), end='')
|
|
for item in group:
|
|
value = self.__values[item]
|
|
color = self.__colors[item]
|
|
print(' (%i, %i, %i)' % (item, value, color), end='')
|
|
print(']')
|
|
|
|
|
|
def main():
|
|
# Data.
|
|
num_groups = 10
|
|
num_items = 100
|
|
num_colors = 3
|
|
min_items_of_same_color_per_group = 4
|
|
|
|
all_groups = range(num_groups)
|
|
all_items = range(num_items)
|
|
all_colors = range(num_colors)
|
|
|
|
# Values for each items.
|
|
values = [1 + i + (i * i // 200) for i in all_items]
|
|
# Color for each item (simple modulo).
|
|
colors = [i % num_colors for i in all_items]
|
|
|
|
sum_of_values = sum(values)
|
|
average_sum_per_group = sum_of_values // num_groups
|
|
|
|
num_items_per_group = num_items // num_groups
|
|
|
|
# Collect all items in a given color.
|
|
items_per_color = {}
|
|
for c in all_colors:
|
|
items_per_color[c] = []
|
|
for i in all_items:
|
|
if colors[i] == c:
|
|
items_per_color[c].append(i)
|
|
|
|
print('Model has %i items, %i groups, and %i colors' %
|
|
(num_items, num_groups, num_colors))
|
|
print(' average sum per group = %i' % average_sum_per_group)
|
|
|
|
# Model.
|
|
|
|
model = cp_model.CpModel()
|
|
|
|
item_in_group = {}
|
|
for i in all_items:
|
|
for g in all_groups:
|
|
item_in_group[(i, g)] = model.NewBoolVar('item %d in group %d' %
|
|
(i, g))
|
|
|
|
# Each group must have the same size.
|
|
for g in all_groups:
|
|
model.Add(
|
|
sum(item_in_group[(i, g)]
|
|
for i in all_items) == num_items_per_group)
|
|
|
|
# One item must belong to exactly one group.
|
|
for i in all_items:
|
|
model.Add(sum(item_in_group[(i, g)] for g in all_groups) == 1)
|
|
|
|
# The deviation of the sum of each items in a group against the average.
|
|
e = model.NewIntVar(0, 550, 'epsilon')
|
|
|
|
# Constrain the sum of values in one group around the average sum per group.
|
|
for g in all_groups:
|
|
model.Add(
|
|
sum(item_in_group[(i, g)] * values[i]
|
|
for i in all_items) <= average_sum_per_group + e)
|
|
model.Add(
|
|
sum(item_in_group[(i, g)] * values[i]
|
|
for i in all_items) >= average_sum_per_group - e)
|
|
|
|
# color_in_group variables.
|
|
color_in_group = {}
|
|
for g in all_groups:
|
|
for c in all_colors:
|
|
color_in_group[(c, g)] = model.NewBoolVar(
|
|
'color %d is in group %d' % (c, g))
|
|
|
|
# Item is in a group implies its color is in that group.
|
|
for i in all_items:
|
|
for g in all_groups:
|
|
model.AddImplication(item_in_group[(i, g)],
|
|
color_in_group[(colors[i], g)])
|
|
|
|
# If a color is in a group, it must contains at least
|
|
# min_items_of_same_color_per_group items from that color.
|
|
for c in all_colors:
|
|
for g in all_groups:
|
|
literal = color_in_group[(c, g)]
|
|
model.Add(
|
|
sum(item_in_group[(i, g)] for i in items_per_color[c]) >=
|
|
min_items_of_same_color_per_group).OnlyEnforceIf(literal)
|
|
|
|
# Compute the maximum number of colors in a group.
|
|
max_color = num_items_per_group // min_items_of_same_color_per_group
|
|
# Redundant contraint: The problem does not solve in reasonable time without it.
|
|
if max_color < num_colors:
|
|
for g in all_groups:
|
|
model.Add(
|
|
sum(color_in_group[(c, g)] for c in all_colors) <= max_color)
|
|
|
|
# Minimize epsilon
|
|
model.Minimize(e)
|
|
|
|
model.ExportToFile('balance_group_sat.pbtxt')
|
|
|
|
solver = cp_model.CpSolver()
|
|
solution_printer = SolutionPrinter(values, colors, all_groups, all_items,
|
|
item_in_group)
|
|
status = solver.Solve(model, solution_printer)
|
|
|
|
if status == cp_model.OPTIMAL:
|
|
print('Optimal epsilon: %i' % solver.ObjectiveValue())
|
|
print('Statistics')
|
|
print(' - conflicts : %i' % solver.NumConflicts())
|
|
print(' - branches : %i' % solver.NumBranches())
|
|
print(' - wall time : %f s' % solver.WallTime())
|
|
else:
|
|
print('No solution found')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|