AdventOfCode 2022 Day 17

Part One

俄罗斯方块的游戏,模拟方块下落时被左右移动,并最终计算2022个方块后的高度。一共5种方块

####

.#.
###
.#.

..#
..#
###

#
#
#
#

##
##

存储结构

为了尽量提升速度,无需插入到表中,只在内存中进行计算。最主要的运算,其实就是方块移动到新位置后,是否与已有的方块冲突。如果无法继续下落时,则方块确定位置,开始掉落下一个方块。

一共只有7列位置可以选择,那么可以用7个字符串来存储已有方块占用的位置信息,比如”1|4″代表y轴的[1-4]被占用,再将多个位置汇集到一起,比如”1|4, 6|7″。

每个方块都有确定的覆盖位置,假如一个方块最左上的坐标为(pos_x, pos_y),那么可以计算出方块的覆盖位置信息

    IF shape_type = '-' THEN
      origin_array[pos_x] := pos_y || '|' || pos_y;
      origin_array[pos_x + 1] := pos_y || '|' || pos_y;
      origin_array[pos_x + 2] := pos_y || '|' || pos_y;
      origin_array[pos_x + 3] := pos_y || '|' || pos_y;
    END IF;

    IF shape_type = '+' THEN
      origin_array[pos_x] := (pos_y - 1) || '|' || (pos_y - 1);
      origin_array[pos_x + 1] := (pos_y - 2) || '|' || pos_y;
      origin_array[pos_x + 2] := (pos_y - 1) || '|' || (pos_y - 1);
    END IF;

再通过一个函数,来计算方块是否和已有方块位置冲突,从而确定方块是否可以移动到新位置

CREATE OR REPLACE FUNCTION shape_rock_intersect(shape_pos VARCHAR, rock_pos VARCHAR)
RETURNS BOOLEAN AS $$
DECLARE
    shape_min INTEGER;
    shape_max INTEGER;
    rock_array VARCHAR[];
    rock_min INTEGER;
    rock_max INTEGER;
BEGIN
    IF shape_pos IS NULL OR shape_pos = '' OR rock_pos IS NULL OR rock_pos = '' THEN
      RETURN FALSE;
    END IF;

    shape_min := split_part(shape_pos, '|', 1) :: INTEGER;
    shape_max := split_part(shape_pos, '|', 2) :: INTEGER;

    rock_array := string_to_array(rock_pos, ',');

    FOR i IN 1..cardinality(rock_array) LOOP
      rock_min := split_part(rock_array[i], '|', 1) :: INTEGER;
      rock_max := split_part(rock_array[i], '|', 2) :: INTEGER;

      IF rock_min <= shape_max AND rock_max >= shape_min THEN
        RETURN TRUE;
      END IF;
    END LOOP;

    RETURN FALSE;

END;
$$ LANGUAGE plpgsql;

移动方块

移动方块,可以按照输入的方向进行左右移动,随后再下落一层,用<>v来代替对应方向

    -- move the shape to new position
    IF move_direction = 'v' THEN
      new_x := pos_x;
      new_y := pos_y - 1;
    END IF;

    IF move_direction = '>' THEN
      new_x := pos_x + 1;
      new_y := pos_y;
      min_x := min_x + 1;
      max_x := max_x + 1;
    END IF;

    IF move_direction = '<' THEN
      new_x := pos_x - 1;
      new_y := pos_y;
      min_x := min_x - 1;
      max_x := max_x - 1;
    END IF;

移动到新位置后,先判断是否越过了边界。如果左右超过边界,则直接返回,如果向下超过边界,则代表已经触底,停止后续移动

    -- if we cross the border
    IF new_y < 1 THEN
      -- touch the bottom, become stopped
      FOR i IN 1..7 LOOP
        IF origin_array[i] IS NOT NULL THEN
          pos_array[i] := ltrim(coalesce(pos_array[i], '') || ',' || origin_array[i], ',');
        END IF;
      END LOOP;

      RETURN (TRUE, pos_x, pos_y, pos_array);
    END IF;

    IF min_x < 1 OR max_x > 7 THEN
      -- touch the border
      RETURN (FALSE, pos_x, pos_y, pos_array);
    END IF;

