AdventOfCode 2022 Day 19

Part One

robot可以采集资源,制作robot需要资源,设计最佳的策略让采集的资源最多

Blueprint 1:
Each ore robot costs 4 ore.
Each clay robot costs 2 ore.
Each obsidian robot costs 3 ore and 14 clay.
Each geode robot costs 2 ore and 7 obsidian.

问题分析

robot可以采集资源,但是同样消耗资源。很多SLG游戏就是类似的玩法,比如红警、星际等。如果前期优先采集资源,那么后期资源充足,但是没有足够的兵力;如果前期资源优先投入在兵力上,后期兵力生产效率就会降低。

Day 16,问题与本文中的问题比较相似,都是找出一定的排列,从而让某个目标最大。但是还是有一定的差异的,Day 16的问题,是找出一条路径途径每个节点,且每个节点只会经过一次。而这里,在24分钟的限制内,需要决定在哪一分钟建造哪种robot,或者不做任何建造。

每一分钟,就会有5种选择,build ore / build clay / build obsidian / build geode / no build。5的24次方,数量级无法想象。因此,需要针对可能的路径做裁剪,裁剪方式有如下几种:

  • 如果上一分钟没有建造robot,那么只可能是为了积累资源建造更贵的robot。比如ore robot需要4个ore,因此当前有两个ore时,可以选择clay robot,但是也可以选择不造,积累ore。如果上一轮的资源足够建造clay robot,那么这一轮也必要造了,因为故意推迟一轮,最后的资源肯定是要少的。
  • 如果当前资源足够建造最贵的robot,那么要尽快建造出来,积累资源而不转化为生产效率,无疑是愚蠢的。最终的目标是geode最多,其他的资源再多也没有用。
  • 如果当前某种类型的robot,已经足够产生该类型最贵的资源,那么就没有必要继续建造robot了,因此每一轮产生的资源足够消耗了。

路径裁剪

用RCBG,分别代表建造ore/clay/obsidian/geode类型的robot,N代表不建造任何robot,每一轮最多会膨胀5倍。同时,计算出每一轮最多需要的资源数

WITH recursive origin AS (
  SELECT rtrim(split_part(line, ' ', 2), ':') :: INTEGER as blueprint,
         split_part(line, ' ', 7) :: INTEGER as ore_robot_ore,
         split_part(line, ' ', 13) :: INTEGER as clay_robot_ore,
         split_part(line, ' ', 19) :: INTEGER as obsidian_robot_ore,
         split_part(line, ' ', 22) :: INTEGER as obsidian_robot_clay,
         split_part(line, ' ', 28) :: INTEGER as geode_robot_ore,
         split_part(line, ' ', 31) :: INTEGER as geode_robot_obsidian
  FROM lance_input
), possible_actions AS (
  SELECT regexp_split_to_table('RCBGN', '') as action
)

greatest(ore_robot_ore, clay_robot_ore, obsidian_robot_ore, geode_robot_ore) as max_need_ore,
obsidian_robot_clay as max_need_clay,
geode_robot_obsidian as max_need_obsidian,

下面就会去实现三种裁剪方式

  • 上一轮不造,这一轮也不去造了
WHEN y.action = 'R' THEN minerals[1] >= ore_robot_ore 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= ore_robot_ore)
WHEN y.action = 'C' THEN minerals[1] >= clay_robot_ore 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= clay_robot_ore)
WHEN y.action = 'B' THEN minerals[1] >= obsidian_robot_ore 
                  AND minerals[2] >= obsidian_robot_clay AND NOT (last_action = 'N' AND last_minerals[1] >= obsidian_robot_ore AND last_minerals[2] >= obsidian_robot_clay)
WHEN y.action = 'G' THEN minerals[1] >= geode_robot_ore AND minerals[3] >= geode_robot_obsidian 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= geode_robot_ore AND last_minerals[3] >= geode_robot_obsidian)
  • 当前资源足够,则一定要建造robot。就像在游戏中,行动点足够的时候,一定要做些事情,而不是直接跳过本轮。
WHEN y.action = 'N' THEN minerals[1] < max_need_ore OR minerals[2] < max_need_clay OR minerals[3] < max_need_obsidian
  • robot足够的时候,没有必要继续生产了
WHEN y.action = 'R' THEN minerals[1] >= ore_robot_ore
                  AND robots[1] < max_need_ore
WHEN y.action = 'C' THEN minerals[1] >= clay_robot_ore 
                  AND robots[2] < max_need_clay
WHEN y.action = 'B' THEN minerals[1] >= obsidian_robot_ore 
                  AND robots[3] < max_need_obsidian

完整SQL

