def is_nice(s): sufficient_vowels =len(re.findall("[aeiou]", s)) >=3 contains_double = (np.array(list(s))[1:] == np.roll(list(s), 1)[1:]).any() contains_forbidden =any(val in s for val in ["ab", "cd", "pq", "xy"])return sufficient_vowels and contains_double andnot contains_forbiddensum(is_nice(x) for x in load(5))
Part 2
Code
def is_nice(s): contains_double = (np.array(list(s))[2:] == np.roll(list(s), 2)[2:]).any() contains_double_pair =bool(re.findall("(..).*\\1", s))return contains_double and contains_double_pairsum(is_nice(x) for x in load(5))
circuit = {target: source for source, target inmap(lambda x: x.split(" -> "), load(7))}binops = {"AND": lambda x, y: x & y,"OR": lambda x, y: x | y,"LSHIFT": lambda x, y: x << y,"RSHIFT": lambda x, y: x >> y}@functools.cachedef evaluate(symbol):try: result =int(symbol)return resultexceptValueError:pass operation = circuit[symbol].split()iflen(operation) ==1:return evaluate(operation[0])eliflen(operation) ==2:return evaluate(operation[1]) ^ (2**16-1)else: arg1, op, arg2 = operationreturn binops[op](evaluate(arg1), evaluate(arg2))evaluate("a")
Part 2
We can reset everything by clearing out the cache, and setting a wire to a specific value (or expression) can be accomplished by modifying the circuit.
d = {}data = [x.split() for x in load(9)]for source, _, destination, __, distance in data: d[(source, destination)] =int(distance) d[(destination, source)] =int(distance)cities =set(x[0] for x in d.keys())tours = [sum(d[route[start], route[start +1]] for start inrange(len(cities) -1))for route in itertools.permutations(cities)]min(tours)
Iterate over candidate passwords in order, starting with the puzzle input
Is valid is not too difficult to accomplish. The “straight” condition can be rewritten as “1, 1” appears somewhere in the list of differences between neighboring characters. The “double pair” condition can be shortly expressed as matching a simple regex. Forbidding certain characters outright is most easily accomplished by never generating them as candidates
To iterate over candidate passwords, we first construct a helper method to iterate over candidate passwords that keep some prefix string fixed. The full iterator is then a chain over all these with successively shorter prefix strings.
Code
def has_straight(password):ifisinstance(password, str): password = np.array([ord(x) for x in password], dtype=int) differences = np.diff(password)return (1, 1) inzip(differences, differences[1:])r = re.compile(r"(.)\1.*(.)\2")def has_double_pair(password):returnbool(re.search(r, "".join(chr(x) for x in password)))def is_valid_password(password):return has_double_pair(password) and has_straight(password)puzzle_input =tuple(ord(x) for x in"hxbxwxba")password = puzzle_inputcharacters =tuple(ord(x) for x in"abcdefghjkmnpqrstuvwxyz")def iterate(string, prefix_length): n_free =len(string) - prefix_length -1 first = characters[characters.index(string[prefix_length]) +1 :] suffixes = itertools.product(first, *([characters] * n_free))for suffix in suffixes:yield string[:prefix_length] + suffixpassword_iterator = itertools.chain.from_iterable( [iterate(password, l) for l inrange(len(password))][::-1])whilenot is_valid_password(password): password =next(password_iterator)print("".join(chr(x) for x in password))
Part 2
Code
password =next(password_iterator)whilenot is_valid_password(password): password =next(password_iterator)print("".join(chr(x) for x in password))
For the first part, we’ve been promised that integers only appear as integers. So there’s no reason to try and read in the json properly - a simple regex does the trick
Code
s = load(12, "int")sum([n for line in s for n in line])
Part 2
That approach obviously doesn’t work for the second part, so we’ll need a json library
Code
import jsons = json.loads(load(12, "raw"))def find_value(structure):ifisinstance(structure, str):return0ifisinstance(structure, int):return structureifisinstance(structure, list):return(sum(find_value(x) for x in structure))if"red"in structure.values():return0returnsum(find_value(x) for x in structure.values())find_value(s)
data = load(13)def parse(line): words = line.split() people =tuple(sorted([words[0], words[-1][:-1]])) amount =int(re.search("(\d+)", line).groups(0)[0]) sign =2* ("gain"in words) -1return people, amount * signscores = defaultdict(int)for line in load(13): people, score = parse(line) scores[people] += scorepeople =sorted(set([person for pair in scores.keys() for person in pair]))def calculate_score(permutation): score =0 n =len(permutation)for i inrange(n): score += scores[tuple(sorted([permutation[i], permutation[(i +1) % n]]))]return scoremaxval =0for permutation in itertools.permutations(people[1:]): score = calculate_score((people[0],) + permutation)if score > maxval: maxval = scoremaxval
Part 2
Here we see the magic of the defaultdict - since all of the pairs involving “You” have a net score of zero, we don’t need to change the scoring dictionary at all. We just add “You” to the people we are permuting over, and run everything exactly as before.
Since each of the values has to be positive, we can derive some constraints on how much of each ingredient we can use. We know there are 100 of each in total, so letting the four variables be \(w, x, y, z\), we have \(w + x + y + z = 100\). Additionally, since only one ingredient contributes a positive value to any given quantitity we have to use at least one of each. With that out of the way we can use the matrix to set up the following system of inequalities:
For example, the upper bound on \(w\) follows from the last inequality, which implies that \(z > 1.5 w\). The one on \(x\) comes from the first inequality, which implies that \(x < w\).
The last thing to consider is that once three of the values are fixed, the fourth is known. Together, these optimizations let us reduce the cases we have to consider from 1 million to less than 50k.
Code
data = np.array(load(15, "int")).Tinitial_bounds = [[1, 39+1], [1, 39+1], [1, 72+1], [1, 65+1]]def calculate(part=1): maxval =0for w inrange(*initial_bounds[0]):for x inrange(1, w): left, right = initial_bounds[2] new_y =3* (w - x)for y inrange(left, min(right, new_y)): z =100- x - y - w score = (data @ (w, x, y, z))if (score <=0).any() or (part ==2and (score[-1] !=500)):continue val = np.product(score[:-1])if val > maxval: maxval = valreturn maxvalcalculate()
data = load(16)sues = {}for line in data: sep = line.index(":") sue, info = line[:sep], line[sep +1 :] sues[int(sue.split()[1])] = { k: int(v) for k, v inmap(lambda x: x.split(": "), info.split(", ")) }match = {"children": 3,"cats": 7,"samoyeds": 2,"pomeranians": 3,"akitas": 0,"vizslas": 0,"goldfish": 5,"trees": 3,"cars": 2,"perfumes": 1,}for sue in sues: comparison = sues[sue]for key in match:if key notin comparison:continueif match[key] != comparison[key]:breakelse:print(sue)break
Part 2
Code
for sue in sues: comparison = sues[sue]for key in match:if key notin comparison:continue f =lambda known, measured: known == measuredif key in ["cats", "trees"]: f =lambda known, measured: measured > knownelif key in ["pomeranians", "goldfish"]: f =lambda known, measured: measured < knownifnot f(match[key], comparison[key]):breakelse:print(sue)break
data = load(19)transitions, initial_string = data[:-2], data[-1]transitions = [x.split(" => ") for x in transitions]transformations = defaultdict(list)for source, dest in transitions: transformations[source].append(dest)element_regex ="[A-Z][a-z]?"elements = re.findall(element_regex, initial_string)result =set()for idx, element inenumerate(elements): prefix =''.join(elements[:idx]) suffix =''.join(elements[idx+1:])for transformation in transformations[element]: result.add(prefix + transformation + suffix)len(result)
Part 2
Instead of trying to make the final string starting from “e” and using the given transformations, we can equivalently try to reduce the final string to “e” using the reverse transformations - that should be the same thing.
If we’re naive about this, it’s going to take a very long time. One thing to notice is that “Ca” only appears on the right hand side of our transformation rules as “X => XCa” or “X => CaX”. So generating one unit of “Ca” always takes one step, and we can pretend there’s a rule of the form “’∅ => Ca”.
That still leaves us with more than 100k candidates for shortening after only 4 reverse substitutions, which is less than ideal. Luckily, there’s a pen and paper solution!
Looking further at the list of reactions given, all the ones that don’t produce Rn are of the form “A => BC”, and thus always take exactly one step to increase the length of the molecule by one.
The only remaining question is how efficiently we can use the Rn we have available. Now, some of the “Rn” reactions give a “C” to start with, but “C” is not the source of any reaction and it’s not present in our string, so we can completely ignore these. Looking at the remainder, all the reactions convert one element into four, apart from “H => NRnFYFAr” and “Ca => SiRnFYFAr”, which convert one to six. There are only 6 Ys in the initial string, so these reactions have to run exactly six times, and the others run 30 times (There are 36 Rns in my input)
There are 292 elements in the initial string. After getting rid of Rns I have 292 - 5 * 6 - 3 * 30 = 172 elements left, and have spent 36 reactions. Getting to one electron requires a further 171 reactions for a total of 207.
We’re looking for numbers that have lots of divisors compared to how big they are. A bit of ass-pulling lets me guess that they have to be divisible by 60.
Code
target =33100000def sum_of_factors(n, part=1): result =0for i inrange(1, int(np.sqrt(n)) +1):if n % i ==0:if part ==1: result += i +int(n // i)else: div = n // i result += (i if div <=50else0) + (div if i <=50else0)return resultdef run(target, part=1): i, total =0, 0while total < target: i +=60 total = sum_of_factors(i, part)return irun(target /10)
Part 2
The only things that change for part 2 are the target, and the calculation of the sum of factors. That’s most easily done by passing a “part” flag to the sum of factors function, and a “target” parameter to run. Of course, that makes the following look fairly boring:
data = {k: int(v) for k, v inmap(lambda x: x.split(":"), load(20))}turns = [(armor, np.ceil(100/ (data["Damage"] - armor)) -1) for armor inrange(8)]attack_needed = [ np.ceil(data["Hit Points"] / (x[1] +1)) + data["Armor"] for x in turns]equipment = load("20_auxiliary", "int")equipment = equipment[:10] + [x[1:] for x in equipment[10:]]weapons = equipment[:5]armor = equipment[5:10]rings = equipment[10:]# Use itertools to select one weapon, at most one armor and at most two ringsoptions = itertools.product( weapons, itertools.chain([[0, 0, 0]], armor), itertools.chain([[0, 0, 0]], rings, itertools.combinations(rings, 2)),)# The two ring case has to be flattenedoptions = [list(option[:-1]) +list(option[-1]) ifisinstance(option[-1], tuple) else optionfor option in options]# Then we have regular data and can just convert to a numpy array and sumsums = [np.array(option, dtype=int).sum(axis=0) for option in options]# We need the smallest gold value that has enough (damage, armor)min(s[0] for s in sums if s[1] >= attack_needed[min(s[2], 7)])
Part 2
With all that in place, part 2 is just inverting an inequality and changing a min to a max:
Code
max(s[0] for s in sums if s[1] < attack_needed[min(s[2], 7)])
arithmetics = {"inc": lambda x: x +1,"tpl": lambda x: x *3,"hlf": lambda x: int(x //2),}jumps = {"jmp": lambda x: True, "jie": lambda x: (x %2) ==0, "jio": lambda x: x ==1}known_tokens =list(arithmetics.keys()) +list(jumps.keys()) + ["a", "b"]data = [line.replace(",", "").split() for line in load(23)]data = [ [token if token in known_tokens elseint(token) for token in line] for line in data]def run(program, part=1): ip =0 registers = defaultdict(int) registers["a"] =int(part ==2)while ip <len(program): instruction = program[ip]if instruction[0] in arithmetics: registers[instruction[1]] = arithmetics[instruction[0]]( registers[instruction[1]] )else:if jumps[instruction[0]](registers[instruction[1]]): ip += instruction[-1] -1 ip +=1return registersrun(data)["b"]
Part 2
A flag in the “run” function lets us change the relevant register for part 2
Here’s a buggy implementation of part 1. It only looks at the smallest sets that can make the first compartment and completely ignores the others. For the first part that’s semi justifiable, since the amount of small numbers in the input make it very likely that the leftover set can be partitioned into two.
Code
data = load(24, "np")def run(splits =3): target = data.sum() / splits i =1whileTrue:for combination in itertools.combinations(data[::-1], i):ifsum(combination) == target:breakelse: i +=1continuebreakreturnmin(map(lambda x: np.product(x),filter(lambda x: sum(x) == target, itertools.combinations(data[::-1], i))))run()
Part 2
For the second part, the same cheat works, but is less justifiable, since I don’t actually check that the remaining set can be partitioned into three.
def coordinates_to_n(row, column): n = row + column complete_triangles = (n -1) * (n -2) /2returnint(complete_triangles) + columnrow, column =3010, 3019n = coordinates_to_n(row, column)s =20151125for i inrange(n -1): s = (s * (252533)) %33554393s
Source Code
---title: 2015 Solutions---# Imports```{python}import functoolsimport itertoolsimport osimport reimport sysfrom collections import defaultdictfrom queue import PriorityQueueimport more_itertoolsimport numpy as npimport pandas as pdimport scipysys.path.insert(1, os.path.join(sys.path[0], ".."))import utilsload = utils.year_load(2015)```# [Day 1: Not Quite Lisp](https://adventofcode.com/2015/day/1)## Part 1```{python}line = load(1)[0]line.count("(") - line.count(")")```## Part 2```{python}position =0for idx, character inenumerate(line): position = position + (1if character =="("else-1)if position <0:breakidx +1```# [Day 2: I Was Told There Would Be No Math](https://adventofcode.com/2015/day/2)## Part 1```{python}data = load(2)boxes = [[int(x) for x in line.split('x')] for line in data]sum([(3*x*y +2*x*z +2*y*z) for x,y,z inmap(sorted, boxes)])```## Part 2```{python}sum([ x * y * z +2* (x + y) for x,y,z inmap(sorted, boxes)])```# [Day 3: Perfectly Spherical Houses in a Vacuum](https://adventofcode.com/2015/day/3)## Part 1```{python}instructions = load(3)[0]commands = {"^": lambda x, y: (x, y +1), "v": lambda x, y: (x, y -1),">": lambda x, y: (x +1, y), "<": lambda x, y: (x -1, y),}def execute_instructions(instructions, houses=None): position =0, 0if houses isNone: houses = {(0, 0): 1}for instruction in instructions: position = commands[instruction](*position) houses[position] =1return houseslen(execute_instructions(instructions))```## Part 2```{python}houses = execute_instructions(instructions[::2])len(execute_instructions(instructions[1::2], houses))```# [Day 4: The Ideal Stocking Stuffer](https://adventofcode.com/2015/day/4)## Part 1```{python}import hashlibh = hashlib.md5()prefix ="iwrupvqb"def brute_force(n): i =0while i := i +1: md5 = hashlib.md5((prefix +str(i)).encode(encoding="UTF-8")).hexdigest()if md5[:n] == n *"0":return ibrute_force(5)```## Part 2```{python}brute_force(6)```# [Day 5: Doesn't He Have Intern-Elves For This?](https://adventofcode.com/2015/day/5)## Part 1```{python}def is_nice(s): sufficient_vowels =len(re.findall("[aeiou]", s)) >=3 contains_double = (np.array(list(s))[1:] == np.roll(list(s), 1)[1:]).any() contains_forbidden =any(val in s for val in ["ab", "cd", "pq", "xy"])return sufficient_vowels and contains_double andnot contains_forbiddensum(is_nice(x) for x in load(5))```## Part 2```{python}def is_nice(s): contains_double = (np.array(list(s))[2:] == np.roll(list(s), 2)[2:]).any() contains_double_pair =bool(re.findall("(..).*\\1", s))return contains_double and contains_double_pairsum(is_nice(x) for x in load(5))```# [Day 6: Probably a Fire Hazard](https://adventofcode.com/2015/day/6)## Part 1```{python}lines = load(6)numbers = [[int(x) for x in re.findall("\d+", line)] for line in lines]instructions = [line.replace("turn ", "").split()[0] for line in lines]field = np.zeros([1000, 1000], dtype=int)for (x1, y1, x2, y2), instruction inzip(numbers, instructions):if instruction =="toggle": field[x1:x2 +1, y1:y2 +1] ^=1else: field[x1:x2 +1, y1:y2 +1] =int(instruction =="on")field.sum()```## Part 2```{python}field = np.zeros([1000, 1000], dtype=int)for (x1, y1, x2, y2), instruction inzip(numbers, instructions):if instruction =="toggle": field[x1:x2 +1, y1:y2 +1] +=2else: field[x1:x2 +1, y1:y2 +1] +=2*int(instruction =="on") -1 field[np.where(field <0)] =0field.sum()```# [Day 7: Some Assembly Required](https://adventofcode.com/2015/day/7)## Part 1```{python}circuit = {target: source for source, target inmap(lambda x: x.split(" -> "), load(7))}binops = {"AND": lambda x, y: x & y,"OR": lambda x, y: x | y,"LSHIFT": lambda x, y: x << y,"RSHIFT": lambda x, y: x >> y}@functools.cachedef evaluate(symbol):try: result =int(symbol)return resultexceptValueError:pass operation = circuit[symbol].split()iflen(operation) ==1:return evaluate(operation[0])eliflen(operation) ==2:return evaluate(operation[1]) ^ (2**16-1)else: arg1, op, arg2 = operationreturn binops[op](evaluate(arg1), evaluate(arg2))evaluate("a")```## Part 2We can reset everything by clearing out the cache, and setting a wire to a specific value (or expression) can be accomplished by modifying the circuit.That gives```{python}evaluate.cache_clear()circuit["b"] =str(evaluate("a"))evaluate("a")```# [Day 8: Matchsticks](https://adventofcode.com/2015/day/8)## Part 1```{python}lines = [x[:-1] for x load(8)]sum(len(line) -len(eval(line)) for line in lines)```## Part 2```{python}sum(2+len([x for x in line if x in ["\"", "\\"]]) for line in lines)```# [Day 9: All in a Single Night](https://adventofcode.com/2015/day/9)## Part 1```{python}d = {}data = [x.split() for x in load(9)]for source, _, destination, __, distance in data: d[(source, destination)] =int(distance) d[(destination, source)] =int(distance)cities =set(x[0] for x in d.keys())tours = [sum(d[route[start], route[start +1]] for start inrange(len(cities) -1))for route in itertools.permutations(cities)]min(tours)```## Part 2```{python}max(tours)```# [Day 10: Elves Look, Elves Say](https://adventofcode.com/2015/day/10)## Part 1```{python}message ="3113322113"regex = re.compile(r"(([123])\2*)")for _ inrange(40): runs = re.findall(regex, message) message =''.join([str(len(run)) + run[0] for run inmap(lambda x: x[0], runs)])len(message)```## Part 2```{python}for _ inrange(10): runs = re.findall(regex, message) message =''.join([str(len(run)) + run[0] for run inmap(lambda x: x[0], runs)])len(message)```# [Day 11: Corporate Policy](https://adventofcode.com/2015/day/11)## Part 1So there are two jobs here:1. Determine whether a candidate password is valid2. Iterate over candidate passwords in order, starting with the puzzle inputIs valid is not too difficult to accomplish. The "straight" condition can be rewritten as "1, 1" appears somewhere in the list of differences between neighboring characters. The "double pair" condition can be shortly expressed as matching a simple regex. Forbidding certain characters outright is most easily accomplished by never generating them as candidatesTo iterate over candidate passwords, we first construct a helper method to iterate over candidate passwords that keep some prefix string fixed. The full iterator is then a chain over all these with successively shorter prefix strings.```{python}def has_straight(password):ifisinstance(password, str): password = np.array([ord(x) for x in password], dtype=int) differences = np.diff(password)return (1, 1) inzip(differences, differences[1:])r = re.compile(r"(.)\1.*(.)\2")def has_double_pair(password):returnbool(re.search(r, "".join(chr(x) for x in password)))def is_valid_password(password):return has_double_pair(password) and has_straight(password)puzzle_input =tuple(ord(x) for x in"hxbxwxba")password = puzzle_inputcharacters =tuple(ord(x) for x in"abcdefghjkmnpqrstuvwxyz")def iterate(string, prefix_length): n_free =len(string) - prefix_length -1 first = characters[characters.index(string[prefix_length]) +1 :] suffixes = itertools.product(first, *([characters] * n_free))for suffix in suffixes:yield string[:prefix_length] + suffixpassword_iterator = itertools.chain.from_iterable( [iterate(password, l) for l inrange(len(password))][::-1])whilenot is_valid_password(password): password =next(password_iterator)print("".join(chr(x) for x in password))```## Part 2```{python}password =next(password_iterator)whilenot is_valid_password(password): password =next(password_iterator)print("".join(chr(x) for x in password))```# [Day 12: JSAbacusFramework.io](https://adventofcode.com/2015/day/12)## Part 1For the first part, we've been promised that integers only appear as integers. So there's no reason to try and read in the json properly - a simple regex does the trick```{python}s = load(12, "int")sum([n for line in s for n in line])```## Part 2That approach obviously doesn't work for the second part, so we'll need a json library```{python}import jsons = json.loads(load(12, "raw"))def find_value(structure):ifisinstance(structure, str):return0ifisinstance(structure, int):return structureifisinstance(structure, list):return(sum(find_value(x) for x in structure))if"red"in structure.values():return0returnsum(find_value(x) for x in structure.values())find_value(s)```# [Day 13: Knights of the Dinner Table](https://adventofcode.com/2015/day/13)## Part 1```{python}data = load(13)def parse(line): words = line.split() people =tuple(sorted([words[0], words[-1][:-1]])) amount =int(re.search("(\d+)", line).groups(0)[0]) sign =2* ("gain"in words) -1return people, amount * signscores = defaultdict(int)for line in load(13): people, score = parse(line) scores[people] += scorepeople =sorted(set([person for pair in scores.keys() for person in pair]))def calculate_score(permutation): score =0 n =len(permutation)for i inrange(n): score += scores[tuple(sorted([permutation[i], permutation[(i +1) % n]]))]return scoremaxval =0for permutation in itertools.permutations(people[1:]): score = calculate_score((people[0],) + permutation)if score > maxval: maxval = scoremaxval```## Part 2Here we see the magic of the defaultdict - since all of the pairs involving "You" have a net score of zero, we don't need to change the scoring dictionary at all. We just add "You" to the people we are permuting over, and run everything exactly as before.```{python}maxval =0for permutation in itertools.permutations(people[1:] + ["You"]): score = calculate_score((people[0],) + permutation)if score > maxval: maxval = scoremaxval```# [Day 14: Reindeer Olympics](https://adventofcode.com/2015/day/14)## Part 1```{python}reindeer = load(14, "int")def score(time, speed, on, off): cycle_length = on + off n_cycles = time // (cycle_length) offset =min(on, n_cycles % cycle_length)return speed * (n_cycles * on + offset)max(map(lambda x: score(2503, *x), reindeer))```## Part 2```{python}wins = np.zeros(len(numbers))positions = np.zeros(len(numbers))for i inrange(2503):for idx, (speed, on, off) inenumerate(reindeer): cycle_length = on + offif i % cycle_length < on: positions[idx] += speed wins += (positions ==max(positions))max(wins)```# [Day 15: Science for Hungry People](https://adventofcode.com/2015/day/15)## Part 1Since each of the values has to be positive, we can derive some constraints on how much of each ingredient we can use. We know there are 100 of each in total, so letting the four variables be $w, x, y, z$, we have $w + x + y + z = 100$. Additionally, since only one ingredient contributes a positive value to any given quantitity we have to use at least one of each. With that out of the way we can use the matrix to set up the following system of inequalities:```{=latex}\begin{align*} 3w - 3x - y &> 0 \\ 4y - 3z &> 0 \\ -3w + 2z &> 0\end{align*}```From that we can derive the following bounds for the amount of each ingredient```{=latex}\begin{align*}1 &\leq w\leq 39\\1 &\leq x\leq 39\\1 &\leq y\leq 72\\1 &\leq z\leq 65\end{align*}```For example, the upper bound on $w$ follows from the last inequality, which implies that $z > 1.5 w$. The one on $x$ comes from the first inequality, which implies that $x < w$.The last thing to consider is that once three of the values are fixed, the fourth is known. Together, these optimizations let us reduce the cases we have to consider from 1 million to less than 50k.```{python}data = np.array(load(15, "int")).Tinitial_bounds = [[1, 39+1], [1, 39+1], [1, 72+1], [1, 65+1]]def calculate(part=1): maxval =0for w inrange(*initial_bounds[0]):for x inrange(1, w): left, right = initial_bounds[2] new_y =3* (w - x)for y inrange(left, min(right, new_y)): z =100- x - y - w score = (data @ (w, x, y, z))if (score <=0).any() or (part ==2and (score[-1] !=500)):continue val = np.product(score[:-1])if val > maxval: maxval = valreturn maxvalcalculate()```## Part 2```{python}calculate(2)```# [Day 16: Aunt Sue](https://adventofcode.com/2015/day/16)## Part 1```{python}data = load(16)sues = {}for line in data: sep = line.index(":") sue, info = line[:sep], line[sep +1 :] sues[int(sue.split()[1])] = { k: int(v) for k, v inmap(lambda x: x.split(": "), info.split(", ")) }match = {"children": 3,"cats": 7,"samoyeds": 2,"pomeranians": 3,"akitas": 0,"vizslas": 0,"goldfish": 5,"trees": 3,"cars": 2,"perfumes": 1,}for sue in sues: comparison = sues[sue]for key in match:if key notin comparison:continueif match[key] != comparison[key]:breakelse:print(sue)break```## Part 2```{python}for sue in sues: comparison = sues[sue]for key in match:if key notin comparison:continue f =lambda known, measured: known == measuredif key in ["cats", "trees"]: f =lambda known, measured: measured > knownelif key in ["pomeranians", "goldfish"]: f =lambda known, measured: measured < knownifnot f(match[key], comparison[key]):breakelse:print(sue)break```# [Day 17: No Such Thing as Too Much](https://adventofcode.com/2015/day/17)## Part 1```{python}def count(value, containers):if value ==0:return1if value <0orlen(containers) ==0:return0return count(value - containers[0], containers[1:]) + count(value, containers[1:])count(150, load(17, "np"))```## Part 2```{python}def count(value, containers): result = defaultdict(int)def inner(value, containers, depth):if value ==0: result[depth] +=1returnif value <0orlen(containers) ==0:return inner(value - containers[0], containers[1:], depth +1) inner(value, containers[1:], depth) inner(value, containers, 0)return resultresult = count(150, load(17, "np"))result[min(result.keys())]```# [Day 18: Like a GIF For Your Yard](https://adventofcode.com/2015/day/18)## Part 1```{python}weights = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]initial_board = np.array( [[0if char =="."else1for char in line] for line in load(18)])board = initial_board.copy()for i inrange(100): mask = scipy.ndimage.convolve(board, weights, mode="constant") board = ((mask ==3) | ((mask - board) ==3)).astype(int)board.sum()```## Part 2```{python}def fix_corners(board): board = np.roll(board, 1, axis=[0,1]) board[:2, :2] =1return np.roll(board, -1, axis=[0,1])board = fix_corners(initial_board)for i inrange(100): mask = scipy.ndimage.convolve(board, weights, mode='constant') board = fix_corners(((mask ==3) | ((mask - board) ==3)).astype(int))board.sum()```# [Day 19: Medicine for Rudolph](https://adventofcode.com/2015/day/19)## Part 1```{python}data = load(19)transitions, initial_string = data[:-2], data[-1]transitions = [x.split(" => ") for x in transitions]transformations = defaultdict(list)for source, dest in transitions: transformations[source].append(dest)element_regex ="[A-Z][a-z]?"elements = re.findall(element_regex, initial_string)result =set()for idx, element inenumerate(elements): prefix =''.join(elements[:idx]) suffix =''.join(elements[idx+1:])for transformation in transformations[element]: result.add(prefix + transformation + suffix)len(result)```## Part 2Instead of trying to make the final string starting from "e" and using the given transformations, we can equivalently try to reduce the final string to "e" using the reverse transformations - that should be the same thing.If we're naive about this, it's going to take a very long time. One thing to notice is that "Ca" only appears on the right hand side of our transformation rules as "X =\> XCa" or "X =\> CaX". So generating one unit of "Ca" always takes one step, and we can pretend there's a rule of the form "'∅ =\> Ca".That still leaves us with more than 100k candidates for shortening after only 4 reverse substitutions, which is less than ideal. Luckily, there's a pen and paper solution!Looking further at the list of reactions given, all the ones that don't produce Rn are of the form "A =\> BC", and thus always take exactly one step to increase the length of the molecule by one.The only remaining question is how efficiently we can use the Rn we have available. Now, some of the "Rn" reactions give a "C" to start with, but "C" is not the source of any reaction and it's not present in our string, so we can completely ignore these. Looking at the remainder, all the reactions convert one element into four, apart from "H =\> NRnFYFAr" and "Ca =\> SiRnFYFAr", which convert one to six. There are only 6 Ys in the initial string, so these reactions have to run exactly six times, and the others run 30 times (There are 36 Rns in my input)There are 292 elements in the initial string. After getting rid of Rns I have 292 - 5 \* 6 - 3 \* 30 = 172 elements left, and have spent 36 reactions. Getting to one electron requires a further 171 reactions for a total of 207.# [Day 20: Infinite Elves and Infinite Houses](https://adventofcode.com/2015/day/20)## Part 1We're looking for numbers that have lots of divisors compared to how big they are. A bit of ass-pulling lets me guess that they have to be divisible by 60.```{python}target =33100000def sum_of_factors(n, part=1): result =0for i inrange(1, int(np.sqrt(n)) +1):if n % i ==0:if part ==1: result += i +int(n // i)else: div = n // i result += (i if div <=50else0) + (div if i <=50else0)return resultdef run(target, part=1): i, total =0, 0while total < target: i +=60 total = sum_of_factors(i, part)return irun(target /10)```## Part 2The only things that change for part 2 are the target, and the calculation of the sum of factors. That's most easily done by passing a "part" flag to the sum of factors function, and a "target" parameter to run. Of course, that makes the following look fairly boring:```{python}run(target /11, 2)```# [Day 21: RPG Simulator 20XX](https://adventofcode.com/2015/day/21)## Part 1```{python}data = {k: int(v) for k, v inmap(lambda x: x.split(":"), load(20))}turns = [(armor, np.ceil(100/ (data["Damage"] - armor)) -1) for armor inrange(8)]attack_needed = [ np.ceil(data["Hit Points"] / (x[1] +1)) + data["Armor"] for x in turns]equipment = load("20_auxiliary", "int")equipment = equipment[:10] + [x[1:] for x in equipment[10:]]weapons = equipment[:5]armor = equipment[5:10]rings = equipment[10:]# Use itertools to select one weapon, at most one armor and at most two ringsoptions = itertools.product( weapons, itertools.chain([[0, 0, 0]], armor), itertools.chain([[0, 0, 0]], rings, itertools.combinations(rings, 2)),)# The two ring case has to be flattenedoptions = [list(option[:-1]) +list(option[-1]) ifisinstance(option[-1], tuple) else optionfor option in options]# Then we have regular data and can just convert to a numpy array and sumsums = [np.array(option, dtype=int).sum(axis=0) for option in options]# We need the smallest gold value that has enough (damage, armor)min(s[0] for s in sums if s[1] >= attack_needed[min(s[2], 7)])```## Part 2With all that in place, part 2 is just inverting an inequality and changing a min to a max:```{python}max(s[0] for s in sums if s[1] < attack_needed[min(s[2], 7)])```# [Day 22: Wizard Simulator 20XX](https://adventofcode.com/2015/day/22)## Part 1```{python}import queuedef update_effects(effects):returntuple([max(x -2, 0) for x in effects])def apply_effects(state, part=1): own_hp, boss_hp, mana, shield, poison, recharge = statereturn (own_hp - (2if shield else9) - (1if part ==2else0), boss_hp - (6if poison else0), mana + (202if recharge >1else101if recharge else0))def neighbors(state, part=1):if state[0] <=0:return [] new_hp, new_boss_hp, new_mana = apply_effects(state, part=part) new_effects = update_effects(state[-3:]) neighbors = [(53, (new_hp, new_boss_hp -4, new_mana -53) + new_effects), (73, (new_hp +2, new_boss_hp -2, new_mana -73) + new_effects)] costs = [113, 173, 229] durations = [6, 6, 5]for idx, (cost, duration) inenumerate(zip(costs, durations)):if state[2] < cost or new_effects[idx] !=0:continue new_state =list(state).copy() new_state[idx +3] = duration new_hp, new_boss_hp, new_mana = apply_effects(new_state, part=part) new_effects = update_effects(new_state[-3:]) neighbors.append((cost, (new_hp, new_boss_hp, new_mana - cost) + new_effects))return [neighbor for neighbor in neighbors if neighbor[1][2] >=0]initial_state = (50, 58, 500, 0, 0, 0)q = PriorityQueue()q.put((0, initial_state))while q.qsize() >0: cost, state = q.get()if state[1] <=0:breakfor neighbor in neighbors(state): extra_cost, new_state = neighbor q.put((cost + extra_cost, new_state))cost```## Part 2```{python}initial_state = (49, 58, 500, 0, 0, 0)q = PriorityQueue()q.put((0, initial_state))while q.qsize() >0: cost, state = q.get()if state[1] <=0:breakfor neighbor in neighbors(state, part=2): extra_cost, new_state = neighbor q.put((cost + extra_cost, new_state))cost```# [Day 23: Opening the Turing Lock](https://adventofcode.com/2015/day/23)## Part 1```{python}arithmetics = {"inc": lambda x: x +1,"tpl": lambda x: x *3,"hlf": lambda x: int(x //2),}jumps = {"jmp": lambda x: True, "jie": lambda x: (x %2) ==0, "jio": lambda x: x ==1}known_tokens =list(arithmetics.keys()) +list(jumps.keys()) + ["a", "b"]data = [line.replace(",", "").split() for line in load(23)]data = [ [token if token in known_tokens elseint(token) for token in line] for line in data]def run(program, part=1): ip =0 registers = defaultdict(int) registers["a"] =int(part ==2)while ip <len(program): instruction = program[ip]if instruction[0] in arithmetics: registers[instruction[1]] = arithmetics[instruction[0]]( registers[instruction[1]] )else:if jumps[instruction[0]](registers[instruction[1]]): ip += instruction[-1] -1 ip +=1return registersrun(data)["b"]```## Part 2A flag in the "run" function lets us change the relevant register for part 2```{python}run(data, 2)["b"]```# [Day 24: It Hangs in the Balance](https://adventofcode.com/2015/day/24)## Part 1Here's a buggy implementation of part 1. It only looks at the smallest sets that can make the first compartment and completely ignores the others. For the first part that's semi justifiable, since the amount of small numbers in the input make it very likely that the leftover set can be partitioned into two.```{python}data = load(24, "np")def run(splits =3): target = data.sum() / splits i =1whileTrue:for combination in itertools.combinations(data[::-1], i):ifsum(combination) == target:breakelse: i +=1continuebreakreturnmin(map(lambda x: np.product(x),filter(lambda x: sum(x) == target, itertools.combinations(data[::-1], i))))run()```## Part 2For the second part, the same cheat works, but is less justifiable, since I don't actually check that the remaining set can be partitioned into three.```{python}run(4)```# [Day 25: Let It Snow](https://adventofcode.com/2015/day/25)## Part 1```{python}def coordinates_to_n(row, column): n = row + column complete_triangles = (n -1) * (n -2) /2returnint(complete_triangles) + columnrow, column =3010, 3019n = coordinates_to_n(row, column)s =20151125for i inrange(n -1): s = (s * (252533)) %33554393s```