ETOOBUSY 🚀 minimal blogging for the impatient
AoC 2016/11 - New successors
TL;DR
On with Advent of Code puzzle 11 from 2016: a new function for finding successors of a node.
The New representation forces us to update the successors_for
function to read the new state layout and, more importantly, produce new
states that adhere to the new convention.
While we’re at it, anyway, we’ll also chip in some enhancements. Hopefully.
Now buckle up, because it’s going to be a longish journey!
The function
The function implementing the new successors_for
is the following.
As we already introduced, it takes an input state and returns a list of
other states that can be reached from the input one with one allowed
move of the elevator.
1 sub successors_for ($state) {
2 my ($elevator, $generators, $microchips) =
3 $state->@{qw<elevator generators microchips>};
4 my $floor_start_mask = 0x01 << 8 * $elevator;
5 my @retval;
6 for my $ne ($elevator - 1, $elevator + 1) {
7 next unless 0 <= $ne && $ne <= 3;
8
9 # I can move (g), (m), (gg), (mm), (gm)*
10 # (gm)* means matching and only 1 move makes sense (prune others)
11 my $outer_mask = $floor_start_mask;
12 my $did_mixed = 0;
13 for my $outer_element (1 .. $state->{n_elements}) {
14 my @masks_prefix = ();
15 for my $type (qw< generators microchips >)
16 { # (g), (gg), (m), (mm)
17 if ($state->{$type} & $outer_mask) {
18 push @retval,
19 new_candidate($state, $ne, @masks_prefix, $outer_mask)
20 ; # (x)
21 my $inner_mask = $outer_mask << 1;
22 for my $inner_element (
23 $outer_element + 1 .. $state->{n_elements})
24 {
25 if ($state->{$type} & $inner_mask) {
26 push @retval,
27 new_candidate($state, $ne, @masks_prefix,
28 $outer_mask, $inner_mask); # (xx)
29 }
30 $inner_mask <<= 1;
31 } ## end for my $inner_element (...)
32 } ## end if ($state->{$type} & ...)
33 push @masks_prefix, 0, 0;
34 } ## end for my $type (qw< generators microchips >)
35 if ( !$did_mixed
36 && ($generators & $outer_mask)
37 && ($microchips & $outer_mask))
38 {
39 $did_mixed = 1;
40 push @retval,
41 new_candidate($state, $ne, $outer_mask, 0, $outer_mask, 0);
42 } ## end if (!$did_mixed && ($generators...))
43
44 $outer_mask <<= 1; # move to next position
45 } ## end for my $outer_element (...)
46 } ## end for my $ne ($elevator -...)
47 return @retval;
48 } ## end sub successors_for ($state)
Lines 2 and 3 extract the current relevant values from the input state to avoid having to do indirections on the input hash reference all the time.
Line 4 introduces a mask that marks the starting position for the
current floor. As you might remember, floors are represented by
individual octets in a 32-bits integer, with the fourth floor (indexed
with 0
) sitting at the least significant octet, and the first floor
(indexed with 3
) sitting at the most significant one. We keep this
variable only because we will need to reuse this starting value in line
11, possibly 2 times. So it’s not much the efficiency gain that we get
here, but more the readability (i.e. *let’s start from the beginning of
the source floor).
An observation is due at this moment about what movements are possible and/or meaningful (this is by no means efficient but it’s at least complete and correct). All of the following possible moves are of course subject to additional checking for feasibility (i.e. not frying up any microchip in the process):
- we might do a single-step move of each item in the source floor. This
is what the comments refer to as
(g)
and(m)
; - we might move two same-typed elements, i.e. two generators (
(gg)
) or two microchips ((mm)
). - we might move one pair of same-element items, i.e. one generator
and one matching microchip (
(gm)*
). If the starting floor has more than one such pair, it only makes sense to consider one of them (all other alternatives would be isomorphic and a total waste of resources). This is why variable$did_mixed
exists: it tracks whether we considered such a pair and skips any following ones for a possible elevator move.
These are the only three possibilities. We are explicitly cutting out
the possibility to move two different-element items of different type,
e.g. a plutonium
generator together with a curium
microchip, because
it would always lead to frying the microchip.
Not convinced?
If the starting state is correct, and the microchip is still alive even in presence of the other-element generator in the same floor, it means that the microchip is protected by the corresponding generator in that starting floor. Hence, if we move the microchip away from a floor where it is protected by its corresponding generator, and we also make sure that we bring a generator of a different element together with it… we’re going to fry it. Hence, we know beforehand it’s a bad idea.
So, we’re left with the five alternatives described above.
The loop in line 13 (ending online 45, which is pretty long) goes
through each slot in the floors. We leverage the count of number of
elements here, but we might just as well use 8
, possibly at the
expense of some efficiency (but avoiding a bugCOUGH).
At each slot, we might either decide to focus on microchips or on
generators (we’ll save the mixed case for later at line 35, don’t
worry), so we iterate over generators
and microchips
(line 15) to
consider them. At this point, anyway, it only makes sense to go on if
the specific element type is present in the current floor, which is
established by the $outer_mask
. “Outer” here is in reference to the
fact that we’re considering a outer loop (lines 15 to 34).
The first thing we do is to consider the single-movement alternantive,
i.e. (g)
or (m)
(depending on $type
). Function new_candidate
here is supposed to return either a new neighbor of the current state,
or an empty list if the candidate is not good; hence, the push
in line
18 might actually not push anything in the @retval
array used to
collect all return values.
Now it’s time to consider the (gg)
or (mm)
pair (again, depending on
$type
), which is why we have to setup an inner loop (lines 22
through 31). To avoid duplications, we only iterate after the current
slot, so we initialize the $inner_mask
to the following slot with
respect to $outer_mask
(following means doing a left shift, line
21).
Then, again, for the pair to be valid we have to check that the slot in
the inner loop is filled in (check at line 25) and in case repeat the
call to new_candidate
with the same contact we discussed above (i.e.
it returns either a valid new neighbor, or the empty list).
A little word on the new_candidate
function is due here; its calling
interface is the following:
my @new_items = new_candidate(
$starting_state, # this has elevator, microchips, ...
$target_elevator_index, # where the elevator is heading to
$generator_1_mask,
$generator_2_mask,
$microchip_1_mask,
$microchip_2_mask,
);
The four *_mask
variables are either false (in which case they are
ignored) or contain a valid mask to detect which item to move for the
specific type. At any given call, only one or two of these masks are
different from a false value, accounting for one or two-item
movements.
For this reason, the @masks_prefix
is initially empty, making sure
that $outer_mask
in line 19 and $outer_mask
/$inner_mask
in line 28
refer to generators.
After the first loop in line 15 ends, it’s time for microchips, right?
This is why at the very end of the first loop (line 33) we set
@masks_prefix
to (0, 0)
: in this way, the following round will put
$outer_mask
at line 19 and $outer_mask
/$inner_mask
at line 29 in
the positional places for the microchip masks. Maybe it’s not that
elegant… but it works.
Line 35 addresses the mixed case where we might move one generator
and one microchip together. As we already saw, the must be of the
same type (which is why lines 36 and 37 do checks against the same
$outer_mask
, i.e. the same slot) and we must not have done this
attempt before for this elevator movement (so we check against
$did_mixed
, initially set to a false value in line 12 and then set
to a true value in line 39).
Lines 40 and 41 are two old friends at this point… you will notice
that we’re calling new_candidate
again, this time with the mask for
one generator and one microchip.
Note that we set $did_mixed
to 1
in line 39 independently of whether
the mixed move is allowed or not. Considering that we’re moving two
same-element items, either a pair can be moved, or no pair can be moved.
Line 44, at last, sets the $outer_mask
on the next item by shifting
our *aiming bit” one position to the left.
After all these loops, array @retval
collected all possible and
admissible neighbors… so we’re only left to return them in line 47.
Whew, what a ride!
Crafing and checking a new candidate
The new_candidate
function follows. As a matter of fact, it should
have probably been called new_neighbor
instead, because this funciton
not only builds up a candidate, but it also tests it for being
feasible. Anyway.
1 sub new_candidate ($state, $ne, @masks) {
2 my $target_shift = 8 * ($ne - $state->{elevator}); # shift: <<
3 my %retval = (elevator => $ne, n_elements => $state->{n_elements});
4 for my $type (qw< generators microchips >) {
5 my $v = $state->{$type};
6 for (1 .. 2) {
7 my $mask = shift @masks or next;
8 $v = ($v & ~$mask) | ($mask << $target_shift);
9 }
10 $retval{$type} = $v;
11 } ## end for my $type (qw< generators microchips >)
12
13 # now check if the new candidate is viable
14 state $mf4 = 0xFF;
15 state $mf3 = $mf4 << 8;
16 state $mf2 = $mf3 << 8;
17 state $mf1 = $mf2 << 8;
18 my $generators = $retval{generators};
19 my $naked_microchips = $retval{microchips} & ~$generators;
20 return
21 if ((($naked_microchips & $mf1) && ($generators & $mf1))
22 || (($naked_microchips & $mf2) && ($generators & $mf2))
23 || (($naked_microchips & $mf3) && ($generators & $mf3))
24 || (($naked_microchips & $mf4) && ($generators & $mf4)));
25 return \%retval;
26 } ## end sub new_candidate
As we already explained, we can get four masks in, but only two of them will be actually filled. This is anyway how we use this function, but this is in no way enforced here. Ah… the joy of very specialized functions!
The input masks allow us to pinpoint a bit in the starting floor, but
we also need to know the right mask/bit for the landing floor. The
shift will be by 8 bits (because each floor is an octet), while the
direction will be given by the difference between the landing floor
identifier and the source floor identifier. This should explain line 2;
the note refers to the fact that the sign of $target_shift
is such
that it works properly when used with a left shift.
Did you know it? Doing a left bit shift by a negative amount actually yields a right shift by the corresponding positive amount! Amazing!
Varible %retval
holds our candidate, and is initialized with the same
n_elements
as the source one (this is an invariant) and with the
elevator in the target floor. This all happens at line 3.
The loop in lines 4 through 11 considers each input mask and does an
action only for true ones (line 7, note the or next
to skip false
values). Line 8 is an obfuscated way to say that we set the bit in the
starting floor to 0
($v & ~$mask
) and we set the corresponding bit
on the landing floor to 1
(with | ($mask << $target_shift)
). This
effectively moves the item across the two floors.
When we arrive at line 12, our candidate %retval
is built, but is it
feasible? Or would it fry any microchip?
A correct, modular approach here would require us to encapsulate the check in its own function. Alas, my Perl-fu is a bit rusty, and I’m not sure that tail call optimization has been implemented at all, so we’ll spare the cost of calling another sub here and just put the check in the same function. At the end of the day… it’s the only place in the code where we need this check, so it’s not a big deal.
Well, maybe it’s a big aesthetic deal.
But we have a puzzle to solve here, not to do decorations!
The check in lines 14 to 24 makes sure that unfeasible states are
pruned out, returning the empty list (because we’re calling
new_candidate
in list context) if applicable (line 20).
Lines 14 to 17 declare some handy masks to isolate each single floor.
This is needed because the check for “lonely microchip in the same floor
as a different-element generator) has to be done floor by floor. as you
can see, these masks are “all bits high” (0xFF
in line 14), properly
shifted (from floor 4 down to floor 1).
We’re testing for generators in our candidate, so our $generators
in
line 18 is initialized to that value.
Also, we’re looking for naked microchips, i.e. microchips that have no
corresponding generator in the same floor. This is calculated at line
19, by doing an &
bitwse operation between the microchips themselves
and the inverse of the generators. This is the right way to detect a
naked microchip, because…
- if the microchip bit is
0
, the&
operation will yield0
(so, no naked microchip in that bit position); - otherwise, if the corresponding generator bit is
1
, inverting it will yield0
and the&
operation will yield0
as well. This is correct, because the microchip is protected by the generator and is not naked; - otherwise, we have a
1
in the microchip and a0
in the generator, the output of the expression is1
in that bit position… and it marks a naked microchip.
So… it seems that those years studying electronic engineering finally gave some result, yay!
Now that we located all naked microchips, it’s time to do the test floor by floor. In each floor, the frying condition is that we have naked microchips in that floor and (boolean and) we also have generators in that floor. Naked microchips in a floor without generators are fine!
So, in the first floor:
$naked_microchips & $mf1
tells us whether the floor has naked microchips or not, while$generators & $mf1
tells us whether the floor has generators or not.
Doing a boolean and between these two conditions does the trick for
this floor. Applying the same approach to the other floors (using their
respective mask $mf2
, $mf3
, and $mf4
) does the trick overall.
If we manage to get past the dreaded test in lines 21 to 24… congratulations, we have a feasible new state, and we can happily return it in line 24, yay!
This was an intense ride…
… and we’re stopping it here.
No, I’ll save running this for the next post, MBWAHAHAHAAHAH!
OK, OK.
I’m not this bad.
If you’re curious, you can try this local version here and see it by yourself if it works or not.
Until then… stay safe!