Skip to content

Commit 0a94ff0

Browse files
committed
day 14
1 parent 97a9a53 commit 0a94ff0

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

14_chocolate_charts.rb

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
INITIAL = [3, 7].freeze
2+
3+
two_only = ARGV.delete('-2')
4+
5+
input = !ARGV.empty? && ARGV.first.match?(/^\d+$/) ? ARGV.first : ARGF.read
6+
7+
unless two_only
8+
first = 0
9+
second = 1
10+
scores = INITIAL.dup
11+
target = Integer(input)
12+
until scores.size >= target + 10
13+
scores.concat((scores[first] + scores[second]).digits.reverse)
14+
first = (first + 1 + scores[first]) % scores.size
15+
second = (second + 1 + scores[second]) % scores.size
16+
end
17+
puts scores[target, 10].join
18+
end
19+
20+
# This isn't as formally verified as Knuth-Morris-Pratt,
21+
# but I think it should be fine.
22+
# I'm assuming the search pattern is small compared to the digit stream,
23+
# so it's fine if this is not the most efficient.
24+
def state_transitions(digits)
25+
next_state = Array.new(digits.size) { [0] * 10 }
26+
digits.each_with_index { |d, i|
27+
next_state[i][d] = i + 1
28+
(0..9).each { |wrong_digit|
29+
next if wrong_digit == d
30+
prefix = digits.first(i) << wrong_digit
31+
until prefix.empty?
32+
if digits[0, prefix.size] == prefix
33+
next_state[i][wrong_digit] = prefix.size
34+
break
35+
end
36+
prefix.shift
37+
end
38+
}
39+
}
40+
next_state.freeze
41+
end
42+
43+
# This code does some bad things solely for the purpose of being fast.
44+
def find(digits)
45+
first = 0
46+
second = 1
47+
scores = INITIAL.dup
48+
49+
state_table = state_transitions(digits)
50+
good_digits = 0
51+
52+
score1 = scores[first]
53+
score2 = scores[second]
54+
55+
# while true is faster than loop
56+
# https://github.com/JuanitoFatas/fast-ruby#loop-vs-while-true-code
57+
while true
58+
new_score = score1 + score2
59+
60+
# Normally, you'd write new_scores = new_score >= 10 ? new_score.divmod(10) : [new_score]
61+
# and then iterate over new_scores.
62+
# Instead, here we manually unroll that loop.
63+
# Unfortunately, the fastest way was code duplication.
64+
65+
if new_score >= 10
66+
new_score -= 10
67+
good_digits = state_table[good_digits][1]
68+
return scores.size + 1 - digits.size if good_digits == digits.size
69+
scores << 1
70+
end
71+
72+
good_digits = state_table[good_digits][new_score]
73+
return scores.size + 1 - digits.size if good_digits == digits.size
74+
scores << new_score
75+
76+
unless (score1 = scores[first += 1 + score1])
77+
first %= scores.size
78+
score1 = scores[first]
79+
end
80+
unless (score2 = scores[second += 1 + score2])
81+
second %= scores.size
82+
score2 = scores[second]
83+
end
84+
end
85+
end
86+
87+
{
88+
[5, 1, 5, 8, 9] => 9,
89+
[0, 1, 2, 4, 5] => 5,
90+
[9, 2, 5, 1, 0] => 18,
91+
[5, 9, 4, 1, 4] => 2018,
92+
}.each { |k, want|
93+
got = find(k)
94+
puts "#{k.join}: want #{want}, got #{got}" if want != got
95+
}
96+
97+
puts find(input.chars.map(&method(:Integer)))

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Some may additionally support other ways:
3232

3333
* Day 9 (Marble Mania): Pass the number of players and the last marble in ARGV
3434
* Day 11 (Max Square / Chronal Charge): Pass the serial in ARGV
35+
* Day 14 (Chocolate Charts): Pass the input in ARGV
3536

3637
# Highlights
3738

@@ -60,6 +61,7 @@ Interesting approaches:
6061
* Day 06 (Chronal Coordinates): Assumed I needed to be clever about which coordinates I check, wrote incorrect code that attempted to scan every row for the start/end column of the safe region on that row, but was actually an infinite loop. Changed to a flood-fill solution to get a spot on the leaderboard. Later discovered that using only the points in the bounding box would have been fine, given how far away the points are.
6162
* Day 09 (Marble Mania): Assumed there would be some pattern to be found within the sequence and wasted time trying to find it, rather than just brute-forcing it with a better data structure.
6263
* Day 11 (Max Square / Chronal Charge): Attempted to cache (only add new edges and corners) which still ends up taking 2 minutes to run because it's O(n^4), and was bug-prone and took a long time to write. For getting on the leaderboard, consider a completely different approach: Just use the O(n^5) approach, but with size as the outermost loop. Print out the largest square found for each size and submit when they start decreasing. In other words, try asymptotically-slow approaches that can nevertheless give an answer reasonably fast, rather than waiting for an asymptotically-fast approach to finish.
64+
* Day 14 (Chocolate Charts): As I recall, I tried just taking a subarray of the last 6 scores at any time and comparing this against the desired sequence, but even this was too slow; I switched to only incrementing a counter when I had a match, which worked fast enough but was a bit error prone. I saw others just check every 1000 scores or so, only checking the last 1000+6 scores.
6365

6466
# Posting schedule and policy
6567

0 commit comments

Comments
 (0)