Skip to content

Commit 6a318a8

Browse files
committed
day 14: skip storing most elements
All sequences converge pretty quickly on index 23. We now only need to store digits reachable from index 23. All other digits can be immediately discarded, since we already updating the number of good digits as each digit is generated. The effect on runtime is disappointing, at best a 0.1 second improvement from 8.3 seconds -> 8.2 seconds. However, memory usage should be greatly improved!
1 parent 0a94ff0 commit 6a318a8

File tree

3 files changed

+145
-18
lines changed

3 files changed

+145
-18
lines changed

14_chocolate_charts.rb

+65-18
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,39 @@ def state_transitions(digits)
4242

4343
# This code does some bad things solely for the purpose of being fast.
4444
def find(digits)
45-
first = 0
46-
second = 1
47-
scores = INITIAL.dup
45+
# Generated by gen14.rb
46+
# 371010124515891677925107
47+
prefixes = [
48+
[3, 1, 1, 4, 9], # 0
49+
[7, 5, 6, 0], # 1
50+
[1, 1, 1, 4, 9], # 2
51+
[0, 1, 1, 4, 9], # 3
52+
[1, 1, 4, 9], # 4
53+
[0, 1, 4, 9], # 5
54+
[1, 4, 9], # 6
55+
[2, 1, 8, 1], # 7
56+
[4, 9], # 8
57+
[5, 6, 0], # 9
58+
].map(&:freeze).freeze
59+
suffix = [7]
60+
61+
# first starts at 4
62+
first_on_suffix = false
63+
first_track = prefixes[0]
64+
first_pos = 1
65+
# second starts at 13
66+
second_on_suffix = false
67+
second_track = prefixes[0]
68+
second_pos = 4
69+
70+
size = 24
71+
next_write = 31
4872

4973
state_table = state_transitions(digits)
5074
good_digits = 0
5175

52-
score1 = scores[first]
53-
score2 = scores[second]
76+
score1 = first_track[first_pos]
77+
score2 = second_track[second_pos]
5478

5579
# while true is faster than loop
5680
# https://github.com/JuanitoFatas/fast-ruby#loop-vs-while-true-code
@@ -65,29 +89,52 @@ def find(digits)
6589
if new_score >= 10
6690
new_score -= 10
6791
good_digits = state_table[good_digits][1]
68-
return scores.size + 1 - digits.size if good_digits == digits.size
69-
scores << 1
92+
return size + 1 - digits.size if good_digits == digits.size
93+
if size == next_write
94+
suffix << 1
95+
next_write += 2
96+
end
97+
size += 1
7098
end
7199

72100
good_digits = state_table[good_digits][new_score]
73-
return scores.size + 1 - digits.size if good_digits == digits.size
74-
scores << new_score
101+
return size + 1 - digits.size if good_digits == digits.size
102+
if size == next_write
103+
suffix << new_score
104+
next_write += 1 + new_score
105+
end
106+
size += 1
75107

76-
unless (score1 = scores[first += 1 + score1])
77-
first %= scores.size
78-
score1 = scores[first]
108+
unless (score1 = first_track[first_pos += 1])
109+
first_pos = 0
110+
if first_on_suffix
111+
first_track = prefixes[next_write - size]
112+
first_on_suffix = false
113+
else
114+
first_track = suffix
115+
first_on_suffix = true
116+
end
117+
score1 = first_track[0]
79118
end
80-
unless (score2 = scores[second += 1 + score2])
81-
second %= scores.size
82-
score2 = scores[second]
119+
unless (score2 = second_track[second_pos += 1])
120+
second_pos = 0
121+
if second_on_suffix
122+
second_track = prefixes[next_write - size]
123+
second_on_suffix = false
124+
else
125+
second_track = suffix
126+
second_on_suffix = true
127+
end
128+
score2 = second_track[0]
83129
end
84130
end
85131
end
86132

