Set and List Variables¶
In addition to Boolean, integers and floats, Hexaly Optimizer offers higher-level decision variables: lists and sets, presented here, and interval variables.
Mathematical principle and creation operator¶
The set and list operators define a decision variable whose value is
a collection of integers within domain [0, n-1], where n is the unique
operand of the operator. They do not necessarily contain all the elements in
[0, n-1]. The elements in a list or set are pairwise different, non negative
and strictly smaller than n. Note that the operand must be a constant,
strictly positive integer.
Set decisions¶
Mathematically, a set decision variable of domain size n represents a subset
of {0, ..., n-1}. It is a set in the usual mathematical sense: each element
is unique in the set, and the elements inside a set are unordered.
For instance, the following line creates a set decision variable of domain size 5:
x <- set(5);
x = model.set(5)
HxExpression x = model.setVar(5);
HxExpression x = model.Set(5);
HxExpression x = model.setVar(5);
Based on this definition, the following examples represent valid values for a set decision variable of domain size 5:
{} // Empty set
{2, 3} // Equivalent to {3, 2}
{4, 2, 0, 1, 3} // Full set, equivalent to {0, 1, 2, 3, 4}
On the contrary, the following examples are invalid values:
{5} // Out of bounds for a set decision of domain size 5
{1, 2, 3, 1} // Repetition of element 1, which is impossible since each element is unique
List decisions¶
Mathematically, a list decision variable of domain size n represents a
subpermutation of {0, ..., n-1}. As for sets, each element is unique in a
list. The difference between sets and lists is that elements inside a list are
ordered.
For instance, the following line creates a list decision variable of domain size 5:
x <- list(5);
x = model.list(5)
HxExpression x = model.listVar(5);
HxExpression x = model.List(5);
HxExpression x = model.listVar(5);
Based on this definition, the following examples represent valid values for a list decision variable of domain size 5:
[] // Empty list
[2, 3] // Different from [3, 2], which is also valid
[4, 2, 0, 1, 3] // Full list
On the contrary, the following examples are invalid values:
[5] // Out of bounds for a list decision of domain size 5
[1, 2, 3, 1] // Repetition of element 1, which is impossible since each element is unique
Setting and retrieving values¶
As mentioned above, the value of a list or a set is a collection of integers.
This value is obtained with the syntax x.value in Hexaly Modeler, with
x.get_value() in Python, with getCollectionValue() in C++ and Java,
and with GetCollectionValue() in C#. It returns an object of type
HxCollection, that can be read and modified through the methods:
count, get, clear, add.
Modifying this HxCollection object modifies the value of the corresponding
list or set variable. The code below illustrates the use of these methods:
println(x.value.count()); // Current size of the collection
x.value.clear(); // Empty the list
x.value.add(3); // Add a value, throw an error if this value in not in interval [0,4], if x was defined as list(5)
x.value.add(1); // Add a value, throw an error if this value is already included in the list
for[e in x.value] println(e); // Print the content of the list
println(x.value); // Print the content of the list (``[3, 1]`` in this case)
collection = x.get_value()
print(collection.count()) # Current size of the collection
collection.clear() # Empty the list
collection.add(3) # Add a value, throw an error if this value in not in interval [0,4], if x was defined as list(5)
collection.add(1) # Add a value, throw an error if this value is already included in the list
for e in collection:
print(e) # Print the content of the list
print(collection) # Print the content of the list (``[3, 1]`` in this case)
HxCollection collection = x.getCollectionValue();
std::cout << collection.count() << std::endl; // Current size of the collection
collection.clear(); // Empty the list
collection.add(3); // Add a value, throw an error if this value in not in interval [0,4], if x was defined as list(5)
collection.add(1); // Add a value, throw an error if this value is already included in the list
for (int i = 0; i < collection.count(); ++i)
std::cout << collection.get(i) << std::endl; // Print the content of the list
HxCollection collection = x.GetCollectionValue();
Console.WriteLine(collection.Count()); // Current size of the collection
collection.Clear(); // Empty the list
collection.Add(3); // Add a value, throw an error if this value in not in interval [0,4], if x was defined as list(5)
collection.Add(1); // Add a value, throw an error if this value is already included in the list
foreach (long e in collection)
Console.WriteLine(e); // Print the content of the list
HxCollection collection = x.getCollectionValue();
System.out.println(collection.count()); // Current size of the collection
collection.clear(); // Empty the list
collection.add(3); // Add a value, throw an error if this value in not in interval [0,4], if x was defined as list(5)
collection.add(1); // Add a value, throw an error if this value is already included in the list
for (int i = 0; i < collection.count(); ++i)
System.out.println(collection.get(i)); // Print the content of the list
Operators on lists and sets¶
Unary and binary operators¶
The count operator returns the number of elements in a collection. For
example, the following model merely expresses the search for a set of maximum
size:
x <- set(5);
maximize count(x);
x = model.set(5)
model.maximize(model.count(x))
HxExpression x = model.setVar(5);
model.maximize(model.count(x));
HxExpression x = model.Set(5);
model.Maximize(model.Count(x));
HxExpression x = model.setVar(5);
model.maximize(model.count(x));
The contains operator expresses that an element is present in a collection.
For example, the following model defines a knapsack problem using a set:
knapsack <- set(n);
constraint sum[i in 0...n](weight[i] * contains(knapsack, i)) <= capacity;
maximize sum[i in 0...n](value[i] * contains(knapsack, i));
knapsack = model.set(n)
knapsack_weight = model.sum(
weight[i] * model.contains(knapsack, i) for i in range(n))
knapsack_value = model.sum(
value[i] * model.contains(knapsack, i) for i in range(n))
model.constraint(knapsack_weight <= capacity)
model.maximize(knapsack_value)
HxExpression knapsack = model.setVar(n);
HxExpression knapsackWeight = model.sum();
HxExpression knapsackValue = model.sum();
for (int i = 0; i < n; ++i) {
knapsackWeight.addOperand(weight[i] * model.contains(knapsack, i));
knapsackValue.addOperand(value[i] * model.contains(knapsack, i));
}
model.constraint(knapsackWeight <= capacity);
model.maximize(knapsackValue);
HxExpression knapsack = model.Set(n);
HxExpression knapsackWeight = model.Sum();
HxExpression knapsackValue = model.Sum();
for (int i = 0; i < n; ++i)
{
knapsackWeight.AddOperand(weight[i] * model.Contains(knapsack, i));
knapsackValue.AddOperand(value[i] * model.Contains(knapsack, i));
}
model.Constraint(knapsackWeight <= capacity);
model.Maximize(knapsackValue);
HxExpression knapsack = model.setVar(n);
HxExpression knapsackWeight = model.sum();
HxExpression knapsackValue = model.sum();
for (int i = 0; i < n; ++i) {
knapsackWeight.addOperand(model.prod(weight[i], model.contains(knapsack, i)));
knapsackValue.addOperand(model.prod(value[i], model.contains(knapsack, i)));
}
model.constraint(model.leq(knapsackWeight, capacity));
model.maximize(knapsackValue);
The distinct operator takes as input a collection and a lambda function,
and returns the unordered set of distinct values among all the values returned
by the function. For example, the following constraint forces a machine to
produce at most two distinct types of product:
machine <- list(10);
productTypes = { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 };
constraint count(distinct(machine, i => productTypes[i])) <= 2;
machine = model.list(10)
product_types = model.array([0, 1, 2, 1, 2, 0, 0, 1, 2, 1])
product_type = model.lambda_function(lambda i: product_types[i])
model.constraint(model.count(model.distinct(machine, product_type)) <= 2)
HxExpression machine = model.listVar(10);
std::vector<int> productTypes { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 };
HxExpression hxProductTypes = model.array(productTypes.begin(), productTypes.end());
HxExpression productType = model.lambdaFunction(
[&](HxExpression i) { return hxProductTypes[i]; });
model.constraint(model.count(model.distinct(machine, productType)) <= 2);
HxExpression machine = model.List(10);
long[] productTypes = { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 };
HxExpression hxProductTypes = model.Array(productTypes);
HxExpression productType = model.LambdaFunction(
(HxExpression i) => model.At(hxProductTypes, i));
model.Constraint(model.Count(model.Distinct(machine, productType)) <= 2);
HxExpression machine = model.listVar(10);
long[] productTypes = { 0, 1, 2, 1, 2, 0, 0, 1, 2, 1 };
HxExpression hxProductTypes = model.array(productTypes);
HxExpression productType = model.lambdaFunction(
i -> model.at(hxProductTypes, i));
model.constraint(model.leq(model.count(model.distinct(machine, productType)), 2));
The intersection operator takes two operands if operands are collections or
arrays. It returns the unordered set of the values present in both operands.
For example the following constraint prevents the set from containing values 0,
1 and 2:
forbiddenValues <- array(0, 1, 2);
numbers <- set(10);
constraint count(intersection(numbers, forbiddenValues)) == 0;
forbidden_values = model.array([0, 1, 2])
numbers = model.set(10)
model.constraint(model.count(model.intersection(numbers, forbidden_values)) == 0)
std::vector<int> forbiddenValues { 0, 1, 2 };
HxExpression hxForbiddenValues = model.array(forbiddenValues.begin(), forbiddenValues.end());
HxExpression numbers = model.setVar(10);
model.constraint(model.count(model.intersection(numbers, hxForbiddenValues)) == 0);
HxExpression forbiddenValues = model.Array(new long[] { 0, 1, 2 });
HxExpression numbers = model.Set(10);
model.Constraint(model.Count(model.Intersection(numbers, forbiddenValues)) == 0);
HxExpression forbiddenValues = model.array(new long[] { 0, 1, 2 });
HxExpression numbers = model.setVar(10);
model.constraint(model.eq(model.count(model.intersection(numbers, forbiddenValues)), 0));
The sort operator takes as input a collection and a lambda function, and
sorts the input based on the values returned by the function into ascending
order. For example, the following lines sort the objects contained in a knapsack
into ascending order of values:
knapsack <- set(n);
sortedValues <- sort(knapsack, i => value[i]);
knapsack = model.set(n)
value_array = model.array(value)
sorted_values = model.sort(knapsack, model.lambda_function(lambda i: value_array[i]))
HxExpression knapsack = model.setVar(n);
HxExpression valueArray = model.array(value.begin(), value.end());
HxExpression sortedValues = model.sort(knapsack,
model.lambdaFunction([&](HxExpression i) { return valueArray[i]; }));
HxExpression knapsack = model.Set(n);
HxExpression valueArray = model.Array(value);
HxExpression sortedValues = model.Sort(knapsack,
model.LambdaFunction((HxExpression i) => model.At(valueArray, i)));
HxExpression knapsack = model.setVar(n);
HxExpression valueArray = model.array(value);
HxExpression sortedValues = model.sort(knapsack,
model.lambdaFunction(i -> model.at(valueArray, i)));
Operators specific to lists¶
The at operator allows accessing the value at a given position in the list.
It takes two operands: a list and an integer expression. It fails when the given
index is negative or larger or equal to count(x).
For example, the objective function in the following model is to maximize the product of the first and last items in the list:
x <- list(5);
constraint count(x) > 0;
maximize x[0] * x[count(x)-1];
x = model.list(5)
model.constraint(model.count(x) > 0)
model.maximize(x[0] * x[model.count(x) - 1])
HxExpression x = model.listVar(5);
model.constraint(model.count(x) > 0);
model.maximize(x[0] * x[model.count(x) - 1]);
HxExpression x = model.List(5);
model.Constraint(model.Count(x) > 0);
model.Maximize(model.At(x, 0) * model.At(x, model.Count(x) - 1));
HxExpression x = model.listVar(5);
model.constraint(model.gt(model.count(x), 0));
model.maximize(model.prod(model.at(x, 0), model.at(x, model.sub(model.count(x), 1))));
The indexOf operator returns the position of a given integer in the list
or -1 if this integer is not included in the list. It takes two operands: a
list and an integer expression. For example, given a matrix c of size n,
the linear ordering problem consists in finding a permutation of [0, n-1]
of minimum cost, where a cost c[i][j] is paid when j is before i in
the ordering. Here is the corresponding model:
x <- list(n);
constraint count(x) == n;
minimize sum[i in 0...n][j in 0...n](c[i][j] * (indexOf(x,i) > indexOf(x,j)));
x = model.list(n)
model.constraint(model.count(x) == n)
ordering_cost = model.sum(
c[i][j] * (model.index(x, i) > model.index(x, j))
for i in range(n) for j in range(n))
model.minimize(ordering_cost)
HxExpression x = model.listVar(n);
model.constraint(model.count(x) == n);
HxExpression orderingCost = model.sum();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j)
orderingCost.addOperand(c[i][j] * (model.indexOf(x, i) > model.indexOf(x, j)));
}
model.minimize(orderingCost);
HxExpression x = model.List(n);
model.Constraint(model.Count(x) == n);
HxExpression orderingCost = model.Sum();
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
orderingCost.AddOperand(c[i][j] * (model.IndexOf(x, i) > model.IndexOf(x, j)));
}
model.Minimize(orderingCost);
HxExpression x = model.listVar(n);
model.constraint(model.eq(model.count(x), n));
HxExpression orderingCost = model.sum();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
HxExpression isJBeforeI = model.gt(model.indexOf(x, i), model.indexOf(x, j));
orderingCost.addOperand(model.prod(c[i][j], isJBeforeI));
}
}
model.minimize(orderingCost);
The array operator takes as input a list and a lambda function, and
returns the array created by applying the function to every element in the list.
It can also be used with a two-argument lambda function to create
recursive arrays. For example, the following array
defines the end times of consecutive tasks on a machine:
x <- list(n);
endTimes <- array(x, (i, prev) => prev + duration[i]);
x = model.list(n)
duration_array = model.array(duration)
end_times = model.array(
x, model.lambda_function(lambda i, prev: prev + duration_array[i]))
HxExpression x = model.listVar(n);
HxExpression durationArray = model.array(duration.begin(), duration.end());
HxExpression endTimes = model.array(x, model.lambdaFunction(
[&](HxExpression i, HxExpression prev) { return prev + durationArray[i]; }));
HxExpression x = model.List(n);
HxExpression durationArray = model.Array(duration);
HxExpression endTimes = model.Array(x, model.LambdaFunction(
(HxExpression i, HxExpression prev) => prev + model.At(durationArray, i)));
HxExpression x = model.listVar(n);
HxExpression durationArray = model.array(duration);
HxExpression endTimes = model.array(x, model.lambdaFunction(
(i, prev) -> model.sum(prev, model.at(durationArray, i))));
N-ary operators¶
The disjoint operator applies to N lists or N sets sharing the same domain.
It takes value 1 when all collections are pairwise disjoint (that is to say that
no value appears in more than one collection), and value 0 otherwise. It takes
at least one operand. In the following example we try to maximize the minimum
size among three lists. Since they are constrained to be disjoint, this maximum
will be 3:
x <- list(10);
y <- list(10);
z <- list(10);
constraint disjoint(x, y, z);
maximize min(count(x), count(y), count(z));
x = model.list(10)
y = model.list(10)
z = model.list(10)
model.constraint(model.disjoint(x, y, z))
model.maximize(model.min(model.count(x), model.count(y), model.count(z)))
HxExpression x = model.listVar(10);
HxExpression y = model.listVar(10);
HxExpression z = model.listVar(10);
model.constraint(model.disjoint(x, y, z));
model.maximize(model.min(model.count(x), model.count(y), model.count(z)));
HxExpression x = model.List(10);
HxExpression y = model.List(10);
HxExpression z = model.List(10);
model.Constraint(model.Disjoint(x, y, z));
model.Maximize(model.Min(model.Count(x), model.Count(y), model.Count(z)));
HxExpression x = model.listVar(10);
HxExpression y = model.listVar(10);
HxExpression z = model.listVar(10);
model.constraint(model.disjoint(x, y, z));
model.maximize(model.min(model.count(x), model.count(y), model.count(z)));
The cover operator applies to N lists or N sets sharing the same domain. It
takes value 1 when the given collections form a cover of the set [0, n-1], and
value 0 otherwise. It takes at least one operand. For example, the following
constraint ensures that every customer is served by at least one truck:
trucks[k in 0...nbTrucks] <- list(nbCustomers);
constraint cover[k in 0...nbTrucks](trucks[k]);
trucks = [model.list(nb_customers) for k in range(nb_trucks)]
model.constraint(model.cover(trucks))
std::vector<HxExpression> trucks(nbTrucks);
for (int k = 0; k < nbTrucks; ++k)
trucks[k] = model.listVar(nbCustomers);
model.constraint(model.cover(trucks.begin(), trucks.end()));
HxExpression[] trucks = new HxExpression[nbTrucks];
for (int k = 0; k < nbTrucks; ++k)
trucks[k] = model.List(nbCustomers);
model.Constraint(model.Cover(trucks));
HxExpression[] trucks = new HxExpression[nbTrucks];
for (int k = 0; k < nbTrucks; ++k)
trucks[k] = model.listVar(nbCustomers);
model.constraint(model.cover(trucks));
The partition operator applies to N lists or N sets sharing the same domain.
It takes value 1 when the given collections form a partition of the set
[0, n-1], and value 0 otherwise. It takes at least one operand. In other words,
it combines the disjoint and cover operators: it returns 1 if every
element in [0, n-1] is present in exactly one of the collections. For example,
the following constraint ensures that each item is placed in exactly one bin:
bins[k in 0...nbBins] <- set(nbItems);
constraint partition[k in 0...nbBins](bins[k]);
bins = [model.set(nb_items) for k in range(nb_bins)]
model.constraint(model.partition(bins))
std::vector<HxExpression> bins(nbBins);
for (int k = 0; k < nbBins; ++k)
bins[k] = model.setVar(nbItems);
model.constraint(model.partition(bins.begin(), bins.end()));
HxExpression[] bins = new HxExpression[nbBins];
for (int k = 0; k < nbBins; ++k)
bins[k] = model.Set(nbItems);
model.Constraint(model.Partition(bins));
HxExpression[] bins = new HxExpression[nbBins];
for (int k = 0; k < nbBins; ++k)
bins[k] = model.setVar(nbItems);
model.constraint(model.partition(bins));
These three operators are particularly useful when items are to be assigned to one of several groups or containers. Each group will be represented by its own collection. For instance, the items may be tasks to be dispatched to one of several machines, or delivery locations to be serviced by one of several trucks.
The union operator applies to N collections and/or arrays, and returns the
unordered set of the values present in at least one operand. For example, the
following lines returns the unordered set of customers served by at least one
truck:
truck[i in 0...nbTrucks] <- list(nbCustomers);
servedCustomers <- union[i in 0...nbTrucks](truck[i]);
truck = [model.list(nb_customers) for i in range(nb_trucks)]
served_customers = model.union(truck)
std::vector<HxExpression> truck(nbTrucks);
for (int i = 0; i < nbTrucks; ++i)
truck[i] = model.listVar(nbCustomers);
HxExpression servedCustomers = model.union_();
servedCustomers.addOperands(truck.begin(), truck.end());
HxExpression[] truck = new HxExpression[nbTrucks];
for (int i = 0; i < nbTrucks; ++i)
truck[i] = model.List(nbCustomers);
HxExpression servedCustomers = model.Union();
for (int i = 0; i < nbTrucks; ++i)
servedCustomers.AddOperand(truck[i]);
HxExpression[] truck = new HxExpression[nbTrucks];
for (int i = 0; i < nbTrucks; ++i)
truck[i] = model.listVar(nbCustomers);
HxExpression servedCustomers = model.union();
for (int i = 0; i < nbTrucks; ++i)
servedCustomers.addOperand(truck[i]);
Modeling with sets¶
Set decision variables are very useful to model optimization problems with
assignment relationships. If a problem involves “containers” and “contained”
elements, with several elements inside each container, and a requirement that
every element belongs to at most, exactly, or at least one container, it can
generally be modeled using set decisions linked together by a disjoint,
partition, or cover constraint.
For example, the Bin Packing Problem (BPP)
illustrates the use of set decisions very well: each bin is modeled using a set
decision, and the elements in the set correspond to the indices of the items in
the bin. To ensure that each items belong to exactly one bin, the sets are
constrained to form a partition thanks to the partition operator.
Note
While an equivalent model can often be written by replacing each set
variable with n Booleans (x[i] equals 1 if element i is in the set,
0 if not), set decisions are generally preferred. Indeed, Hexaly Optimizer’s
large collection of dedicated set-based operators make the modeling more
straightforward. Furthermore, set decisions are higher-level decision
variables, and they provide more structural information for the solver,
leading to overall better performance.
Detailed packing examples using set variables are available in our code templates.
Modeling with lists¶
List decision variables are very useful to model optimization problems which
involve an ordering, or an assignment relationship combined with an ordering.
If a problem involves one or several “containers” with “contained” elements,
with several elements inside each container, an ordering on the elements in each
container, and a requirement that every element belongs to at most, exactly, or
at least one container, it can generally be modeled using list decisions linked
together by a disjoint, partition, or cover constraint.
Lists are often used to model the order of customers visited by the vehicles in
routing problems. For example, a pure Traveling Salesman Problem (TSP) is modeled
with a single list x with a constraint count(x) == n in order to ensure
that all cities are visited. For routing problems involving several vehicles,
such as the Capacitated Vehicle Routing Problem (CVRP),
the model associates one list decision to each vehicle, representing the
customers served by this vehicle.
Another typical use case for list decision variables is disjunctive scheduling. Scheduling problems are modeled using interval variables representing the time span of the tasks. These decisions allow easy access to the start and end times of the tasks. However, when modeling a disjunctive scheduling problem, such as the Job Shop Scheduling Problem (JSSP), is it generally also useful to know the order in which the tasks are processed, to be able to access the task in position i in the schedule, or the neighboring tasks of a given task. In this context, we use one list decision variable for each disjunctive resource, representing the order of the tasks scheduled on this resource.
Detailed routing and scheduling examples are available in our code templates.