AdventOfCode 2022 Day 9

Part One

绳子有两个绳结:head和tail,head移动的时候,tail跟随移动。求几步移动操作后,tail的移动轨迹

R 4
U 4
L 3
D 1

假设初始起点的坐标为(100, 100),记录head和tail的坐标变化。其中head的坐标变化容易记录,只需要依据每一步的移动,简单对坐标进行加减即可。

CASE WHEN x.direction = 'U' THEN z.head_row - y.id 
     WHEN x.direction = 'D' THEN z.head_row + y.id
     ELSE z.head_row
 END as head_row,
CASE WHEN x.direction = 'L' THEN z.head_col - y.id 
     WHEN x.direction = 'R' THEN z.head_col + y.id
     ELSE z.head_col
 END as head_col

而tail的坐标变化,需要依据和tail的距离来决定,只有当距离超过范围(绳子已经“完全绷紧了”)时,才需要移动

CASE WHEN (abs(tail_row - head_row) > 1) OR (abs(tail_col - head_col) > 1) THEN
       CASE WHEN direction = 'U' THEN head_row + 1 
            WHEN direction = 'D' THEN head_row - 1
            ELSE head_row
        END
     ELSE tail_row
 END as tail_row,
CASE WHEN (abs(tail_row - head_row) > 1) OR (abs(tail_col - head_col) > 1) THEN
       CASE WHEN direction = 'L' THEN head_col + 1 
            WHEN direction = 'R' THEN head_col - 1
            ELSE head_col
        END
     ELSE tail_col
 END as tail_col

最终的SQL如下

with recursive origin AS (
  SELECT row_number() over () as rn, split_part(line, ' ', 1) as direction, split_part(line, ' ', 2) :: integer as steps
  FROM lance_input
), seq AS (
  SELECT 1 as id
  UNION ALL
  SELECT id + 1 as id
  FROM seq
), positions AS (
  SELECT 100 as head_row, 100 as head_col, 100 as tail_row, 100 as tail_col, 0 :: bigint as rn, 0 :: bigint as steps

  UNION ALL

  SELECT *
  FROM (
    WITH last_record AS (
      SELECT head_row, head_col, tail_row, tail_col, rn, steps
      FROM (
        SELECT head_row, head_col, tail_row, tail_col, rn, steps, 
               row_number() over(order by steps desc) as row_num
        FROM positions
      ) t
      WHERE row_num = 1
    )
    SELECT head_row, head_col, 
           CASE WHEN (abs(tail_row - head_row) > 1) OR (abs(tail_col - head_col) > 1) THEN
                  CASE WHEN direction = 'U' THEN head_row + 1 
                       WHEN direction = 'D' THEN head_row - 1
                       ELSE head_row
                   END
                ELSE tail_row
            END as tail_row,
           CASE WHEN (abs(tail_row - head_row) > 1) OR (abs(tail_col - head_col) > 1) THEN
                  CASE WHEN direction = 'L' THEN head_col + 1 
                       WHEN direction = 'R' THEN head_col - 1
                       ELSE head_col
                   END
                ELSE tail_col
            END as tail_col,
           rn, steps
    FROM (
      SELECT CASE WHEN x.direction = 'U' THEN z.head_row - y.id 
                  WHEN x.direction = 'D' THEN z.head_row + y.id
                  ELSE z.head_row
              END as head_row,
             CASE WHEN x.direction = 'L' THEN z.head_col - y.id 
                  WHEN x.direction = 'R' THEN z.head_col + y.id
                  ELSE z.head_col
              END as head_col,
             z.tail_row,
             z.tail_col,
             x.rn as rn,
             z.steps + y.id as steps,
             x.direction
      FROM origin x, 
           (select id from seq limit (select max(steps) from origin)) y,
           last_record z
      WHERE x.rn = z.rn + 1
      AND y.id <= x.steps
    ) t
  ) t
) 
select count(distinct tail_row || ',' || tail_col) From positions;

Part Two

绳子不再只有head和tail,而是变成了10个绳结,求出最后一个绳结的移动轨迹。

head - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - tail

先分析下,现在变成了10个绳结,head移动的时候,会通过连带触发后续绳结的移动。因此,需要计算每一个绳结的移动轨迹,才能确定tail的移动轨迹。

因此这里每移动一步,需要迭代10次运算,才能确定tail的移动轨迹。在这10次运算中,head的运算完全根据输入的方向来确定,后续绳结的运算完全根据前一个绳结的位置来确定。

拆解移动步数

将移动步数拆解到每一步,某个方向上行动多少步,就产生多少条记录。