WITH recursive origin AS (
  SELECT rtrim(split_part(line, ' ', 2), ':') :: INTEGER as blueprint,
         split_part(line, ' ', 7) :: INTEGER as ore_robot_ore,
         split_part(line, ' ', 13) :: INTEGER as clay_robot_ore,
         split_part(line, ' ', 19) :: INTEGER as obsidian_robot_ore,
         split_part(line, ' ', 22) :: INTEGER as obsidian_robot_clay,
         split_part(line, ' ', 28) :: INTEGER as geode_robot_ore,
         split_part(line, ' ', 31) :: INTEGER as geode_robot_obsidian
  FROM lance_input
), possible_actions AS (
  SELECT regexp_split_to_table('RCBGN', '') as action
), walkthrough AS (
  SELECT blueprint,
        ore_robot_ore,
        clay_robot_ore,
        obsidian_robot_ore,
        obsidian_robot_clay,
        geode_robot_ore,
        geode_robot_obsidian,
        greatest(ore_robot_ore, clay_robot_ore, obsidian_robot_ore, geode_robot_ore) as max_need_ore,
        obsidian_robot_clay as max_need_clay,
        geode_robot_obsidian as max_need_obsidian,
        24 as remain_minutes, 
        '{1, 0, 0, 0}' :: INTEGER[] as robots, 
        '{0, 0, 0, 0}' :: INTEGER[] as minerals, 
        '' as path,
        '' as last_action,
        '{0, 0, 0, 0}' :: INTEGER[] as last_minerals
  FROM origin

  UNION ALL

  SELECT blueprint,
        ore_robot_ore,
        clay_robot_ore,
        obsidian_robot_ore,
        obsidian_robot_clay,
        geode_robot_ore,
        geode_robot_obsidian,
        max_need_ore,
        max_need_clay,
        max_need_obsidian,
        remain_minutes - 1,
        ARRAY[robots[1] + CASE WHEN action= 'R' THEN 1 ELSE 0 END,
              robots[2] + CASE WHEN action= 'C' THEN 1 ELSE 0 END,
              robots[3] + CASE WHEN action= 'B' THEN 1 ELSE 0 END,
              robots[4] + CASE WHEN action= 'G' THEN 1 ELSE 0 END],
        ARRAY[minerals[1] + robots[1] - CASE WHEN action = 'R' THEN ore_robot_ore 
                                             WHEN action = 'C' THEN clay_robot_ore 
                                             WHEN action = 'B' THEN obsidian_robot_ore 
                                             WHEN action = 'G' THEN geode_robot_ore
                                             ELSE 0
                                        END, 
              minerals[2] + robots[2] - CASE WHEN action = 'B' THEN obsidian_robot_clay ELSE 0 END, 
              minerals[3] + robots[3] - CASE WHEN action = 'G' THEN geode_robot_obsidian ELSE 0 END, 
              minerals[4] + robots[4]],
        path || y.action,
        y.action as last_action,
        minerals as last_minerals
  FROM walkthrough x, possible_actions y
  WHERE CASE WHEN y.action = 'R' THEN minerals[1] >= ore_robot_ore 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= ore_robot_ore)
                  AND robots[1] < max_need_ore
              WHEN y.action = 'C' THEN minerals[1] >= clay_robot_ore 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= clay_robot_ore)
                  AND robots[2] < max_need_clay
              WHEN y.action = 'B' THEN minerals[1] >= obsidian_robot_ore 
                  AND minerals[2] >= obsidian_robot_clay AND NOT (last_action = 'N' AND last_minerals[1] >= obsidian_robot_ore AND last_minerals[2] >= obsidian_robot_clay)
                  AND robots[3] < max_need_obsidian
              WHEN y.action = 'G' THEN minerals[1] >= geode_robot_ore AND minerals[3] >= geode_robot_obsidian 
                  AND NOT (last_action = 'N' AND last_minerals[1] >= geode_robot_ore AND last_minerals[3] >= geode_robot_obsidian)
              WHEN y.action = 'N' THEN minerals[1] < max_need_ore OR minerals[2] < max_need_clay OR minerals[3] < max_need_obsidian
        END 
  AND x.remain_minutes > 0
)
SELECT SUM(blueprint * blueprint)
FROM (
  SELECT blueprint, max(minerals[4]) as geode_cnt
  FROM walkthrough 
  GROUP BY blueprint
) t

Part Two

时间从24分钟延长至32分钟,输入不变。迭代轮次增加了8轮,很有可能结果膨胀很大,需要继续进行裁剪。

问题分析

用例子的第一条记录测试了下,果然速度非常慢,从24分钟增加到28分钟,花费了4分钟才得出结果。

postgres=# select remain_minutes, count(*) from lance_test group by remain_minutes;
 remain_minutes |  count  
