// Copyright 2010-2019 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.
using System;
using System.Collections.Generic;
using System.Linq;
using Google.OrTools.Sat;
///
/// 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.
///
public class BalanceGroupSat
{
static void Main(string[] args)
{
int numberGroups = 10;
int numberItems = 100;
int numberColors = 3;
int minItemsOfSameColorPerGroup = 4;
var allGroups = Enumerable.Range(0, numberGroups).ToArray();
var allItems = Enumerable.Range(0, numberItems).ToArray();
var allColors = Enumerable.Range(0, numberColors).ToArray();
var values = allItems.Select(i => 1 + i + (i * i / 200)).ToArray();
var colors = allItems.Select(i => i % numberColors).ToArray();
var sumOfValues = values.Sum();
var averageSumPerGroup = sumOfValues / numberGroups;
var numItemsPerGroup = numberItems / numberGroups;
var itemsPerColor = new Dictionary>();
foreach (var color in allColors)
{
itemsPerColor[color] = new List();
foreach (var item in allItems)
{
if(colors[item] == color)
itemsPerColor[color].Add(item);
}
}
Console.WriteLine($"Model has {numberItems}, {numberGroups} groups and {numberColors} colors");
Console.WriteLine($" Average sum per group = {averageSumPerGroup}");
var model = new CpModel();
var itemInGroup = new IntVar[numberItems, numberGroups];
foreach (var item in allItems)
{
foreach (var @group in allGroups)
{
itemInGroup[item, @group] = model.NewBoolVar($"item {item} in group {@group}");
}
}
// Each group must have the same size.
foreach (var @group in allGroups)
{
var itemsInGroup = allItems.Select(x => itemInGroup[x, @group]).ToArray();
model.AddLinearConstraint(LinearExpr.Sum(itemsInGroup), numItemsPerGroup, numItemsPerGroup);
}
//# One item must belong to exactly one group.
foreach (var item in allItems)
{
var groupsForItem = allGroups.Select(x => itemInGroup[item, x]).ToArray();
model.Add(LinearExpr.Sum(groupsForItem) == 1);
}
// The deviation of the sum of each items in a group against the average.
var e = model.NewIntVar(0, 550, "epsilon");
// Constrain the sum of values in one group around the average sum per group.
foreach (var @group in allGroups)
{
var itemValues = allItems.Select(x => itemInGroup[x, @group]).ToArray();
var sum = LinearExpr.ScalProd(itemValues, values);
model.Add(sum <= averageSumPerGroup + e);
model.Add(sum >= averageSumPerGroup - e);
}
// colorInGroup variables.
var colorInGroup = new IntVar[numberColors, numberGroups];
foreach (var @group in allGroups)
{
foreach (var color in allColors)
{
colorInGroup[color, @group] = model.NewBoolVar($"color {color} is in group {@group}");
}
}
// Item is in a group implies its color is in that group.
foreach (var item in allItems)
{
foreach (var @group in allGroups)
{
model.AddImplication(itemInGroup[item, @group], colorInGroup[colors[item], @group]);
}
}
// If a color is in a group, it must contains at least
// min_items_of_same_color_per_group items from that color.
foreach (var color in allColors)
{
foreach (var @group in allGroups)
{
var literal = colorInGroup[color, @group];
var items = itemsPerColor[color].Select(x => itemInGroup[x, @group]).ToArray();
model.Add(LinearExpr.Sum(items) >= minItemsOfSameColorPerGroup).OnlyEnforceIf(literal);
}
}
// Compute the maximum number of colors in a group.
int maxColor = numItemsPerGroup / minItemsOfSameColorPerGroup;
// Redundant contraint: The problem does not solve in reasonable time without it.
if (maxColor < numberColors)
{
foreach (var @group in allGroups)
{
var all = allColors.Select(x => colorInGroup[x, @group]).ToArray();
model.Add(LinearExpr.Sum(all) <= maxColor);
}
}
// Minimize epsilon
model.Minimize(e);
var solver = new CpSolver();
solver.StringParameters = "";
var solutionPrinter = new SolutionPrinter(values, colors, allGroups, allItems, itemInGroup);
var status = solver.SolveWithSolutionCallback(model, solutionPrinter);
}
public class SolutionPrinter : CpSolverSolutionCallback
{
private int[] _values;
private int[] _colors;
private int[] _allGroups;
private int[] _allItems;
private IntVar[,] _itemInGroup;
private int _solutionCount;
public SolutionPrinter(int[] values, int[] colors, int[] allGroups, int[] allItems, IntVar[,] itemInGroup)
{
this._values = values;
this._colors = colors;
this._allGroups = allGroups;
this._allItems = allItems;
this._itemInGroup = itemInGroup;
}
public override void OnSolutionCallback()
{
Console.WriteLine($"Solution {_solutionCount}");
_solutionCount++;
Console.WriteLine($" objective value = {this.ObjectiveValue()}");
Dictionary> groups = new Dictionary>();
int[] sum = new int[_allGroups.Length];
foreach (var @group in _allGroups)
{
groups[@group] = new List();
foreach (var item in _allItems)
{
if (BooleanValue(_itemInGroup[item, @group]))
{
groups[@group].Add(item);
sum[@group] += _values[item];
}
}
}
foreach (var g in _allGroups)
{
var group = groups[g];
Console.Write($"Group {g}: sum = {sum[g]} [");
foreach (var item in group)
{
Console.Write($"({item}, {_values[item]}, {_colors[item]})");
}
Console.WriteLine("]");
}
}
}
}