AdventOfCode 2022 Day 22

Part One

按照指令走迷宫,遇到墙就停止,求出最后的终点位置。指令中的方向仅有L和R。

10R5L5R10L4R5L5

预处理

先将指令进行拆分,拆分为具体方向以及行动距离

WITH recursive origin AS (
  SELECT row_number() over () as rn, line
  FROM lance_input
), path AS (
  SELECT row_number() over () as seq,
         substring(move, 1, 1) as direction,
         substring(move, 2) :: INTEGER as steps
  FROM (
    SELECT string_to_table('E' || replace(replace(line, 'R', '|R'), 'L', '|L'), '|') as move
    FROM origin
    WHERE rn = (select max(rn) from origin)
  ) t
)

 seq  | direction | steps 
------+-----------+-------
    1 | E         |    13
    2 | L         |     2
    3 | R         |    45
    4 | R         |    31
    5 | R         |    34
    6 | R         |    41
    7 | L         |    14
    8 | L         |    26
    9 | L         |    47

因为有大量的空窗,需要确定每一行列的起始位置和结束位置。同时,也找出每一行列的墙的所有位置,方便后续的处理

maps AS (
  SELECT rn :: INTEGER as _row, x.idx :: INTEGER as _col, x.word as tile
  FROM origin, regexp_split_to_table(line, '') with ordinality as x(word, idx)
  WHERE rn <= (select max(rn) from origin) - 2
), column_stats AS (
  SELECT _col, 
         COUNT(CASE WHEN tile = '#' THEN 1 END) as wall_cnt, 
         MIN(CASE WHEN tile IN ('#', '.') THEN _row END) as min_pos,
         MAX(CASE WHEN tile IN ('#', '.') THEN _row END) as max_pos,
         ARRAY_AGG(_row) FILTER (WHERE tile = '#') as wall_arr
  FROM maps
  GROUP BY _col
), row_stats AS (
  SELECT _row,
         COUNT(CASE WHEN tile = '#' THEN 1 END) as wall_cnt, 
         MIN(CASE WHEN tile IN ('#', '.') THEN _col END) as min_pos,
         MAX(CASE WHEN tile IN ('#', '.') THEN _col END) as max_pos,
         ARRAY_AGG(_col) FILTER (WHERE tile = '#') as wall_arr
  FROM maps
  GROUP BY _row
)

迷宫行走

如果某一行列完全没有墙,则无需做复杂的计算,直接移动到最终位置即可

WHEN direction = 'N' AND c.wall_cnt = 0 THEN 
    CASE WHEN start_row >= steps + c.min_pos THEN start_row - steps 
         ELSE start_row - steps - c.min_pos + c.max_pos + 1
     END
WHEN direction = 'S' AND c.wall_cnt = 0 THEN 
    CASE WHEN start_row <= c.max_pos - steps THEN start_row + steps 
         ELSE start_row + steps + c.min_pos - c.max_pos - 1
     END
WHEN direction = 'W' AND c.wall_cnt = 0 THEN 
    CASE WHEN start_col >= steps + r.min_pos THEN start_col - steps 
         ELSE start_col - steps - r.min_pos + r.max_pos + 1
     END
WHEN direction = 'E' AND c.wall_cnt = 0 THEN 
    CASE WHEN start_col <= r.max_pos - steps THEN start_col + steps 
         ELSE start_col + steps + r.min_pos - r.max_pos - 1
     END

如果有墙,则需要判断遇墙停止的问题,通过一个函数来判断最终位置

CREATE OR REPLACE FUNCTION move_tile(start_pos INTEGER, new_pos INTEGER, wall_arr INTEGER[], min_pos INTEGER, max_pos INTEGER)
RETURNS INTEGER AS $$
DECLARE
  wall_pos INTEGER;