----------------+---------
              0 | 8881990
              1 | 2940004
              2 |  991089
              3 |  343090
              4 |  123991
              5 |   46817
              6 |   19058
              7 |    8382
              8 |    3883
              9 |    1823

从上图可以看出,最后几步,基本都是按照3倍的指数级膨胀,因此会越来越慢。再看下,最终结果中,采集的geode数量的分布。

postgres=# select minerals[4] as geode_cnt, count(*) from lance_test where remain_minutes = 0 group by minerals[4];
 geode_cnt |  count  
-----------+---------
         0 | 1518051
         1 |  677504
         2 |  699417
         3 |  944477
         4 |  940016
         5 | 1108704
         6 |  954078
         7 |  745418
         8 |  508224
         9 |  400113
        10 |  220047
        11 |  108580
        12 |   45170
        13 |   10421
        14 |    1472
        15 |     298
(16 rows)

由分布可见,大部分的路径,其产生的geode_cnt非常少,可以在更早的时候直接淘汰掉。可以从以下几点尽早过滤掉某些路径。

  • 首先,假设总的轮次是m,那么m-1轮至少要有一个geode robot。
  • 其次,一个geode消耗7个obsidian,那么在(m-1)-4轮,至少有一个obsidian_robot。因此n轮的时间,从0开始,最多生产(n-1) * n / 2个资源。4轮生产6个,并不足够满足生产geode robot的需求。
  • 同理,推理出clay robot生产的最晚时机。
  • 最后,每轮结束之后,都淘汰掉最终geode个数肯定非最高的路径。这里的最终结果,指的是,计算剩下的轮次,每一轮都建造一个geode robot,也肯定无法追上的。

路径裁剪

首先,定义出三个最晚生产的时机。n轮最多生产(n-1) * n / 2 = m,则n = sqrt(2m + 1/4) + 1/2。

1 as latest_time_geode,
1 + ceil(sqrt(2 * geode_robot_obsidian + 0.25) + 0.5) as latest_time_obsidian,
1 + ceil(sqrt(2 * geode_robot_obsidian + 0.25) + 0.5) + ceil(sqrt(2 * obsidian_robot_clay + 0.25) + 0.5) as latest_time_clay,

关于geode数量淘汰策略,一种路径最后至少产生的geode数量,就是已有geode + robot * remain_minutes。而一种路径最多生产的geode数量,就是已有geode + robot * remain_minutes + (remain_minutes – 1) * remain_minutes / 2,意思就是剩下的时间,每一分钟都生产一个geode robot。

如果一条路径,其最多生产的数量,还不如其他路径最少生产的,那么这个路径就可以淘汰掉了。

max_geode AS (
      SELECT blueprint, max(minerals[4] + robots[4] * remain_minutes) as geode_cnt
      FROM tmp
      GROUP BY blueprint
)
SELECT a.*
FROM tmp a JOIN max_geode b
ON a.blueprint = b.blueprint
WHERE NOT (minerals[4] = 0 AND remain_minutes <= latest_time_geode)
AND NOT (minerals[3] = 0 AND remain_minutes <= latest_time_obsidian)
AND NOT (minerals[2] = 0 AND remain_minutes <= latest_time_clay)
AND NOT (minerals[4] + robots[4] * remain_minutes + (remain_minutes - 1) * remain_minutes / 2 < geode_cnt)

经过裁剪后,从下面的统计看出,越接近结尾,裁剪的效果越明显。

postgres=# select remain_minutes, count(*) from lance_test group by remain_minutes order by 1;
 remain_minutes | count  
----------------+--------
              0 |     69
              1 |     18
              2 |     42
              3 |     34
              4 |   4611
              5 | 101794
              6 | 129627
              7 | 221994
              8 | 123987
              9 |  46813
             10 |  19054
             11 |   8378

完整SQL

