3. Structured costs¶
The sum of chapter 2 weighs every term against the others: a longer tour is acceptable when some other term improves enough. Some problems do not work that way. A constraint must be satisfied before the objective counts; two objectives have no exchange rate; a component measures several numbers at once. This chapter builds those costs, from the same components.
Choosing an expression¶
Choose an expression that matches how you want to compare solutions:
| Expression | Cost |
|---|---|
cost::sum(t1, ..., tn) |
Σ wᵢ · costᵢ over arithmetic costs; child * w gives a term its weight (default 1) |
cost::in_order(c1, ..., cn) |
a cost::lexicographic cost: compared child by child |
cost::hard_soft(hard, soft) |
a cost::hierarchical cost: hard has strict priority over soft |
cost::objectives(c1, ..., cn) |
a cost::pareto cost: one value per objective, compared by Pareto dominance |
cost::apply(f, c1, ..., cn) |
f(cost₁, ..., costₙ), for anything else |
An expression nests: a branch of one is a component or another expression, so the structure and the weights stay together. A hard/soft model is:
auto sm = el::solution_manager<TimetableManager>()
| el::cost::hard_soft(
el::cost::sum(el::component<Conflicts>(),
el::component<Unavailability>()),
el::cost::sum(el::component<Compactness>() * 2,
el::component<Preferences>() * 5));
- Each branch has its own weights, named after its path: here
cost.hard.weightsandcost.soft.weights(see Configuration). cost::sumadds numbers. A component with a domain value is turned into one withcost::apply, for examplecost::apply([](const Load& l) { return l.overload; }, component<Capacity>()); Domain values below is about those components.solvers::two_stage(first, second)(see Solvers) evaluates only the components of thehardbranch in its first stage.
Costs compared by priority¶
cost::in_order and cost::hard_soft build the two types of
<easylocal/cost.hpp> that compare values by priority:
cost::lexicographic<Ts...>compares its values in order: the second one matters only between costs whose first one is equal, and so on;cost::hierarchical<Hard, Soft>gives the hard branch strict priority over the soft one: no improvement of the soft cost can make up for a worse hard cost. Each branch is compared with itsoperator<=>, or else with<and==alone; the hierarchical cost then has all the comparisons,<=included.
For separate objectives, cost::objectives builds a third type,
cost::pareto, compared by Pareto dominance. This supports searches that
return a front of non-dominated solutions (see Cost).
You rarely need to spell the types: cost::in_order and cost::hard_soft
deduce them from the components. Here are two ways to combine MaxEdge and
TourLength:
// Lexicographic: the longest edge first; between tours with the same
// longest edge, the shorter one.
auto bottleneck_sm = el::solution_manager<TourManager>()
| el::cost::in_order(el::component<MaxEdge>(), el::component<TourLength>());
// Hierarchical: edges longer than 8 are a violation, measured by how much
// the longest edge exceeds 8 (hard); the tour length is the objective (soft).
auto bounded_sm = el::solution_manager<TourManager>()
| el::cost::hard_soft(
el::cost::apply(
[](double longest) { return std::max(0.0, longest - 8.0); },
el::component<MaxEdge>()),
el::component<TourLength>());
bottleneck_smhas acost::lexicographic<double, double>cost: a tour is better if its longest edge is shorter, and between two tours with the same longest edge, the shorter tour is better. The order of the children is the order of comparison.bounded_smtreats edges longer than 8 as a constraint. Its hard cost measures the violation:cost::applyturns the value ofMaxEdgeinto the excess over 8, zero when every edge is short enough. Its soft cost is the length. The result is acost::hierarchical<double, double>: a tour with a smaller excess is always better, whatever its length. The bound is a literal here; chapter 10 makes it a parameter.
Both recipes work with the runner from chapter 6 and the 2-opt neighborhood from chapters 4 and 5. Read a lexicographic result by position and a hierarchical result by branch:
auto bottleneck =
(el::make_runner<runners::FirstImprovement>(runners::FirstImprovementParameters{})
| bottleneck_sm | nhe)
.bind(tsp);
const auto by_edge = bottleneck.run(bottleneck.initial_solution());
// A lexicographic cost is read by position, a hierarchical one by branch.
const double longest_edge = by_edge.cost.get<0>();
const double length_after_edge = by_edge.cost.get<1>();
auto bounded =
(el::make_runner<runners::FirstImprovement>(runners::FirstImprovementParameters{})
| bounded_sm | nhe)
.bind(tsp);
const auto within_bound = bounded.run(bounded.initial_solution());
const double excess = within_bound.cost.hard();
const double length_within_bound = within_bound.cost.soft();
On the five-city instance, both find a tour of length 26 with a longest edge
of 8. Here the shortest tour also minimizes the longest edge, so the
constraint does not increase the length. For a nested example, see
examples/assignment/cost.hpp: its cost::hard_soft has a lexicographic hard
branch computed with cost::apply.
Domain values¶
Sometimes a component measures more than one number at once. LongEdges
counts the edges longer than 6 and adds up how much they exceed 6, in a single
pass over the tour:
// A domain value: the edges longer than 6, as a total excess and a count.
struct LongEdges
{
double excess{}; // the total length beyond 6 of the long edges
std::size_t count{}; // how many edges are longer than 6
// Needed only when LongEdges is itself the cost: tours are then compared
// by excess first and by count between equal excesses.
auto operator<=>(const LongEdges&) const = default;
};
class LongEdgesComponent : public easylocal::input_base<Tsp>
{
public:
using input_base::input_base;
LongEdges evaluate(const Tour& tour) const
{
const auto n = tour.order.size();
LongEdges value;
for (std::size_t k = 0; k < n; ++k)
{
const auto edge = input().distance[tour.order[k]][tour.order[(k + 1) % n]];
if (edge > 6.0)
{
value.excess += edge - 6.0;
++value.count;
}
}
return value;
}
};
The component returns the struct, and the framework stores it like any other value. What the struct needs depends on how the cost expression uses it:
// The struct is the cost: it is compared with its operator<=>.
auto long_edges_sm =
el::solution_manager<TourManager>() | el::component<LongEdgesComponent>();
// A function turns it into a number: here the excess is a hard cost.
auto excess_sm = el::solution_manager<TourManager>()
| el::cost::hard_soft(
el::cost::apply(
[](const LongEdges& value) { return value.excess; },
el::component<LongEdgesComponent>()),
el::component<TourLength>());
- The struct is the cost (
long_edges_sm). Algorithms compare costs, so the struct must be ordered:better,equivalentandbetter_or_equivalentdefault to<,==and<=(see Cost semantics below). The defaultedoperator<=>provides them all, comparing the members in declaration order: firstexcess, thencount. Without it, the runner does not compile. - A function turns it into a number (
excess_sm).cost::applymaps the struct to itsexcess, a number that is then the hard cost. Here the struct needs no operators: the algorithms compare the numberscost::applyreturns.cost::applyis also how a domain value enters acost::sum, which adds numbers only. - With a delta cost component (chapter 5), the delta is added to the value, so
the struct also needs
operator+(Value, Delta).
With First Improvement and the 2-opt moves, long_edges_sm stops at an
excess of 3 over two edges. That is a local optimum: no 2-opt move improves
it, although a tour with the same excess on a single edge exists.
excess_sm finds the minimum excess, 3, and then the shortest tour with it,
of length 26.
Cost semantics¶
Algorithms compare costs through better, equivalent and
better_or_equivalent. These use the cost type's <, == and <= by
default. To customize all three, give the function at the root cost::apply
a compare(a, b) member returning std::partial_ordering; less means that
a is better.
Simulated Annealing also reads cost::delta and treats a negative delta as an
improvement. Negating a profit is therefore the simplest way to maximize it.
For floating-point costs, wrap the expression in cost::approximately(...)
to treat differences within a tolerance as equivalent.
See also¶
- Cost: cost components, cost expressions, cost models and semantics.
Next steps¶
Chapter 4 defines the moves.