postgres=# with recursive origin AS (
postgres(#   SELECT row_number() over () as rn, split_part(line, ' ', 1) as direction, split_part(line, ' ', 2) :: integer as steps
postgres(#   FROM lance_input
postgres(# ), seq AS (
postgres(#   SELECT 1 as id
postgres(#   UNION ALL
postgres(#   SELECT id + 1 as id
postgres(#   FROM seq
postgres(# ), moves AS (
postgres(#   SELECT row_number() over () as step, x.direction
postgres(#   FROM origin x
postgres(#   JOIN (select id from seq limit (select max(steps) from origin)) y
postgres(#   ON y.id <= x.steps
postgres(# ) 
postgres-# SELECT * FROM moves limit 10;
 step | direction 
------+-----------
    1 | D
    2 | U
    3 | U
    4 | R
    5 | R
    6 | U
    7 | R
    8 | U
    9 | U
   10 | L
(10 rows)

每一步10次运算

每一步需要10次运算,可以通过10次子查询,并且将10个绳结的坐标打宽来实现,不过这样可读性较差。也可以用纵表的方式,通过每个绳结1条记录来实现。

初始化10个绳结的坐标,step为当前步数,knot_seq为绳结的序号,current_knot为当前处理的绳结序号

SELECT 100 as _row, 100 as _col, seq.id as knot_seq, 0 :: bigint as step, 10 as current_knot
FROM seq limit 10

再通过CTE来记录下上一步的坐标

    WITH ps AS (
      SELECT * FROM positions
    ), pre_position AS (
      SELECT _row, _col, current_knot as knot
      FROM ps
      WHERE knot_seq = current_knot
      LIMIT 1
    )

每一步迭代,将current_knot持续递增,直到knot=10的时候,执行下一步的移动,再继续下一轮10次运算。因此,每一步移动会生成100条记录。这里的终止条件,就是处理到了第10个knot,且moves中所有的移动走遍历完毕。

   SELECT  ps.knot_seq,
           coalesce(moves.step, ps.step) as step,
           ps.current_knot % 10 + 1 as current_knot 
   FROM ps
    LEFT JOIN moves
    ON ps.current_knot = 10
    AND ps.step + 1 = moves.step
    JOIN pre_position pre
    ON 1 = 1
    WHERE NOT (ps.current_knot = 10 AND moves.step is null)

绳结坐标计算

knot_seq=1,即head,其坐标完全根据moves中的移动方向来确定

CASE WHEN moves.direction = 'U' THEN ps._row - 1 
     WHEN moves.direction = 'D' THEN ps._row + 1
     ELSE ps._row
END as _row,
CASE WHEN moves.direction = 'L' THEN ps._col - 1 
     WHEN moves.direction = 'R' THEN ps._col + 1
     ELSE ps._col
END as _col

而其他的绳结,则完全根据前一个绳结的坐标来确定。当距离过大时,进行必要的移动

CASE WHEN pre._row < ps._row - 1 THEN pre._row + 1
     WHEN pre._row > ps._row + 1 THEN pre._row - 1
     WHEN pre._col < ps._col - 1 THEN pre._row
     WHEN pre._col > ps._col + 1 THEN pre._row
     ELSE ps._row
END as _row,
CASE WHEN pre._col < ps._col - 1 THEN pre._col + 1
     WHEN pre._col > ps._col + 1 THEN pre._col - 1
     WHEN pre._row < ps._row - 1 THEN pre._col
     WHEN pre._row > ps._row + 1 THEN pre._col
     ELSE ps._col
END as _col

当前处理的不是自己的时候,则保持不变即可

最终SQL

with recursive origin AS (
  SELECT row_number() over () as rn, split_part(line, ' ', 1) as direction, split_part(line, ' ', 2) :: integer as steps
  FROM lance_input
), seq AS (
  SELECT 1 as id
  UNION ALL
  SELECT id + 1 as id
  FROM seq
), moves AS (
  SELECT row_number() over () as step, x.direction
  FROM origin x
  JOIN (select id from seq limit (select max(steps) from origin)) y
  ON y.id <= x.steps
), positions AS (
  SELECT *
  FROM (
    SELECT 100 as _row, 100 as _col, seq.id as knot_seq, 0 :: bigint as step, 10 as current_knot
    FROM seq limit 10
  ) t

  UNION ALL

  SELECT *
  FROM (
    WITH ps AS (
      SELECT * FROM positions
    ), pre_position AS (
      SELECT _row, _col, current_knot as knot
      FROM ps
      WHERE knot_seq = current_knot
      LIMIT 1
    )

    SELECT CASE WHEN moves.step is not null AND knot_seq = 1 THEN
                  CASE WHEN moves.direction = 'U' THEN ps._row - 1 
                       WHEN moves.direction = 'D' THEN ps._row + 1
                       ELSE ps._row
                  END
                WHEN moves.step is not null AND knot_seq != 1 THEN ps._row
                WHEN moves.step is null AND knot_seq = pre.knot + 1 THEN 
                  CASE WHEN pre._row < ps._row - 1 THEN pre._row + 1
                       WHEN pre._row > ps._row + 1 THEN pre._row - 1
                       WHEN pre._col < ps._col - 1 THEN pre._row
                       WHEN pre._col > ps._col + 1 THEN pre._row
                       ELSE ps._row
                   END
                WHEN moves.step is null AND knot_seq != pre.knot + 1 THEN ps._row
            END as _row,
           CASE WHEN moves.step is not null AND knot_seq = 1 THEN
                  CASE WHEN moves.direction = 'L' THEN ps._col - 1 
                       WHEN moves.direction = 'R' THEN ps._col + 1
                       ELSE ps._col
                  END
                WHEN moves.step is not null AND knot_seq != 1 THEN ps._col
                WHEN moves.step is null AND knot_seq = pre.knot + 1 THEN
                  CASE WHEN pre._col < ps._col - 1 THEN pre._col + 1
                       WHEN pre._col > ps._col + 1 THEN pre._col - 1
                       WHEN pre._row < ps._row - 1 THEN pre._col
                       WHEN pre._row > ps._row + 1 THEN pre._col
                       ELSE ps._col
                   END
                WHEN moves.step is null AND knot_seq != pre.knot + 1 THEN ps._col
            END as _col,
           ps.knot_seq,
           coalesce(moves.step, ps.step) as step,
           ps.current_knot % 10 + 1 as current_knot
    FROM ps
    LEFT JOIN moves
    ON ps.current_knot = 10
    AND ps.step + 1 = moves.step
    JOIN pre_position pre
    ON 1 = 1
    WHERE NOT (ps.current_knot = 10 AND moves.step is null)
  ) t
) 
SELECT count(distinct _row || ',' || _col) FROM positions where knot_seq = 10 and current_knot = 10;

发表评论