87133
{
88-
[5, 1, 5, 8, 9] => 9,
89-
[0, 1, 2, 4, 5] => 5,
90-
[9, 2, 5, 1, 0] => 18,
134+
# No longer operative since we start w/ 24 elements.
135+
#[5, 1, 5, 8, 9] => 9,
136+
#[0, 1, 2, 4, 5] => 5,
137+
#[9, 2, 5, 1, 0] => 18,
91138
[5, 9, 4, 1, 4] => 2018,
92139
}.each { |k, want|
93140
got = find(k)

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Interesting approaches:
5555
* https://en.wikipedia.org/wiki/Summed-area_table
5656
* We can calculate a maximum upper bound on the size of a square with length 2N by using four squares of length N.
5757
* We can calculate a maximum upper bound on the size of a square with length 2N+1 by using two squares of length N plus two squares of length N+1 minus one square.
58+
* Day 14 (Chocolate Charts):
59+
When wrapping around, you must land on one of the first ten elements of the array.
60+
All sequences converge on index 23.
61+
From that point forward, we only need to store the scores that elves will land on.
62+
All other scores can be immediately discarded after updating the state of "how many charactes have I seen".
5863

5964
# Takeaways
6065

gen14.rb

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
INITIAL = [3, 7].freeze
2+
def converged(scores)
3+
seqs = []
4+
return nil if scores.size < 10
5+
(0...10).each { |start|
6+
pos = start
7+
poses = []
8+
while (score = scores[pos])
9+
poses << [score, pos]
10+
pos += 1 + score
11+
end
12+
seqs[start] = poses
13+
}
14+
minlen = seqs.map(&:size).min
15+
mismatch_from_right = (0...minlen).find { |i|
16+
seqs.map { |s| s[-1 - i] }.uniq.size > 1
17+
}
18+
mismatch_from_right > 0 ? [seqs, mismatch_from_right] : nil
19+
end
20+
21+
first = 0
22+
second = 1
23+
scores = INITIAL.dup
24+
25+
step = -> {
26+
scores.concat((scores[first] + scores[second]).digits.reverse)
27+
first = (first + 1 + scores[first]) % scores.size
28+
second = (second + 1 + scores[second]) % scores.size
29+
}
30+
31+
# This could take a long time since each call to converged traverses the entire array,
32+
# but luckily it doesn't.
33+
until (seqs, mismatch_from_right = converged(scores))
34+
(_arbitrary = 10).times { step[] }
35+
end
36+
37+
first_matching_score, first_matching_index = seqs[0].last(mismatch_from_right).first
38+
39+
first = 0
40+
second = 1
41+
scores = INITIAL.dup
42+
step[] until scores.size >= first_matching_index + 1
43+
44+
def write_pos(name, pos, seqs, first_match)
45+
if pos >= first_match
46+
track = 'suffix'
47+
new_pos = pos - first_match
48+
else
49+
track_index = seqs.index { |s| s.any? { |_, i| i == pos } }
50+
track = "prefixes[#{track_index}]"
51+
new_pos = seqs[track_index].index { |_, i| i == pos }
52+
end
53+
puts "# #{name} starts at #{pos}"
54+
puts "#{name}_on_suffix = #{track == 'suffix'}"
55+
puts "#{name}_track = #{track}"
56+
puts "#{name}_pos = #{new_pos}"
57+
end
58+
59+
puts "# generated by #{__FILE__}"
60+
puts "# #{scores.join}"
61+
puts 'prefixes = ['
62+
maxlen = (seqs.map(&:size).max - mismatch_from_right) * 3 + 1
63+
seqs.each_with_index { |s, i|
64+
s = s.dup
65+
s.pop(mismatch_from_right)
66+
puts " %-#{maxlen}s # #{i}" % ["[#{s.map(&:first).join(', ')}],"]
67+
}
68+
puts '].map(&:freeze).freeze'
69+
puts "suffix = [#{scores.drop(first_matching_index).join(', ')}]"
70+
puts
71+
write_pos('first', first, seqs, first_matching_index)
72+
write_pos('second', second, seqs, first_matching_index)
73+
puts
74+
puts "size = #{scores.size}"
75+
puts "next_write = #{first_matching_score + first_matching_index + 1}"

0 commit comments

Comments
 (0)