ETOOBUSY 🚀 minimal blogging for the impatient
Aquarium - exploiting redundant constraints
TL;DR
Redundancy is often associated to overhead and waste of time, but in constraint programming redundant constraints, when cooperating, can save a lot.
In the last post about aquarium (Aquarium - search differently), we left with a working solution that is capable of solving even the most insidious puzzles on the website (also knowns as the Special Monthly). The time was not too bad, but nothing to brag about either:
$ time ./run.sh 07-search-differently/ monthly.aqp >/dev/null && printf 'OK\n'
real 1m2.964s
user 1m1.764s
sys 0m0.484s
OK
We can do better with some little redundancy. As you can guess, the code is available in stage 8.
Redundant constraints
Up to when we only used picky constraints, adding more redundant stuff had really no point: if our initial set of constraint is complete, why add more?
Things changed dramatically when we taught our constraints to be a bit more cooperative and help us doing some pruning, though. At this point, everything that can help us get rid of unknown cells without guessing is a big time saver!
Empty spaces are just like inverted water
The key insight we can exploit at this point is that whatever happens to water, it also happens to empty spaces when looking in reverse vertical order.
Water must have one single level inside an aquarium? Well, so we have for empty spaces. Water cannot have empty spaces below in the same aquarium? It’s the same as saying that empty spaces cannot have water above.
Hence, we can do a little of copy-and-paste, followed by a little of changing here and there, to add the following cooperating constraint:
1 sub adjust_empty_level ($puzzle) {
2 my ($n, $field, $status) = $puzzle->@{qw< n field status >};
3 my $n_changes = 0;
4 for my $i (reverse 0 .. $n - 1) { # iterate rows from bottom to top
5 my %expected;
6
7 # first sweep: adjust vertical emptying, set expectations
8 for my $j (0 .. $n - 1) {
9 my $id = $field->[$i][$j];
10 my $st = $status->[$i][$j];
11
12 # vertical condition from before-last row on...
13 if (($i < $n - 1) && ($id == $field->[$i + 1][$j])) {
14 if ($st > $status->[$i + 1][$j]) { # possible mismatch?
15 if ($st == 0) { # current cell is *unknown*, relax!
16 $st = $status->[$i][$j] = -1; # mark empty
17 $n_changes++;
18 }
19 elsif ($status->[$i - 1][$j] != 0) {
20 die "wrong vertical leveling for aquarium $id\n";
21 }
22 }
23 }
24
25 $expected{$id} ||= $st; # change only if unknown
26 }
27
28 # second sweep: adjust horizontal emptying based on expectations
29 for my $j (0 .. $n - 1) {
30 my $id = $field->[$i][$j];
31 my $st = $status->[$i][$j];
32
33 if ($st == 0) {
34 if ($expected{$id}) {
35 $st = $status->[$i][$j] = $expected{$id};
36 $n_changes++;
37 }
38 }
39 elsif ($st != $expected{$id}) {
40 die "wrong horizontal leveling for aquarium $id\n"
41 }
42 } ## end for my $j (0 .. $n - 1)
43 } ## end for my $i (0 .. $n - 1)
44 return $n_changes;
45 } ## end sub assert_water_level ($puzzle)
This must be added to our list of constraints, of course (addition in line 6):
1 sub apply_constraints ($puzzle) {
2 my $changes = -1;
3 while ($changes != 0) {
4 $changes = 0;
5 $changes += adjust_water_level($puzzle);
6 $changes += adjust_empty_level($puzzle);
7 $changes += adjust_by_col($puzzle);
8 $changes += adjust_by_row($puzzle);
9 }
10 }
How does it behave?
And now, it’s time for testing:
$ time ./run.sh 08-redundancy 15x15-hard.aqp >/dev/null && printf 'OK\n\n'
real 0m0.184s
user 0m0.156s
sys 0m0.004s
OK
$ time ./run.sh 08-redundancy daily.aqp >/dev/null && printf 'OK\n\n'
real 0m0.379s
user 0m0.356s
sys 0m0.008s
OK
$ time ./run.sh 08-redundancy weekly.aqp >/dev/null && printf 'OK\n\n'
real 0m0.315s
user 0m0.304s
sys 0m0.004s
OK
$ time ./run.sh 08-redundancy monthly.aqp >/dev/null && printf 'OK\n\n'
real 0m1.083s
user 0m1.064s
sys 0m0.008s
OK
Now, I guess, we can say that we’re fine. Of course… everybody has its own parameters, please tell us in the comments if you go beyond!!!