https://adventofcode.com/2021/day/23

For day 23, we needed to solve a lazy shrimp problem.

We had a room and hallway setup (view in monospace font):

sample=:];._2 {{)n
#############
#...........#
###B#C#B#D###
  #A#D#C#A#
  #########
}}

Ignore spaces and '#' (wall) characters. The important locations are
marked with . or letters ABCD.

Actually, also we partially ignore the '.' characters which align
vertically here with a letter -- those locations are significant but
must always be left open.

The horizontal section of dots was the 'hallway' and the sections
initially containing letters were 'rooms'.

The task involved moving each of the letters (the shrimp) into the
rooms so that the shrimp would be arranged in alphabetical order, left
to right with these constraints (stated slightly differently in the
puzzle):

(1) Each shrimp could only move twice
(2) each move must change the shrimp's horizontal position
(3) no shrimp could pass through another shrimp
(4) shrimp cannot break through the walls

also each move cost of distance * 10^'ABCD' i. shrimpId (we use
manhattan distance here, because of rule 4).

So the task was to to do all this while minimizing the total energy
cost expended by the shrimp.

For part B, the task description was the same except that the rooms
were extended with this for the middle of each room (with these
additional shrimp):

  #D#C#B#A#
  #D#B#A#C#

My initial approach here was to solve the puzzle by hand and write
some code to tally the costs. That got me part A, but part B was too
complicated for me to see the solution. It was not until a few days
ago that I managed to solve part B (but I did use my initial solution
to help debug my part B implementation (running the solver against the
part A data set)).

To actually solve this, I wound up using an A* algorithm. For part B
this still took about half an hour (and part A took significantly
longer, ironically).

A* is a highly serial approach, but has the advantage of eliminating
significant parts of some search spaces. Instead of doing a breadth
first or depth first search (which could be parallelized), A* uses a
queue of partial results which are sorted in lowest cost order.

I also maintained a "lowest cost" array for each visited state, and
when revisiting such a state I only proceed if my new cost is lower
than my old cost.

Also,

(a) I precalculated for each shrimp, a characterization of its two
moves (a coordinate pair, with a 0 marking the undetermined part of
each move), and

(b) I assigned the total cost of a shrimp's moves to the move into the
hallway (this meant that the second move was always cheaper than any
first move, which reflects the issue that blocking the hallway would
tend to drive up the costs of other moves).

Other than that, my code is nothing special -- actually, it's rather
verbose. But because a complete test of the code is time consuming and
I did not have any further use for it, I've left it in its crude "just
got it working" state. I apologize for the lack of readability and
lack of relevant abstraction here, but I'm feeling lazy.

(That said, the code echos every intermediate state as it's being
processed, and would probably run faster if I removed that.)

The first part -- a23 -- sets up a batch of globals to be used by the solver:

a23=: {{
   bounds=: $y
   roomsN=: _4]\I.,y e.'ABCD'
   hallN =: I.,(y='.')>"1 +./y e.'ABCD'
   doorsN=: ({.roomsN) - {:$y NB. room positions in hallN
   boardC=: ((#hallN)#'.'),(,y) ([-.-.)'ABCD'
   locationsN=: ($y)#:hallN,,roomsN
   assert. locationsN -:&# boardC
   mask=: (-.y e.'# ')*(1~:i.#y)+./'#'=2{y
   show=: (+./"1 mask)#($mask)$(,mask)#inv ([ assert@('literal'-:datatype))@]
   move =: +./\.'ABCD'~:"1 roomsN {,y
   roomsM=: ($roomsN)$(-#,roomsN){.i.#boardC
   agenda=: move <@(,.&0,0,.|.)@#"1&|: roomsM
   done=: ((#hallN)#'.'),,($roomsN)$'ABCD'
   seen=: ,:done [ costs=: ,999999999
   boardC 0 allpaths agenda
}}

The solver is... not pretty. Conceptually, various parts of this could
have been abstracted, resulting in tighter code.

allpaths=: {{
  mqueue=: 0#,m
  xqueue=: 0#,:x
  yqueue=: 0#,:y
  pqueue=: i.0
  whilst.#mqueue do.
    if. x e. seen do.
      ndx=.seen i. x
      if. m < ndx{costs do.
        costs=: m ndx} costs
        if.0=ndx do.
          B=: b=. mqueue < {.costs
          mqueue=: b#mqueue
          xqueue=: b#xqueue
          yqueue=: b#yqueue
          pqueue=: b#pqueue
        end.
      else.
        M=: m=. {.mqueue
        X=: x=. {.xqueue
        Y=: y=. {.yqueue
        if.0=ndx do.
          B=: b=. }.mqueue < {.costs
        else.
          B=: b=. 1
        end.
        mqueue=: b#}.mqueue
        xqueue=: b#}.xqueue
        yqueue=: b#}.yqueue
        pqueue=: b#}.pqueue
        echo x
        continue.
      end.
    else.
      seen=:seen,x
      costs=:costs,m
    end.
    echo (":(show x);'  ';":,.m,{.costs)-."1' '-.~,":2#<' '
    displaced=. (#hallN){.x
    free=. '.'=displaced
    pending=.i.0 0
    phase=.i.0
    plan=.i.0 0
    paths=. i.0
    for_room.'ABCD' do.
      t=. room_index {:: y
      if.0=#t do.continue. end.
      for_next. (1 (0}) </\ 0={."1 t) #t do.
        todo=. (<t -. next) room_index} y
        sign=. *hallN-room_index{doorsN
        if. 0={:next do. NB. move into hallN
          open=. (*/\.(_1=sign)#free),*/\(1=sign)#free
          piece=. ({. next){x
          assert. piece e. 'ABCD'
          moves=.(#~ openmask */ .<: open"_) next+"1]0,.I.open
          index=. 'ABCD'i.piece
          cost=. 10^index
          prices=. cost*+/"1|-/"2 moves{locationsN
          door=. index{doorsN
          seq=. +/piece=(#hallN){.x
          row=. 1+(+/0={."1 index{::y)-+/piece=(#hallN){.x
          dist=. +/"1|(({:"1 moves){locationsN)-"1 row,}.bounds#:door
          values=. dist*cost
          plan=.plan,(prices+values),.moves
        elseif.'.'=({:next){x do. NB. move from hallN
          'L R'=. sign </. displaced
          opts=.((room={:L-.'.')#L i:room),(#L)+(room={.R-.'.')#R i.room
          if.#opts do.
            'breakpoint here'
          end.
          moves=. (#~ openmask */ .<: (displaced e.'.',room)"_) next+"1]opts,.0
          plan=.plan,0,.moves
        else.
          continue.
        end.
        phase=. phase,(#moves)#*{. next
        pending=.pending,(#moves)#,:todo
      end.
    end.
    energy=. m+{."1 plan
    nexts=. (_2{."1 plan) |.@:{`[`]}"1 x
    B=: b=. energy < {.costs
    mqueue=: mqueue,~ b#energy
    xqueue=: xqueue,~ b#nexts
    yqueue=: yqueue,~ b#pending
    pqueue=: pqueue,~ b#{."1 plan
    if.-.0={.pqueue do.
      order=.  /: pqueue
      mqueue=: order{mqueue
      xqueue=: order{xqueue
      yqueue=: order{yqueue
      pqueue=: order{pqueue
    end.
    M=:m=. {.mqueue
    X=:x=. {.xqueue
    Y=:y=. {.yqueue
    mqueue=: }.mqueue
    xqueue=: }.xqueue
    yqueue=: }.yqueue
    pqueue=: }.pqueue
  end.
}}

The one bit of abstraction that I used here was:

thru=: [ ~.@, <. + i.@(+*)@-~

openmask=:{{
  (({:bounds)|hallN)&e.@thru/"1 y { {:"1 locationsN
}}

This is used in determining whether moves are possible.

Anyways, when this runs, it displays the board (what I think the
puzzle called the shell) positions while it's running, and for
plausible layouts displays the current and final cost to along with
each board position. Discarded states are instead displayed in a
compressed format.

When it finishes, this implementation displays the board in its final
state along with the minimal cost. But that's also available in
{.costs if you wanted to remove the echo from the code.

For part B, I used the same code, with the required extra content added:

b23=: {{
  a23 (3{.y),extension,3}.y
}}

extension=: ];._2 {{)n
###D#C#B#A###
###D#B#A#C###
}}

If I could find a production use for this kind of thing, I would
probably not implement it in J, except maybe this kind of prototype.
These kinds of highly serial algorithms favor compiled code, and we
currently do not have a J compiler.

FYI,

-- 
Raul
----------------------------------------------------------------------
For information about J forums see http://www.jsoftware.com/forums.htm

Reply via email to