如果没有越过边界,则计算是否与现有方块冲突。有冲突的话,则返回旧有位置;无冲突的话,则返回新的位置

    -- if intersect with rocks
    FOR i IN 1..7 LOOP
      IF shape_rock_intersect(moved_array[i], pos_array[i]) THEN
        shape_intersect = TRUE;
      END IF;
    END LOOP;

在有冲突的时候,如果无法继续左右移动,则无需进一步处理;如果是无法继续向下移动,则表示方块已停止,需要更新已有方块信息

    -- stopped, add to the pos_array
    IF move_direction = 'v' THEN
      FOR i IN 1..7 LOOP
        IF origin_array[i] IS NOT NULL THEN
          pos_array[i] := ltrim(coalesce(pos_array[i], '') || ',' || origin_array[i], ',');
        END IF;
      END LOOP;
    END IF;

完整的方块移动函数

CREATE OR REPLACE FUNCTION move_shape(shape_type VARCHAR, pos_x INTEGER, pos_y INTEGER, move_direction VARCHAR, pos_array VARCHAR[7])
RETURNS RECORD AS $$
DECLARE
    origin_array VARCHAR[7];
    moved_array VARCHAR[7];
    shape_intersect BOOLEAN := FALSE;
    min_x INTEGER;
    max_x INTEGER;
    new_x INTEGER;
    new_y INTEGER;
BEGIN
    -- calculate original position array
    IF shape_type = '-' THEN
      origin_array[pos_x] := pos_y || '|' || pos_y;
      origin_array[pos_x + 1] := pos_y || '|' || pos_y;
      origin_array[pos_x + 2] := pos_y || '|' || pos_y;
      origin_array[pos_x + 3] := pos_y || '|' || pos_y;
      min_x := pos_x;
      max_x := pos_x + 3;
    END IF;

    IF shape_type = '+' THEN
      origin_array[pos_x] := (pos_y - 1) || '|' || (pos_y - 1);
      origin_array[pos_x + 1] := (pos_y - 2) || '|' || pos_y;
      origin_array[pos_x + 2] := (pos_y - 1) || '|' || (pos_y - 1);
      min_x := pos_x;
      max_x := pos_x + 2;
    END IF;

    IF shape_type = 'J' THEN
      origin_array[pos_x] := (pos_y - 2) || '|' || (pos_y - 2);
      origin_array[pos_x + 1] := (pos_y - 2) || '|' || (pos_y - 2);
      origin_array[pos_x + 2] := (pos_y - 2) || '|' || pos_y;
      min_x := pos_x;
      max_x := pos_x + 2;
    END IF;


    IF shape_type = '|' THEN
      origin_array[pos_x] := (pos_y - 3) || '|' || pos_y;
      min_x := pos_x;
      max_x := pos_x;
    END IF;

    IF shape_type = '#' THEN
      origin_array[pos_x] := (pos_y - 1) || '|' || pos_y;
      origin_array[pos_x + 1] := (pos_y - 1) || '|' || pos_y;
      min_x := pos_x;
      max_x := pos_x + 1;
    END IF;



    -- move the shape to new position
    IF move_direction = 'v' THEN
      new_x := pos_x;
      new_y := pos_y - 1;
    END IF;

    IF move_direction = '>' THEN
      new_x := pos_x + 1;
      new_y := pos_y;
      min_x := min_x + 1;
      max_x := max_x + 1;
    END IF;

    IF move_direction = '<' THEN
      new_x := pos_x - 1;
      new_y := pos_y;
      min_x := min_x - 1;
      max_x := max_x - 1;
    END IF;

    -- if we cross the border
    IF new_y < 1 THEN
      -- touch the bottom, become stopped
      FOR i IN 1..7 LOOP
        IF origin_array[i] IS NOT NULL THEN
          pos_array[i] := ltrim(coalesce(pos_array[i], '') || ',' || origin_array[i], ',');
        END IF;
      END LOOP;

      RETURN (TRUE, pos_x, pos_y, pos_array);
    END IF;

    IF min_x < 1 OR max_x > 7 THEN
      -- touch the border
      RETURN (FALSE, pos_x, pos_y, pos_array);
    END IF;




    -- calculate the new position array
    IF shape_type = '-' THEN
      moved_array[new_x] := new_y || '|' || new_y;
      moved_array[new_x + 1] := new_y || '|' || new_y;
      moved_array[new_x + 2] := new_y || '|' || new_y;
      moved_array[new_x + 3] := new_y || '|' || new_y;
    END IF;

    IF shape_type = '+' THEN
      moved_array[new_x] := (new_y - 1) || '|' || (new_y - 1);
      moved_array[new_x + 1] := (new_y - 2) || '|' || new_y;
      moved_array[new_x + 2] := (new_y - 1) || '|' || (new_y - 1);
    END IF;

    IF shape_type = 'J' THEN
      moved_array[new_x] := (new_y - 2) || '|' || (new_y - 2);
      moved_array[new_x + 1] := (new_y - 2) || '|' || (new_y - 2);
      moved_array[new_x + 2] := (new_y - 2) || '|' || new_y;
    END IF;

    IF shape_type = '|' THEN
      moved_array[new_x] := (new_y - 3) || '|' || new_y;
    END IF;

    IF shape_type = '#' THEN
      moved_array[new_x] := (new_y - 1) || '|' || new_y;
      moved_array[new_x + 1] := (new_y - 1) || '|' || new_y;
    END IF;

    -- if intersect with rocks
    FOR i IN 1..7 LOOP
      IF shape_rock_intersect(moved_array[i], pos_array[i]) THEN
        shape_intersect = TRUE;
      END IF;
    END LOOP;




    -- no intersection, move to the new position
    IF NOT shape_intersect THEN
      RETURN (FALSE, new_x, new_y, pos_array);
    END IF;


    -- stopped, add to the pos_array
    IF move_direction = 'v' THEN
      FOR i IN 1..7 LOOP
        IF origin_array[i] IS NOT NULL THEN
          pos_array[i] := ltrim(coalesce(pos_array[i], '') || ',' || origin_array[i], ',');
        END IF;
      END LOOP;
    END IF;

    -- intersection exist, return original position
    RETURN (move_direction = 'v', pos_x, pos_y, pos_array);

