summaryrefslogtreecommitdiff
path: root/19/main.py
diff options
context:
space:
mode:
Diffstat (limited to '19/main.py')
-rwxr-xr-x19/main.py128
1 files changed, 128 insertions, 0 deletions
diff --git a/19/main.py b/19/main.py
new file mode 100755
index 0000000..120a44a
--- /dev/null
+++ b/19/main.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+
+from random import shuffle
+
+
+
+def parse_input(lines):
+ rules = {}
+ for line in lines[0:-2]:
+ words = line.split()
+ k = words[0]
+ if k not in rules:
+ rules[k] = []
+ rules[k].append(words[2])
+
+ molecule = lines[-1]
+ return rules, molecule
+
+
+
+# Given a "parent" molecule, find all "child" molecules
+# that can be made from it with exactly one replacement.
+def find_children(rules, parent):
+ result = []
+ # For each replaceable string
+ for k in rules.keys():
+ # For each occurrence in the given molecule
+ p = 0
+ while p < len(parent):
+ i = parent.find(k, p)
+ if i != -1:
+ # For each replacement of the replaceable string
+ for r in rules[k]:
+ m = parent[0 : i] + r + parent[i + len(k) : ]
+ if m not in result:
+ result.append(m)
+ p = i + 1
+ else: # ran out of occurrences
+ break
+ return result
+
+
+
+# Given a "child" molecule, find all "parent" molecules
+# that can be produce it using exactly one replacement.
+def find_parents(rules, child):
+ result = []
+ # For each replaceable string
+ for k in rules.keys():
+ # For each replacement of the replaceable string
+ for r in rules[k]:
+ # For each occurrence in the given molecule
+ p = 0
+ while p < len(child):
+ i = child.find(r, p)
+ if i != -1:
+ m = child[0 : i] + k + child[i + len(r) : ]
+ if m not in result:
+ result.append(m)
+ p = i + 1
+ else: # ran out of occurrences
+ break
+ return result
+
+
+
+def solve_part1(lines):
+ rules, start = parse_input(lines)
+ result = find_children(rules, start)
+ return len(result)
+
+
+
+def solve_part2(lines):
+ rules, target = parse_input(lines)
+ molecules = [target]
+
+ # Compared to part 1, we do part 2 in reverse: from the target molecule,
+ # we work backwards through its history until we encounter "e"; because
+ # we go back one generation at a time, the first "e" must be the answer.
+ depth = 0
+ while "e" not in molecules:
+
+ # Find all ancestors of this generation; there could be HUGELY many
+ ancestors = []
+ for m in molecules:
+ parents = find_parents(rules, m)
+ for p in parents:
+ if p not in ancestors:
+ ancestors.append(p)
+
+ # Extremely greedy algorithm: only keep shortest candidates,
+ # because they're more likely to lie on the shortest path.
+ # However, this may give the wrong answer: the optimal path
+ # may involve some strategic locally non-optimal steps.
+ min_len = min(map(len, ancestors))
+ shortest = []
+ for p in ancestors:
+ if len(p) == min_len:
+ shortest.append(p)
+ ancestors = shortest
+
+ # Sledgehammer solution to keep the runtime under control,
+ # since it turns out the above greedy approach isn't enough.
+ # Fortunately, the puzzle input is well-chosen to allow this.
+ if len(ancestors) > 42:
+ shuffle(ancestors)
+ ancestors = ancestors[0 : 42]
+
+ molecules = ancestors
+ depth += 1
+
+ return depth
+
+
+
+def main():
+ # Read replacement rules and medicine molecules from input text file
+ with open("input.txt", "r") as f:
+ lines = f.read().splitlines()
+
+ print("Part 1 solution:", solve_part1(lines)) # 509 for me
+ print("Part 2 solution:", solve_part2(lines)) # 195 for me
+
+
+
+if __name__ == "__main__":
+ main()