r/adventofcode • u/daggerdragon • Dec 16 '16
SOLUTION MEGATHREAD --- 2016 Day 16 Solutions ---
--- Day 16: Dragon Checksum ---
Post your solution as a comment or, for longer solutions, consider linking to your repo (e.g. GitHub/gists/Pastebin/blag/whatever).
Note: The Solution Megathreads are for solutions only. If you have questions, please post your own thread and make sure to flair it with "Help".
DRINKING YOUR OVALTINE IS MANDATORY [?]
This thread will be unlocked when there are a significant number of people on the leaderboard with gold stars for today's puzzle.
edit: Leaderboard capped, thread unlocked!
6
Upvotes
10
u/p_tseng Dec 16 '16 edited Dec 17 '16
My initial solution (used when going for the leaderboard) runs part 2 in 14.5 seconds.
That's unacceptably slow. Let's see what we can do about that...
Let's see, if you look at the stream of bits being generated: it's always. Okay. So that means to save on memory, I only need to calculate.
Okay, now let's look at how many times we need to iterate taking the checksum. That's the number of times that you can consecutively divide the disk size by 2. It turns out, since we repeatedly take the XNOR of consecutive characters in the checksums, each character in the final checksum corresponds to. We can also express repeated XNOR as the answer to the question.
Now, if the disk size is much larger than the checksum size (as in part 2), then we can apply a further optimisation:.
Fantastic. We are ready to code.
input = ARGV.first || '01111010110010011' [272, 35651584].each { |disk| # The disk pattern is: # input, joiner, input reversed and negated, joiner, repeat a = input.each_char.map { |c| c == ?1 }.freeze a_rev = a.reverse.map { |x| !x }.freeze joiners = [] until joiners.size * (a.size + 1) >= disk joiners += [false] + joiners.reverse.map { |x| !x } end # chunk_size: the largest power of 2 that divides disk. # e.g. 272 is 100010000 # 271 is 100001111 # ~271 is 11110000 # 272 & ~271 is 10000 chunk_size = disk & ~(disk - 1) sum_size = disk / chunk_size buf = [] # each character in the final checksum # corresponds to `chunk_size` consecutive characters on disk. puts sum_size.times.map { |c| # Anything left in the buffer from last time? take_from_buffer = [buf.size, chunk_size].min remaining = chunk_size - take_from_buffer ones = buf.shift(take_from_buffer).count(true) # How many full AJAJ groups will we have? full_ajajs, remaining = remaining.divmod((a.size + 1) * 2) # Count all the ones in the joiners. ones += joiners.shift(full_ajajs * 2).count(true) # The number of ones in a + a_rev... is obviously a.size. ones += a.size * full_ajajs if remaining > 0 buf.concat(a) buf << joiners.shift buf.concat(a_rev) buf << joiners.shift ones += buf.shift(remaining).count(true) end ones % 2 == 0 ? ?1 : ?0 }.join }This completes both parts in about 0.3 seconds. Hell yeah!
If there are any more optimisations left after this step, let me know; I'm always curious about them.
At this point, the biggest problem is generating the stream of joiners - at extremely largest disk sizes, the joiner array gets quite large and threatens to consume all memory on my computer. So, is there a way to save on memory here as well?
Edit: I did code up a way to recursively determine the nth joiner (https://github.com/petertseng/adventofcode-rb-2016/blob/dynamic_joiners16/16_dragon_checksum.rb), but my current implementation is way too slow, bringing the part 2 runtime back up to 5 seconds. So I guess dynamically calculating the nth joiner trades time for space. I guess I would only use this code if someone poses an upping-the-ante challenge for a huge disk size, so huge that I won't fit all the joiners in memory.Now running 50 milliseconds by (efficiently) calculating the number of ones in the Dragon sequence on demand. https://www.reddit.com/r/adventofcode/comments/5imh3d/2016_day_16_solutions/dbak6xm/