AdventOfCode 2023 Day 23

Part One

走迷宫,限制就是遇到箭头,只能按照箭头的方向前进,此外不能重复走同一空格,求最长路径。

#.>.>.#
#.#v#.#
#.#.#..

走迷宫的话比较简单,注意加上两个限制条件即可

  • 前进方向和箭头方向冲突时,则无需继续
  • 不走回头路

最终SQL

WITH RECURSIVE maze AS (
  SELECT _row, row_number() over (partition by _row) as _col, pos
  FROM (
    SELECT row_number() over () as _row, unnest(regexp_split_to_array(line, '')) as pos
    FROM lance_input
  )t
),
path AS (
  SELECT 1 as _row, 2 as _col, '{}' :: integer[] as trail, true as valid

  UNION ALL

  SELECT t._row, t._col, 
         t.trail + (t._row * 1000 + t._col) as trail, 
         case when t.direction = 'E' and s.pos = '<' then false
              when t.direction = 'W' and s.pos = '>' then false
              when t.direction = 'N' and s.pos = 'v' then false
              when t.direction = 'S' and s.pos = '^' then false
              else true
         end as valid
  FROM (
    SELECT case when y.direction = 'N' then x._row - 1
                when y.direction = 'S' then x._row + 1
                else x._row
           end as _row,
           case when y.direction = 'E' then x._col + 1 
                when y.direction = 'W' then x._col - 1
                else x._col 
           end as _col,
           x.trail,
           y.direction
     FROM path x, (select unnest(regexp_split_to_array('ESWN', '')) as direction) y
     WHERE x.valid
   ) t 
   JOIN maze s
   ON t._row = s._row
   AND t._col = s._col
   AND s.pos != '#'
   AND t.trail # (t._row * 1000 + t._col) = 0
)
SELECT max(#trail) FROM path where _row = 141 and _col = 140;

Part Two

第二关去除了前进方向和箭头方向冲突的限制,再次求最长的路径。

穷举路径

使用第一关相同的方法,只是去除了限制。但是运行很久都未计算出最终结果,尝试先获取前1500W条的记录,看看分布的情况。

 step | count 
------+-------
 1645 |  7155
 1646 |  7147
 1647 |  7218
 1648 |  7203
 1649 |  7281
 1650 |  7261
 1651 |  7344
 1652 |  7317
 1653 |  4889

由于有太多的可能分支可以走,且没有约束条件(仅剩余不能重走的限制),因此越往后分支数越多,1600多步时,达到了7000多个分支,性能下降严重。而且1500W条的记录,才走到了1600多步,按照这个迷宫的设计,应该在1w步左右。

精简迷宫

迷宫中有很多条路都是无岔路的,因此一次可以在横向或者纵向方向上移动多步。首先,需要找出一行输入中,可以无岔路移动的最大距离。

CREATE OR REPLACE FUNCTION find_shortcut(line VARCHAR, pre_line VARCHAR, next_line VARCHAR)
RETURNS VARCHAR[] AS $$
DECLARE
    shortcut VARCHAR[];
    start_index INTEGER := 0;
    end_index INTEGER := 0;
BEGIN
    IF pre_line is null OR next_line is null THEN
      RETURN null;
    END IF;

    FOR i IN 1..length(line) LOOP
      IF substring(line, i, 1) != '#' THEN
        IF start_index = 0 THEN
          -- if next position is a wall, ignore this position
          IF substring(line, i+1, 1) != '#' THEN
            start_index := i;
            end_index := i;
          END IF;
        ELSE
          -- no other way to go
          CONTINUE WHEN substring(pre_line, i ,1) = '#' AND substring(next_line, i, 1) = '#' AND i < length(line);

          end_index := i;

          -- add to shortcut
          IF start_index != end_index THEN
            shortcut := shortcut || (start_index || ',' || end_index);
          END IF;

          -- if next position is not a wall, current position is a fork.
          IF substring(line, i+1, 1) != '#' THEN
            start_index := i;
            end_index := 1;
          ELSE
            start_index := 0;
            end_index := 0;
          END IF;
        END IF;
      END IF;
    END LOOP;

    RETURN shortcut;
     
END;
$$ LANGUAGE plpgsql;

通过上面的函数,就可以计算出一行输入中,可以无岔路移动的起点和终点。

postgres=# select find_shortcut('###.....#.>.>.###.#.###', '#######.#########.#.###', '###v#####.#v#.###.#.###');
      find_shortcut      
-------------------------
 {"4,8","10,12","12,14"}
(1 row)

接着就可以根据这个函数,计算出走迷宫的捷径,从而一次可以多走几步。

WITH RECURSIVE maze AS (
  SELECT _row, row_number() over (partition by _row) as _col, pos
  FROM (
    SELECT row_number() over () as _row, unnest(regexp_split_to_array(line, '')) as pos
    FROM lance_input
  )t
),
rows AS (
  SELECT _row, 
         lag(line) over (order by _row) as pre_line,
         lead(line) over (order by _row) as next_line,
         line
  FROM (
    SELECT _row, string_agg(pos, '' order by _col) as line
    FROM maze
    GROUP BY _row
  ) t
),
cols AS (
  SELECT _col, 
         lag(line) over (order by _col) as pre_line,
         lead(line) over (order by _col) as next_line,
         line
  FROM (
    SELECT _col, string_agg(pos, '' order by _row) as line
    FROM maze
    GROUP BY _col
  ) t
),
short_cut AS (
  SELECT _row as start_row, split_part(shortcut, ',', 1) :: integer as start_col,
         _row as finish_row, split_part(shortcut, ',', 2) :: integer as finish_col, 
         'E' as type
  FROM ( 
    SELECT _row, unnest(shortcuts) as shortcut
    FROM (
      SELECT _row, find_shortcut(line, pre_line, next_line) as shortcuts
      FROM rows
    ) t
    WHERE shortcuts IS NOT NULL
  ) s

  UNION ALL

  SELECT _row as start_row, split_part(shortcut, ',', 2) :: integer as start_col,
         _row as finish_row, split_part(shortcut, ',', 1) :: integer as finish_col, 
         'W' as type
  FROM ( 
    SELECT _row, unnest(shortcuts) as shortcut
    FROM (
      SELECT _row, find_shortcut(line, pre_line, next_line) as shortcuts
      FROM rows
    ) t
    WHERE shortcuts IS NOT NULL
  ) s

  UNION ALL

  SELECT split_part(shortcut, ',', 1) :: integer as start_row, _col as start_col,
         split_part(shortcut, ',', 2) :: integer as finish_row, _col as finish_col, 
         'S' as type
  FROM ( 
    SELECT _col, unnest(shortcuts) as shortcut
    FROM (
      SELECT _col, find_shortcut(line, pre_line, next_line) as shortcuts
      FROM cols
    ) t
    WHERE shortcuts IS NOT NULL
  ) s

  UNION ALL

  SELECT split_part(shortcut, ',', 2) :: integer as start_row, _col as start_col,
         split_part(shortcut, ',', 1) :: integer as finish_row, _col as finish_col, 
         'N' as type
  FROM ( 
    SELECT _col, unnest(shortcuts) as shortcut
    FROM (
      SELECT _col, find_shortcut(line, pre_line, next_line) as shortcuts
      FROM cols
    ) t
    WHERE shortcuts IS NOT NULL
  ) s
),
path AS (
  SELECT 1 as _row, 2 as _col, '{}' :: integer[] as trail, 0 as steps

  UNION ALL

  SELECT t._row :: integer, t._col :: integer, 
         t.trail + (t._row * 1000 + t._col)::integer as trail, steps::integer
  FROM (
    SELECT case when z.type IS NOT NULL then z.finish_row
                when y.direction = 'N' then x._row - 1
                when y.direction = 'S' then x._row + 1
                else x._row
           end as _row,
           case when z.type IS NOT NULL then z.finish_col
                when y.direction = 'E' then x._col + 1 
                when y.direction = 'W' then x._col - 1
                else x._col 
           end as _col,
           x.trail,
           case when z.type IS NOT NULL then x.steps + abs(z.finish_row - z.start_row) + abs(z.finish_col - z.start_col)
                else x.steps + 1
           end as steps
     FROM path x 
     JOIN (select unnest(regexp_split_to_array('ESWN', '')) as direction) y
     ON 1 = 1
     LEFT JOIN short_cut z
     ON x._row = z.start_row
     AND x._col = z.start_col
     AND y.direction = z.type
   ) t 
   JOIN maze s
   ON t._row = s._row
   AND t._col = s._col
   AND s.pos != '#'
   AND t.trail # (t._row * 1000 + t._col)::integer = 0
)
SELECT max(steps) from path where _row = 141 and _col = 140;

但是,运行速度仍然很慢,分析后发现,虽然迭代次数少了,但是分支数仍然很多。其实和之前的做法差别不大,迭代次数少了,但是一次迭代的步数多了,相应的分支数仍然很多。而且需要将走过的路径记录下来,因此耗费的存储也比较大。

  iter | count 
------+-------
  519 | 12615
  520 | 12771
  521 | 12955
  522 | 13139
  523 | 13321
  524 | 13516
  525 | 13741
  526 | 13952
  527 | 14184
  528 | 14398
  529 | 14613

分岔路口之间距离

前面记录的是水平或者垂直方向上的捷径,其实我们只需要关注分岔路口之间的距离即可,中途无论水平方向还是垂直方向的移动,只要中途没有分岔,都无需关注,仅计算出分岔口之间的距离。

分岔路口的计算方式,就是根据前面的short_cut,计算出某个点出发的分支数。2个分支代表是一个90度弯或起点终点,3个分支代表三岔路口,4个分支代表四岔路口。在计算的时候,还需要将起点和终点加进去。

    SELECT start_row as _row, start_col as _col
    FROM short_cut
    GROUP BY start_row, start_col
    HAVING COUNT(*) > 2
    UNION ALL
    SELECT 1, 2
    UNION ALL
    SELECT 141, 140

通过递归查询,计算出这些岔路口之间的距离

short_route AS (
  SELECT s.start_row, s.start_col, t.finish_row, t.finish_col,
         abs(t.finish_row - s.start_row) + abs(t.finish_col - s.start_col) as distance,
         case when p._row is not null then false else true end as valid,
         '{}' ::integer[] || (t.start_row * 1000 + t.start_col)::integer || (t.finish_row * 1000 + t.finish_col)::integer as trail
  FROM (
    SELECT start_row, start_col
    FROM short_cut
    GROUP BY start_row, start_col
    HAVING COUNT(*) > 2
    UNION ALL
    SELECT 1, 2
    UNION ALL
    SELECT 141, 140
  ) s 
  JOIN short_cut t
  ON s.start_row = t.start_row
  AND s.start_col = t.start_col
  LEFT JOIN (
    SELECT start_row as _row, start_col as _col
    FROM short_cut
    GROUP BY start_row, start_col
    HAVING COUNT(*) > 2
    UNION ALL
    SELECT 1, 2
    UNION ALL
    SELECT 141, 140
  ) p
  ON t.finish_row = p._row
  AND t.finish_col = p._col

  UNION ALL

  SELECT s.start_row, s.start_col, t.finish_row, t.finish_col,
         s.distance + abs(t.finish_row - s.finish_row) + abs(t.finish_col - s.finish_col) as distance,
         case when p._row is not null then false else true end as valid,
         s.trail || (t.finish_row * 1000 + t.finish_col)::integer as trail
  FROM short_route s
  JOIN short_cut t
  ON s.finish_row = t.start_row
  AND s.finish_col = t.start_col
  AND s.valid
  AND s.trail # (t.finish_row * 1000 + t.finish_col)::integer = 0
  LEFT JOIN (
    SELECT start_row as _row, start_col as _col
    FROM short_cut
    GROUP BY start_row, start_col
    HAVING COUNT(*) > 2
    UNION ALL
    SELECT 1, 2
    UNION ALL
    SELECT 141, 140
  ) p
  ON t.finish_row = p._row
  AND t.finish_col = p._col
)

 start_row | start_col | finish_row | finish_col | distance 
-----------+-----------+------------+------------+----------
        20 |        16 |          8 |         38 |      158
        20 |        16 |         38 |         10 |      164
        20 |        16 |          1 |          2 |      177
(3 rows)

最终SQL

有了这个分岔路口之间的距离数据后,计算就比较简单了,最终耗时100秒左右。

path AS (
  SELECT 1 as _row, 2 as _col, 
         '{}' :: integer[] || 1002 as trail, 0 as steps

  UNION ALL

  SELECT y.finish_row :: integer,
         y.finish_col :: integer,
         x.trail || (y.finish_row * 1000 + y.finish_col)::integer,
         (x.steps + y.distance) :: integer
   FROM path x 
   JOIN short_route y
   ON x._row = y.start_row
   AND x._col = y.start_col
   AND y.valid = false
   AND x.trail # (y.finish_row * 1000 + y.finish_col)::integer = 0
)
SELECT max(steps) from path where _row = 141 and _col = 140;


 max  
------
 6334
(1 row)

Time: 100189.855 ms (01:40.190)

发表评论