WITH recursive origin AS (
  SELECT rtrim(split_part(line, ' ', 2), ':') :: INTEGER as blueprint,
         split_part(line, ' ', 7) :: INTEGER as ore_robot_ore,
         split_part(line, ' ', 13) :: INTEGER as clay_robot_ore,
         split_part(line, ' ', 19) :: INTEGER as obsidian_robot_ore,
         split_part(line, ' ', 22) :: INTEGER as obsidian_robot_clay,
         split_part(line, ' ', 28) :: INTEGER as geode_robot_ore,
         split_part(line, ' ', 31) :: INTEGER as geode_robot_obsidian,
         32 as remain_minutes
  FROM lance_input
), possible_actions AS (
  SELECT regexp_split_to_table('RCBGN', '') as action
), walkthrough AS (
  SELECT blueprint,
        ore_robot_ore,
        clay_robot_ore,
        obsidian_robot_ore,
        obsidian_robot_clay,
        geode_robot_ore,
        geode_robot_obsidian,
        greatest(ore_robot_ore, clay_robot_ore, obsidian_robot_ore, geode_robot_ore) as max_need_ore,
        obsidian_robot_clay as max_need_clay,
        geode_robot_obsidian as max_need_obsidian,
        remain_minutes, 
        1 as latest_time_geode,
        1 + ceil(sqrt(2 * geode_robot_obsidian + 0.25) + 0.5) as latest_time_obsidian,
        1 + ceil(sqrt(2 * geode_robot_obsidian + 0.25) + 0.5) + ceil(sqrt(2 * obsidian_robot_clay + 0.25) + 0.5) as latest_time_clay,
        '{1, 0, 0, 0}' :: INTEGER[] as robots, 
        '{0, 0, 0, 0}' :: INTEGER[] as minerals, 
        '' as path,
        '' as last_action,
        '{0, 0, 0, 0}' :: INTEGER[] as last_minerals
  FROM origin

  UNION ALL

  SELECT *
  FROM (
    WITH tmp AS (
      SELECT blueprint,
             ore_robot_ore,
             clay_robot_ore,
             obsidian_robot_ore,
             obsidian_robot_clay,
             geode_robot_ore,
             geode_robot_obsidian,
             max_need_ore,
             max_need_clay,
             max_need_obsidian,
             remain_minutes - 1 as remain_minutes,
             latest_time_geode,
             latest_time_obsidian,
             latest_time_clay,
             ARRAY[robots[1] + CASE WHEN action= 'R' THEN 1 ELSE 0 END,
                   robots[2] + CASE WHEN action= 'C' THEN 1 ELSE 0 END,
                   robots[3] + CASE WHEN action= 'B' THEN 1 ELSE 0 END,
                   robots[4] + CASE WHEN action= 'G' THEN 1 ELSE 0 END] as robots,
             ARRAY[minerals[1] + robots[1] - CASE WHEN action = 'R' THEN ore_robot_ore 
                                                  WHEN action = 'C' THEN clay_robot_ore 
                                                  WHEN action = 'B' THEN obsidian_robot_ore 
                                                  WHEN action = 'G' THEN geode_robot_ore
                                                  ELSE 0
                                             END, 
                   minerals[2] + robots[2] - CASE WHEN action = 'B' THEN obsidian_robot_clay ELSE 0 END, 
                   minerals[3] + robots[3] - CASE WHEN action = 'G' THEN geode_robot_obsidian ELSE 0 END, 
                   minerals[4] + robots[4]] as minerals,
             path || y.action as path,
             y.action as last_action,
             minerals as last_minerals
      FROM walkthrough x, possible_actions y
      WHERE CASE WHEN y.action = 'R' THEN minerals[1] >= ore_robot_ore 
                      AND NOT (last_action = 'N' AND last_minerals[1] >= ore_robot_ore)
                      AND robots[1] < max_need_ore
                  WHEN y.action = 'C' THEN minerals[1] >= clay_robot_ore 
                      AND NOT (last_action = 'N' AND last_minerals[1] >= clay_robot_ore)
                      AND robots[2] < max_need_clay
                  WHEN y.action = 'B' THEN minerals[1] >= obsidian_robot_ore 
                      AND minerals[2] >= obsidian_robot_clay AND NOT (last_action = 'N' AND last_minerals[1] >= obsidian_robot_ore AND last_minerals[2] >= obsidian_robot_clay)
                      AND robots[3] < max_need_obsidian
                  WHEN y.action = 'G' THEN minerals[1] >= geode_robot_ore AND minerals[3] >= geode_robot_obsidian 
                      AND NOT (last_action = 'N' AND last_minerals[1] >= geode_robot_ore AND last_minerals[3] >= geode_robot_obsidian)
                  WHEN y.action = 'N' THEN minerals[1] < max_need_ore OR minerals[2] < max_need_clay OR minerals[3] < max_need_obsidian
            END 
      AND x.remain_minutes > 0
    ), max_geode AS (
      SELECT blueprint, max(minerals[4] + robots[4] * remain_minutes) as geode_cnt
      FROM tmp
      GROUP BY blueprint
    )
    SELECT a.*
    FROM tmp a JOIN max_geode b
    ON a.blueprint = b.blueprint
    WHERE NOT (minerals[4] = 0 AND remain_minutes <= latest_time_geode)
    AND NOT (minerals[3] = 0 AND remain_minutes <= latest_time_obsidian)
    AND NOT (minerals[2] = 0 AND remain_minutes <= latest_time_clay)
    AND NOT (minerals[4] + robots[4] * remain_minutes + (remain_minutes - 1) * remain_minutes / 2 < geode_cnt)
  ) t
)
SELECT blueprint, max(minerals[4]) as geode_cnt
FROM walkthrough 
GROUP BY blueprint

发表评论