Skip to content

Commit e3a1690

Browse files
committed
Day 21
1 parent d7fd801 commit e3a1690

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

21_fractal_art.rb

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
VERBOSE = ARGV.delete('-v')
2+
ITERS = if (narg = ARGV.find { |a| a.start_with?('-n') })
3+
ARGV.delete(narg)
4+
narg[2..-1].split(?,).map(&method(:Integer))
5+
else
6+
[5, 13]
7+
end.freeze
8+
9+
rules = ARGF.map(&:chomp).freeze
10+
11+
# Base rules (those given in input)
12+
bit = {?# => true, ?. => false}.freeze
13+
rules_by_size = rules.map { |rul|
14+
rul.split('=>').map { |slashed|
15+
slashed.strip.split(?/).map { |rl| rl.chars.map { |c| bit.fetch(c) } }
16+
}
17+
}.group_by { |l, _| l.size }.transform_values { |vs|
18+
vs.flat_map { |l, r|
19+
rotations = [l, l.map(&:reverse)]
20+
# transpose -> map(&:reverse) = rotate by 90
21+
6.times { rotations << rotations[-2].transpose.map(&:reverse) }
22+
rotations.map { |rot| [rot.flatten, r.flatten] }
23+
}.to_h.freeze
24+
}.freeze
25+
26+
# This problem is solvable only using the base rules,
27+
# if we explicitly keep the grid and translate to/from the flat form.
28+
# However, to be more efficient, let's exploit the repeating substructure.
29+
# We start with a 3x3 grid.
30+
#
31+
# Then the grid size follows a cycle of size 3:
32+
# n = 3^k -> 3 -> 4 rule -> 4 * 3^(k-1) (even)
33+
# n = 4 * 3^(k-1) -> 2 -> 3 rule -> 6 * 3^(k-1) = 2 * 3^k (even)
34+
# n = 2 * 3^k -> 2 -> 3 rule -> 3 * 3^k = 3^(k+1) (odd)
35+
# Cycle repeats, and that point every resulting 3x3 subgrid develops independently of the others!
36+
# That means we only need to keep track of how many of each subgrid there are.
37+
#
38+
# To support the cycle, we need to map:
39+
# 9 -> [16] * 1, 16 -> [4] * 9, 4 -> [9] * 1
40+
#
41+
# Why doesn't it work to map 9 -> [4] * 4?
42+
# Because we need the relative positions of the resulting 3x3
43+
# in order to be able to create the [4] * 9 grid.
44+
# If we simply mapped to [4] * 4,
45+
# we would lose the position information.
46+
#
47+
# We could go faster with a matrix of 3x3 -> 3x3 counts every 3 iterations,
48+
# and then exponentiate by squaring, but no motivation to write that code.
49+
50+
# Instead of storing arrays of bits, we'll compress them into a single integer.
51+
# This should avoid allocating so many arrays.
52+
def compress(grid)
53+
# Max number of bits is 16.
54+
# To disambiguate between grids of different sizes,
55+
# we'll also store the size starting at the 16th bit.
56+
(grid.size << 16) | grid.flatten.reduce(0) { |acc, bit| acc << 1 | (bit ? 1 : 0) }
57+
end
58+
59+
# key: 4x4 subgrid
60+
# value: list of nine 2x2 subgrids resulting from the key
61+
rules_16_36 = rules_by_size[3].values.map { |sixteen|
62+
subgrids = [0, 2, 8, 10].map { |i|
63+
rules_by_size[2].fetch(sixteen.values_at(*[0, 1, 4, 5].map { |j| i + j }))
64+
}
65+
# subgrids:
66+
# 0 | 1
67+
# -----
68+
# 2 | 3
69+
#
70+
# within each subgrid:
71+
# 0 1 2
72+
# 3 4 5
73+
# 6 7 8
74+
75+
in_one_subgrid = ->(subgrid, upper_left) {
76+
[
77+
[subgrid, upper_left],
78+
[subgrid, upper_left + 1],
79+
[subgrid, upper_left + 3],
80+
[subgrid, upper_left + 4],
81+
]
82+
}
83+
84+
two_by_twos = [
85+
in_one_subgrid[0, 0],
86+
[[0, 2], [1, 0], [0, 5], [1, 3]],
87+
in_one_subgrid[1, 1],
88+
[[0, 6], [0, 7], [2, 0], [2, 1]],
89+
[[0, 8], [1, 6], [2, 2], [3, 0]],
90+
[[1, 7], [1, 8], [3, 1], [3, 2]],
91+
in_one_subgrid[2, 3],
92+
[[2, 5], [3, 3], [2, 8], [3, 6]],
93+
in_one_subgrid[3, 4],
94+
]
95+
96+
[compress(sixteen), two_by_twos.map { |coords|
97+
compress(coords.map { |cs| subgrids.dig(*cs) })
98+
}.freeze]
99+
}.to_h
100+
101+
# key: any subgrid (might be 4x4, 3x3, 2x2)
102+
# value: the list of subgrids resulting from the key
103+
# note that if the key is K, value is V:
104+
# K = 4x4, V = array of nine 2x2 (from rules_16_36)
105+
# K = 3x3, V = array of one 4x4 (from rules_by_size[3])
106+
# K = 2x2, V = array of one 3x3 (from rules_by_size[2])
107+
complex_rules = (rules_by_size[2].merge(rules_by_size[3])).map { |k, v|
108+
[compress(k), [compress(v)].freeze]
109+
}.to_h.merge(rules_16_36).freeze
110+
111+
grid = {
112+
compress([false, true, false, false, false, true, true, true, true]) => 1,
113+
}.freeze
114+
115+
ITERS.each { |times|
116+
times.times { |n|
117+
output_grid = Hash.new(0)
118+
119+
grid.each { |subgrid, cardinality|
120+
complex_rules.fetch(subgrid).each { |new_subgrid|
121+
output_grid[new_subgrid] += cardinality
122+
}
123+
}
124+
125+
grid = output_grid.freeze
126+
}
127+
puts grid.sum { |subgrid, cardinality|
128+
subgrid.digits(2).take(16).count(1) * cardinality
129+
}
130+
}

0 commit comments

Comments
 (0)