BEGIN
    IF new_pos > max_pos AND start_pos > wall_arr[#wall_arr] THEN
      IF wall_arr[1] = min_pos THEN
        RETURN max_pos;
      END IF;
      start_pos := min_pos;
      new_pos := min_pos + (new_pos - max_pos) - 1;
    ELSIF new_pos < min_pos AND start_pos < wall_arr[1] THEN
      IF wall_arr[#wall_arr] = max_pos THEN
        RETURN min_pos;
      END IF;
      start_pos := max_pos;
      new_pos := max_pos - (min_pos - new_pos) + 1; 
    END IF;

    FOR i IN 1..#wall_arr LOOP
      IF start_pos < new_pos THEN
        wall_pos := wall_arr[i];
      ELSE
        wall_pos := wall_arr[(#wall_arr) - i + 1];
      END IF;

      IF wall_pos > start_pos AND wall_pos <= new_pos THEN
        RETURN wall_pos - 1;
      ELSIF wall_pos < start_pos AND wall_pos >= new_pos THEN
        RETURN wall_pos + 1; 
      END IF;
    END LOOP;

    RETURN new_pos;
 
END;
$$ LANGUAGE plpgsql;

行走路径

walkthrough AS (
  SELECT _row, min(CASE WHEN tile = '.' THEN _col END) _col, 0 as move_seq, null as last_direction, 0 as steps
  FROM maps
  WHERE _row = 1
  GROUP BY _row

  UNION ALL

  SELECT CASE WHEN direction IN ('E', 'W') THEN start_row
              WHEN direction = 'N' AND c.wall_cnt = 0 THEN 
                   CASE WHEN start_row >= steps + c.min_pos THEN start_row - steps 
                        ELSE start_row - steps - c.min_pos + c.max_pos + 1
                    END
              WHEN direction = 'S' AND c.wall_cnt = 0 THEN 
                   CASE WHEN start_row <= c.max_pos - steps THEN start_row + steps 
                        ELSE start_row + steps + c.min_pos - c.max_pos - 1
                    END
              WHEN direction = 'N' THEN move_tile(start_row, start_row - steps, c.wall_arr, c.min_pos, c.max_pos)
              WHEN direction = 'S' THEN move_tile(start_row, start_row + steps, c.wall_arr, c.min_pos, c.max_pos)
          END as _row,
         CASE WHEN direction IN ('N', 'S') THEN start_col
              WHEN direction = 'W' AND c.wall_cnt = 0 THEN 
                   CASE WHEN start_col >= steps + r.min_pos THEN start_col - steps 
                        ELSE start_col - steps - r.min_pos + r.max_pos + 1
                    END
              WHEN direction = 'E' AND c.wall_cnt = 0 THEN 
                   CASE WHEN start_col <= r.max_pos - steps THEN start_col + steps 
                        ELSE start_col + steps + r.min_pos - r.max_pos - 1
                    END
              WHEN direction = 'W' THEN move_tile(start_col, start_col - steps, r.wall_arr, r.min_pos, r.max_pos)
              WHEN direction = 'E' THEN move_tile(start_col, start_col + steps, r.wall_arr, r.min_pos, r.max_pos)
          END as _col,
         move_seq,
         direction,
         steps
  FROM (
  SELECT x._row as start_row,
         x._col as start_col,
         x.move_seq + 1 as move_seq,
         CASE WHEN x.last_direction IS NULL THEN y.direction
              WHEN y.direction = 'L' AND x.last_direction = 'E' THEN 'N'
              WHEN y.direction = 'L' AND x.last_direction = 'S' THEN 'E'
              WHEN y.direction = 'L' AND x.last_direction = 'W' THEN 'S'
              WHEN y.direction = 'L' AND x.last_direction = 'N' THEN 'W'
              WHEN y.direction = 'R' AND x.last_direction = 'E' THEN 'S'
              WHEN y.direction = 'R' AND x.last_direction = 'S' THEN 'W'
              WHEN y.direction = 'R' AND x.last_direction = 'W' THEN 'N'
              WHEN y.direction = 'R' AND x.last_direction = 'N' THEN 'E'
          END as direction,
         y.steps
  FROM walkthrough x, path y
  WHERE y.seq = x.move_seq + 1
  ) t 
  JOIN column_stats c
  ON t.start_col = c._col
  JOIN row_stats r
  ON t.start_row = r._row
)
SELECT *
FROM walkthrough;

Part Two

从二维平面上升到三维空间,之前的迷宫其实是一个立方体展开的6个面,求新的终点位置。

从二维上升到三维,那么三维空间的移动在二维空间,那就是一个个的“虫洞”。通过虫洞后,不仅位置发生了改变,而且移动方向也发生了变化。

寻找虫洞

        1111
        1111
        1111
        1111
222233334444
222233334444
222233334444
222233334444
        55556666
        55556666
        55556666
        55556666

以示例中的立方体为例。立方体总共有12条边,例子中,(1,4)(2,3)(3,4)(4,5)(5,6)这5条边已经相邻了,剩余的7条边在平面上并不相邻,因此需要构建虫洞。

剩余的7条边

1,3
SELECT _row as start_row, 
       _col as start_col, 
       'W' as start_direction,
       5 as end_row,
       4 + _row as end_col,
       'S' as end_direction
FROM maps
WHERE _row between 1 and 4
AND _col = 9

3,5
SELECT _row as start_row, 
       _col as start_col, 
       'S' as start_direction,
       17 - _col as end_row,
       9 as end_col,
       'E' as end_direction
FROM maps
WHERE _row = 8
AND _col between 5 and 8

4,6
SELECT _row as start_row, 
       _col as start_col, 
       'E' as start_direction,
       9 as end_row,
       21 - _row as end_col,
       'S' as end_direction
FROM maps
WHERE _row between 5 and 8
AND _col = 12

1,2
SELECT _row as start_row, 
       _col as start_col, 
       'N' as start_direction,
       5 as end_row,
       13 - _col as end_col,
       'S' as end_direction
FROM maps
WHERE _row = 1
AND _col between 9 and 12

2,5
SELECT _row as start_row, 
       _col as start_col, 
       'S' as start_direction,
       12 as end_row,
       13 - _col as end_col,
       'N' as end_direction
FROM maps
WHERE _row = 8
AND _col between 1 and 4

1,6
SELECT _row as start_row, 
       _col as start_col, 
       'E' as start_direction,
       13 - _row as end_row,
       16 as end_col,
       'W' as end_direction
FROM maps
WHERE _row between 1 and 4
AND _col = 12

2,6
SELECT _row as start_row, 
       _col as start_col, 
       'W' as start_direction,
       12 as end_row,
       21 - _row as end_col,
       'N' as end_direction
FROM maps
WHERE _row between 5 and 8
AND _col = 1

虫洞是双向的,因此反方向的记录也要构建出来

wormholes AS (
  SELECT start_row, start_col, start_direction, end_row, end_col, end_direction
  FROM wormhole

  UNION ALL

  SELECT end_row as start_row, 
         end_col as start_col, 
         CASE when end_direction = 'E' THEN 'W' 
              when end_direction = 'W' THEN 'E' 
              when end_direction = 'N' THEN 'S' 
              when end_direction = 'S' THEN 'N' 
          END as start_direction, 
         start_row as end_row, 
         start_col as end_col, 
         CASE when start_direction = 'E' THEN 'W' 
              when start_direction = 'W' THEN 'E' 
              when start_direction = 'N' THEN 'S' 
              when start_direction = 'S' THEN 'N' 
          END as end_direction
  FROM wormhole
)

仅单面行走

之前的行走函数,并没有考虑跨多个面的情况,这里就需要考虑仅在单个面上进行移动。只能移动到墙,或者移动到边缘。函数的返回结果,包含最终位置以及剩余步数。如果移动到边缘后,还剩余一些步数,这时就需要后续通过虫洞进入到另一面进行后续移动。

CREATE OR REPLACE FUNCTION move_tile_in_one_face(start_pos INTEGER, new_pos INTEGER, wall_arr INTEGER[], min_pos INTEGER, max_pos INTEGER)
RETURNS INTEGER[] AS $$
DECLARE
  wall_pos INTEGER;
BEGIN
    -- no walls
    IF wall_arr IS NULL THEN 
      IF new_pos > max_pos THEN
        return ARRAY[max_pos, new_pos - max_pos];
      ELSIF new_pos < min_pos THEN
        return ARRAY[min_pos, min_pos - new_pos];
      ELSE
        return ARRAY[new_pos, 0];
      END IF;
    END IF;

    -- with walls
    IF new_pos > max_pos AND start_pos > wall_arr[#wall_arr] THEN
      return ARRAY[max_pos, new_pos - max_pos];
    ELSIF new_pos < min_pos AND start_pos < wall_arr[1] THEN
      return ARRAY[min_pos, min_pos - new_pos];
    END IF;

    FOR i IN 1..#wall_arr LOOP
      IF start_pos < new_pos THEN
        wall_pos := wall_arr[i];
      ELSE
        wall_pos := wall_arr[(#wall_arr) - i + 1];
      END IF;

      IF wall_pos > start_pos AND wall_pos <= new_pos THEN
        RETURN ARRAY[wall_pos - 1, 0];
      ELSIF wall_pos < start_pos AND wall_pos >= new_pos THEN
        RETURN ARRAY[wall_pos + 1, 0]; 
      END IF;
    END LOOP;

    RETURN ARRAY[new_pos, 0];
 
END;
$$ LANGUAGE plpgsql;

行走路径

因此,指令中的每一步,可能要分多次进行。每次移动到边缘,通过虫洞后,再进行后续移动,直到剩余步数为0。这里的意思,就是先判断当前位置是否需要通过虫洞继续进行,如果虫洞后面是墙则不再移动,剩余步数清零;如果虫洞后面不是墙,则剩余步数减一后,移动到新的位置上。

        SELECT CASE WHEN z.tile != '#' THEN y.end_row
                    ELSE x._row
                END as _row,
               CASE WHEN z.tile != '#' THEN y.end_col
                    ELSE x._col
                END as _col,
               x.move_seq,
               CASE WHEN z.tile != '#' THEN y.end_direction
                    ELSE x.last_direction
                END as last_direction,
               CASE WHEN z.tile = '#' THEN 0
                    WHEN z.tile != '#' AND x.remain_steps > 0 THEN x.remain_steps - 1 
                    ELSE x.remain_steps
                END as remain_steps
        FROM walkthrough x
        LEFT JOIN wormholes y
        ON x._row = y.start_row
        AND x._col = y.start_col
        AND x.last_direction = y.start_direction
        AND x.remain_steps > 0
        LEFT JOIN maps z
        ON y.end_row = z._row
        AND y.end_col = z._col

后续,则按照前一步类似的方法,持续进行即可

walkthrough AS (
  SELECT _row, min(CASE WHEN tile = '.' THEN _col END) _col, 0 as move_seq, null as last_direction, 0 as remain_steps
  FROM maps
  WHERE _row = 1
  GROUP BY _row

  UNION ALL

  SELECT CASE WHEN direction IN ('E', 'W') THEN start_row
              ELSE move_result[1]
          END as _row,
         CASE WHEN direction IN ('N', 'S') THEN start_col
              ELSE move_result[1]
          END as _col,
         move_seq,
         direction,
         move_result[2] as remain_steps
  FROM (
    SELECT start_row, start_col, move_seq, direction, 
           CASE WHEN direction = 'N' THEN move_tile_in_one_face(start_row, start_row - steps, c.wall_arr, c.min_pos, c.max_pos)
                WHEN direction = 'S' THEN move_tile_in_one_face(start_row, start_row + steps, c.wall_arr, c.min_pos, c.max_pos)
                WHEN direction = 'W' THEN move_tile_in_one_face(start_col, start_col - steps, r.wall_arr, r.min_pos, r.max_pos)
                WHEN direction = 'E' THEN move_tile_in_one_face(start_col, start_col + steps, r.wall_arr, r.min_pos, r.max_pos)
            END as move_result
    FROM (
      SELECT x._row as start_row,
             x._col as start_col,
             case when remain_steps = 0 then x.move_seq + 1 else x.move_seq end as move_seq,
             CASE WHEN x.remain_steps > 0 THEN x.last_direction
                  WHEN x.last_direction IS NULL THEN y.direction
                  WHEN y.direction = 'L' AND x.last_direction = 'E' THEN 'N'
                  WHEN y.direction = 'L' AND x.last_direction = 'S' THEN 'E'
                  WHEN y.direction = 'L' AND x.last_direction = 'W' THEN 'S'
                  WHEN y.direction = 'L' AND x.last_direction = 'N' THEN 'W'
                  WHEN y.direction = 'R' AND x.last_direction = 'E' THEN 'S'
                  WHEN y.direction = 'R' AND x.last_direction = 'S' THEN 'W'
                  WHEN y.direction = 'R' AND x.last_direction = 'W' THEN 'N'
                  WHEN y.direction = 'R' AND x.last_direction = 'N' THEN 'E'
              END as direction,
             case when x.remain_steps = 0 then y.steps else x.remain_steps end as steps
      FROM (
        SELECT CASE WHEN z.tile != '#' THEN y.end_row
                    ELSE x._row
                END as _row,
               CASE WHEN z.tile != '#' THEN y.end_col
                    ELSE x._col
                END as _col,
               x.move_seq,
               CASE WHEN z.tile != '#' THEN y.end_direction
                    ELSE x.last_direction
                END as last_direction,
               CASE WHEN z.tile = '#' THEN 0
                    WHEN z.tile != '#' AND x.remain_steps > 0 THEN x.remain_steps - 1 
                    ELSE x.remain_steps
                END as remain_steps
        FROM walkthrough x
        LEFT JOIN wormholes y
        ON x._row = y.start_row
        AND x._col = y.start_col
        AND x.last_direction = y.start_direction
        AND x.remain_steps > 0
        LEFT JOIN maps z
        ON y.end_row = z._row
        AND y.end_col = z._col
      ) x, path y
      WHERE y.seq = case when remain_steps = 0 then x.move_seq + 1 else x.move_seq end
    ) t
    JOIN column_stats c
    ON t.start_col = c._col
    JOIN row_stats r
    ON t.start_row = r._row
  ) t
)

立方体展开

实际题目与示例略有不同,实际的题目展开的形状不同,对应的虫洞也需要重新计算。

    11112222
    11112222
    11112222
    11112222
    3333
    3333
    3333
    3333
55554444
55554444
55554444
55554444
6666
6666
6666
6666

需要构建虫洞的7条边为
2,3
2,4
2,6
1,5
1,6
3,5
4,6

发表评论