END;
$$ LANGUAGE plpgsql;

判断已有方块的高度

当下一个方块出现的时候,需要计算已有方块的高度,从而确定方块出现的坐标

CREATE OR REPLACE FUNCTION max_height(pos_array VARCHAR[7])
RETURNS INTEGER AS $$
DECLARE
    height INTEGER := 0;
BEGIN
    FOR i IN 1..7 LOOP
      IF pos_array[i] IS NOT NULL THEN
        height := (SELECT greatest(height, (select max(split_part(h, '|', 2) :: INTEGER) from (select string_to_table(pos_array[i], ',') as h) t)));
      END IF;
    END LOOP;
    RETURN height;
END;
$$ LANGUAGE plpgsql;

下落方块过程

WITH recursive origin AS (
  SELECT string_to_array(line, null) as move_arr, string_to_array('-+J|#', null) as shape_arr, '{1, 3, 3, 4, 2}' :: INTEGER[] as shape_height from lance_input
), shape_move AS (
  SELECT 3 as pos_x, 4 as pos_y, 1 as shape_index, 0 as move_index, '{}' :: varchar[7] as pos_array, false as stopped, false as moved

  UNION ALL

  SELECT r.f2 as pos_x, r.f3 as pos_y, shape_index, move_index, r.f4 as pos_array, r.f1 as stopped, not moved as moved
  FROM (
    SELECT case when moved then 'v' else move_arr[(move_index - 1) % cardinality(move_arr) + 1] end as move, 
           shape_arr[(shape_index - 1) % 5 + 1] as shape,
           shape_index, move_index, pos_array, moved, 
           case when stopped then 3 else pos_x end as pos_x, 
           case when stopped then max_height(pos_array) + 4 + shape_height[(shape_index - 1) % 5 + 1] - 1 else pos_y end as pos_y
    FROM (
      SELECT CASE WHEN stopped THEN shape_index + 1 ELSE shape_index END as shape_index, 
             CASE WHEN NOT moved THEN move_index + 1 ELSE move_index END as move_index,
             pos_x,
             pos_y,
             pos_array, 
             stopped,
             moved
      FROM shape_move
    ) x, origin y 
  ) t, move_shape(shape, pos_x, pos_y, move, pos_array) as r(f1 BOOLEAN, f2 INTEGER, f3 INTEGER, f4 VARCHAR[])
  WHERE shape_index < 2023
)
SELECT max_height(pos_array)
FROM shape_move
WHERE shape_index = 2022 AND stopped

