ETOOBUSY š minimal blogging for the impatient
Autobiographical numbers
TL;DR
Letās look at an example of using ConstraintSolver.pm, introduced in previous post More constraint programming.
But firstā¦ a confession: Iām quite not satisfied with the code that Iām going to showā¦ but time is a limited resource, so Iāll stick with it because it shows how to use ConstraintSolver.pm on a real problem and gets the job done.
Autobiographical numbers?
It sort of started from video Can you solve the Leonardo da Vinci riddle? - Tanya Khovanova, but not really. Itās complicated š
Sure, the name autobiographical numbers comes from that video. But full credits have to be given to the stellar Coursera online course Discrete Optimization, by Professor Pascal Van Hentenryck and Dr. Carleton Coffrin. Itās taking more time than I anticipated, but itās totally worth the effort.
In particular, class 6 CP 6 - redundant constraints, magic series, market split in week 3 deals about magic seriesā¦ which are exactly the same concept.
So, in a nutshell, assume you have an array of $N$ elements, indexed starting
from 0
:
0 1 2 3 4 n-2 n-1
+---+---+---+---+---+ ... +---+---+
| | | | | | | | |
+---+---+---+---+---+ ... +---+---+
Each slot in the array is supposed to contain a non-negative integer number, representing the number of times that the slotās index appears in the array itself. I guess Douglas Richard Hofstadter would feel at home.
Example? Letās consider $N = 4$, this would be a solution:
0 1 2 3
+---+---+---+---+
| 1 | 2 | 1 | 0 |
+---+---+---+---+
As you can see:
- slot
0
contains a $1$, which means that there is exactly one $0$ the array. This is true indeed, itās in slot3
; - slot
1
contains a $2$, which is consistent with the fact that $1$ appears exactly two times in the array (in slot0
and in slot2
) - you get the idea for slots
2
and3
š
Letās solve this!
Letās use ConstraintSolver.pm then:
1 sub autobiographical_numbers ($n) {
2 my $solution = [
3 map {
4 +{map { $_ => 1 } 0 .. $n - 1}
5 } 1 .. $n
6 ];
7 my @constraints = map { main->can('constraint_' . $_) }
8 (qw< basic total_sum weighted_sum last_is_zero >);
9 my $state = solve_by_constraints(
10 constraints => \@constraints,
11 is_done => \&is_done,
12 search_factory => \&explore,
13 start => {solution => $solution},
14 logger => ($ENV{VERBOSE} ? \&printout : undef),
15 );
16 } ## end sub autobiographical_numbers ($n)
The function solve_by_constraints
does the overall orchestration, but the
heavy lifting is up to us: think about a data structure, as well as
providing suitable callback functions for the different stages of the
constraints programming search.
Our solution starts as an Array of Arrays where each slot contains the candidates for the specific slot. Itās initialized with all integers between $0$ and $N-1$ (both included) because there can be no less than $0$ and no more tha $N-1$, right?
The data structure is a hash reference passed via start
(line 13). Itās a
reference to a hash with a few keys inside. Itās initialized with
solution
, an array reference containing hashes whose keys are the possible
candidate values to occupy the slot. This choice is probably something that
should be revisited, because it makes it easier to look for specific
elements, but it makes it very bad to e.g. look for the minimum or the
maximum element. As anticipated, Iām not exactly proud of this code!
Accessories
The logger functions is quite simple:
1 sub printout ($phase, $status, $exception = undef) {
2 if ($phase eq 'backtrack') {
3 if ($@) {
4 (my $e = $@) =~ s{\sat\s.*?\sline\s[0-9]+\.\s+\z}{}mxs;
5 $phase = "backtrack[$e]";
6 }
7 else {
8 $phase = 'explore';
9 }
10 }
11 say $phase, ' => ', encode_json [
12 map {
13 my @candidates = sort { $a <=> $b } keys $_->%*;
14 @candidates > 1 ? \@candidates
15 : @candidates > 0 ? 0 + $candidates[0]
16 : '[]'
17 } $status->{solution}->@*
18 ];
19 } ## end sub printout
Each hash reference is transformed back to a sorted array before being put in the output array, whose reference is encoded with json and then printed out. The logging function receives the step as the first argument, so we print that too; additionally, when backtracking, the function also receives the exception that was thrown, so we make sure to include it (line 5) or to mark this as a simple search start (line 8).
Function is_done
tells us whether our quest is complete or not:
sub is_done ($status) {
return scalar(grep { keys $_->%* > 1 } $status->{solution}->@*) == 0;
}
It basically counts the number of slots where we donāt have a decision yet (i.e. where there is more than one alternative), and gives green light only when all slots are decided.
Exploring
The search_factory
function is supposed to give out a sub that iterates
over possible alternative decisions at a specific level in our depth search.
In this case, each level addresses one slot.
1 sub explore ($status) {
2 my $solution = dclone($status->{solution}); # our working solution
3 my $n = $solution->@*;
4
5 # this investigates a single slot...
6 my $slot_id = 0;
7 while ($slot_id < $n) {
8 last if keys $solution->[$slot_id]->%* > 1;
9 $slot_id++;
10 }
11 die 'wtf?!?' if $slot_id >= $n;
12 my @candidates = sort { $a <=> $b } keys $solution->[$slot_id]->%*;
13 my $amount;
14 return sub ($status) { # get "next" solution
15 return unless @candidates;
16 $amount = shift @candidates;
17 $status->{solution} = dclone($solution);
18 $status->{solution}[$slot_id] = {$amount => 1};
19 return 1;
20 };
21 } ## end sub explore ($status)
This is why line 14 returns a sub
. In the preparation phase (lines 2 to
13) we set up for a search in a suitable slot, i.e. a slot where we still
have to take a decision (hence the test in line 8). When this slot is found
in $slot_id
, we take all of its alternatives and populate @candidates
,
which we will use for iterating different alternatives inside the iterator
function.
At each step, we will first of all restore the status as in the beginning (line 17, to undo any pruning that was attempted in the previous step), then set the specific slot to a hash that has one single choice among the candidates (line 18).
Curious?
Weāre at the end of this post nowā¦ if youāre curious about the constraints, please hold on until the next post, or take a sneak peek at the repository.
Update fix code with latest changes, and add reference to repository.
So longā¦
Curious about the whole series? Here it is:
- Autobiographical numbers
- Autobiographical numbers constraints - basic
- Autobiographical numbers constraints - last is zero
- Autobiographical numbers constraints - weighted sum
- Autobiographical numbers constraints - luckier weighted sum
- Autobiographical numbers - step up
- Code repository
Comments? Please comment below!