Part Two

第二关,计算1000000000000个方块掉落后的高度

How tall will the tower be after 1000000000000 rocks have stopped?

一万亿,肯定是无法通过正常渠道计算的,所以计算过程中肯定会出现循环,只需要找出这样的规律,就可以直接计算出结果。

寻找规律

将某个方块停止后,7列固定方块的排列形状,当作一个特征。当遇到相同的特征时,则可以判断出现了循环。那么使用一个函数,来计算出当前固定方块的特征。

CREATE OR REPLACE FUNCTION fingerprint(pos_array VARCHAR[7])
RETURNS VARCHAR AS $$
DECLARE
    min_height INTEGER := 2^30;
    height_array INTEGER[7];
BEGIN
    FOR i IN 1..7 LOOP
      IF pos_array[i] IS NOT NULL THEN
        height_array[i] := (SELECT max(split_part(h, '|', 2) :: INTEGER) from (select string_to_table(pos_array[i], ',') as h) t) :: INTEGER;
      ELSE 
        height_array[i] = 0;
      END IF;
      min_height = (select least(min_height, height_array[i]));
    END LOOP;
    RETURN (select string_agg(h ::VARCHAR, ',') from (select h :: INTEGER - min_height as h from (select unnest(height_array) as h) t) s) ;
END;
$$ LANGUAGE plpgsql;

将每一个方块固定后,打印出当时的特征,方块形状,以及总体高度,结果如下所示

     fingerprint     | shape | height 
---------------------+-------+--------
 0,0,1,1,1,1,0       | -     |      1
 0,0,3,4,3,1,0       | +     |      4
 4,4,6,4,3,1,0       | J     |      6
 4,4,6,4,7,1,0       | |     |      7
 4,4,6,4,9,9,0       | #     |      9
 4,10,10,10,10,9,0   | -     |     10
 4,12,13,12,10,9,0   | +     |     13
 4,12,13,13,13,15,0  | J     |     15
 4,12,13,13,17,15,0  | |     |     17

看下第2000个方块,以及当时的特征,确实出现了循环,每35个方块就会重复出现。

postgres=# select * From lance_test where fingerprint = '0,15,15,15,11,12,11' and shape = '#';
fingerprint | shape_index | shape | height
---------------------+-------------+-------+--------
0,15,15,15,11,12,11 | 40 | # | 66
0,15,15,15,11,12,11 | 75 | # | 119
0,15,15,15,11,12,11 | 110 | # | 172
0,15,15,15,11,12,11 | 145 | # | 225
0,15,15,15,11,12,11 | 180 | # | 278
0,15,15,15,11,12,11 | 215 | # | 331
0,15,15,15,11,12,11 | 250 | # | 384
0,15,15,15,11,12,11 | 285 | # | 437
0,15,15,15,11,12,11 | 320 | # | 490
0,15,15,15,11,12,11 | 355 | # | 543
0,15,15,15,11,12,11 | 390 | # | 596

那么后续计算就很简单了,持续循环最后差10个方块,再加上10个方块的高度即可。

postgres=# select 2000 + 35 * 28571428514;
   ?column?   
--------------
 999999999990
(1 row)

postgres=# select * From lance_test limit  11 offset 1999;
     fingerprint     | shape_index | shape | height 
---------------------+-------------+-------+--------
 0,15,15,15,11,12,11 |        2000 | #     |   3034
 0,15,16,16,16,16,11 |        2001 | -     |   3035
 6,7,6,5,5,5,0       |        2002 | +     |   3037
 1,2,1,0,1,1,3       |        2003 | J     |   3038
 1,2,1,0,5,1,3       |        2004 | |     |   3040
 0,1,2,2,4,0,2       |        2005 | #     |   3040
 0,1,5,5,5,5,2       |        2006 | -     |   3041
 0,1,5,5,7,8,7       |        2007 | +     |   3044
 0,1,8,8,10,8,7      |        2008 | J     |   3046
 0,5,8,8,10,8,7      |        2009 | |     |   3046
 0,5,10,10,10,8,7    |        2010 | #     |   3046
(11 rows)

postgres=# select 3034 + 53 * 28571428514 + (3046 - 3034);
   ?column?    
---------------
 1514285714288
(1 row)

发表评论