-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpuzzle_solutions.py
2620 lines (1965 loc) · 69.6 KB
/
puzzle_solutions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Library imports
import copy
import itertools
import math
import networkx as nx
import numpy as np
import random
import re
import sympy as sym
from collections import defaultdict, namedtuple, Counter
from copy import deepcopy
from dataclasses import dataclass
from enum import Enum
from functools import reduce, cmp_to_key, cache, partial
from matplotlib.path import Path
from queue import PriorityQueue
###
# Puzzle 1
###
## Part A
# Read the input data
with open("input1.txt", "r") as f:
content = f.readlines()
# Utility function to find digits within the input string
def find_digit(content):
first_dig = None
last_dig = None
# Process each character individually
for char in content:
# We want to know if it is a digit
if char.isdigit():
if first_dig is None:
first_dig = char
last_dig = char
return int(first_dig + last_dig)
# Perform it on all lines
all_results = [find_digit(line) for line in content]
# Total up the results
total = sum(all_results)
## Answer
print(f"Puzzle 1 Part A: {total}")
## Part B
# Get the digits from 1 to 9
numbers_to_consider = [str(i) for i in range(1, 10)]
# Dictionary mapping numbers to words
number_to_word = {
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
}
# Creating the reverse dictionary
word_to_number = {v: k for k, v in number_to_word.items()}
# Using list comprehension to spell out the first 10 digits
spelled_out_numbers = [number_to_word[i] for i in range(1, 10)]
# Bind the two dictionaries together
full_list = numbers_to_consider + spelled_out_numbers
# Utility function to find spelled out numbers
def find_spelled_out_numbers(i, s, numbers):
found = []
# Process every token individually
for i in range(len(s)):
for number in numbers:
# Check if the number is in the string
if s.startswith(number, i):
if number in word_to_number:
number = word_to_number[number]
found.append((i, number))
return found
## Now let's go through all the results
nums = [find_spelled_out_numbers(i, s, full_list) for i, s in enumerate(content)]
nums_filt = [(num[0], num[len(num) - 1]) for num in nums]
nums_add = [int(str(num[0][1]) + str(num[1][1])) for num in nums_filt]
# Produce the sum of the results
total = sum(nums_add)
## Answer
print(f"Puzzle 1 Part B: {total}")
###
# Puzzle 2
###
## Parts A and B
# Read the new input data
with open("input2.txt", "r") as f:
content = f.readlines()
# Split on the colon and semicolon delimiters
content2 = [x.split(": ")[1] for x in content]
content3 = [x.split("; ") for x in content2]
# Set the limits of the number of cubes for each color
game_map = {"red": 12, "green": 13, "blue": 14}
pattern = "([0-9]+) (blue|green|red)"
# Loop through the games and find the impossible ones
impossible = []
all_mins = []
for i, game in enumerate(content3):
# Tabulate every sample for this game
res = [re.findall(pattern, x) for x in game]
res2 = [item for sublist in res for item in sublist]
# For each game, track the possible minimums
mins = {"red": -1, "green": -1, "blue": -1}
# Now loop over each and set as appropriate
for r in res2:
# Print games which exceed the specified limit
if game_map[r[1]] < int(r[0]):
impossible.append(i + 1)
# Print games where the minimum is exceeded
if mins[r[1]] < int(r[0]):
mins[r[1]] = int(r[0])
all_mins.append(mins)
# Remove any duplicates
impossible = list(set(impossible))
# These are the ones that are possible
print(f"Puzzle 2 Part A: {sum(range(1, len(content3) + 1)) - sum(impossible)}")
# Compute the powers of each
powers = [x["red"] * x["blue"] * x["green"] for x in all_mins]
# Print the answer to Part B
print(f"Puzzle 2 Part B: {sum(powers)}")
###
# Puzzle 3
###
## Part A
# Read in the input data
with open("input3.txt", "r") as f:
content = f.readlines()
# Split on the new line, convert each token to a character
content_clean = [x.split("\n")[0] for x in content]
content_clean2 = [list(x) for x in content_clean]
# Find positions of the symbols
symbol_pos = []
num_pos = []
id = -1
for i in range(len(content_clean2)):
first_digit = None
# Loop row-wise over all the input
for j in range(len(content_clean2[i])):
char = content_clean2[i][j]
# Case 1: We have a symbol
if not char.isdigit() and char != ".":
symbol_pos.append((i, j))
first_digit = None
# Case 2: We have a digit, and it's the start of a new number
elif char.isdigit() and first_digit is None:
id = id + 1
first_digit = char
num_pos.append((i, j, id))
# Case 3: We have a digit, and it's not the start of a new number
elif char.isdigit():
num_pos.append((i, j, id))
# Case 4: We have the "." character
else:
first_digit = None
valid_ids = []
gear_ids = []
# Loop over the positions of the numbers and symbols
for i, j, id in num_pos:
for i2, j2 in symbol_pos:
# Check if the symbol is at most one coord away in both directions
if abs(i - i2) <= 1 and abs(j - j2) <= 1:
valid_ids.append(id)
gear_ids.append((str(i2) + str(j2), id))
# Remove duplicates
valid_ids = list(set(valid_ids))
# Creating a dictionary to store tuples based on the last integer
groups = {}
for tup in num_pos:
key = tup[-1] # Get the last element of the tuple
if key not in groups:
groups[key] = []
groups[key].append(tup)
# Extracting lists from the dictionary
grouped_lists = list(groups.values())
str_list = []
full_str_list = []
# For every pair of numbers, concatenate them into a string
for lst in grouped_lists:
my_str = ""
for chr in lst:
my_str = my_str + str(content_clean2[chr[0]][chr[1]])
full_str_list.append(my_str)
# Only add to the list if the number is valid
if lst[0][2] not in valid_ids:
continue
str_list.append(int(my_str))
print(f"Puzzle 3 Part A: {sum(str_list)}")
# Creating a dictionary to store tuples based on the last integer
groups_gears = {}
for tup in gear_ids:
key = tup[0] # Get the last element of the tuple
if key not in groups_gears:
groups_gears[key] = []
if tup[1] not in groups_gears[key]:
groups_gears[key].append(tup[1])
# Check the groups_gears
gears_list = []
for gid, id in groups_gears.items():
if len(id) == 2:
gears_list.append(id)
# Now compute the power ratios
ratios = []
for glist in gears_list:
ratios.append(int(full_str_list[glist[0]]) * int(full_str_list[glist[1]]))
print(f"Puzzle 3 Part B: {sum(ratios)}")
###
# Puzzle 4
###
## Part A
# Read the input data
with open("input4.txt", "r") as f:
content = f.readlines()
# Split on the new line, convert each token to a character
content_clean = [x.split("\n")[0] for x in content]
content_clean2 = [x.split(": ")[1] for x in content_clean]
content_clean3 = [x.split(" | ") for x in content_clean2]
content_clean4 = [[x.split(" ") for x in y] for y in content_clean3]
content_clean5 = [
[[item for item in inner_list if item] for inner_list in outer_list]
for outer_list in content_clean4
]
# Store a map of all the matches by card
matches_map = {}
# Now time to score
scores = []
for i, game in enumerate(content_clean5):
# The numbers + winning numbers form the matches
my_numbers = game[0]
winning_numbers = game[1]
# Do a set match
matches = list(set(my_numbers) & set(winning_numbers))
if i not in matches_map:
matches_map[i] = list(range(i + 1, i + 1 + len(matches)))
# Append scores if greater than 0 matches
if len(matches) > 0:
scores.append(2 ** (len(matches) - 1))
print(f"Puzzle 4 Part A: {sum(scores)}")
## Part B
# Utility function to recursively process cards
def recursive_card_process(card, matches_map):
# Check if the card is in the map
if card not in matches_map or len(matches_map[card]) == 0:
return 0
# Get the matches for a given card
matches = matches_map[card]
# Count them, and recursively process every single one
return len(matches) + sum(
[recursive_card_process(match, matches_map) for match in matches]
)
# Add the original count of cards to the result of the recursive processing
final_cards = len(matches_map) + sum(
[recursive_card_process(i, matches_map) for i in matches_map.keys()]
)
print(f"Puzzle 4 Part B: {final_cards}")
###
# Puzzle 5
###
## Part A
# Read the input data
with open("input5.txt", "r") as f:
content = f.readlines()
# Get the actual seed values
seeds = content[0].split(": ")[1].strip().split(" ")
# Find the locations of the new lines
newline_locs = [ind for ind, x in enumerate(content) if x == "\n"]
# Grab all the data in the puzzle
seed_to_soil = [
x.strip().split(" ") for x in content[(newline_locs[0] + 2) : newline_locs[1]]
]
soil_to_fertilizer = [
x.strip().split(" ") for x in content[(newline_locs[1] + 2) : newline_locs[2]]
]
fertilizer_to_water = [
x.strip().split(" ") for x in content[(newline_locs[2] + 2) : newline_locs[3]]
]
water_to_light = [
x.strip().split(" ") for x in content[(newline_locs[3] + 2) : newline_locs[4]]
]
light_to_temperature = [
x.strip().split(" ") for x in content[(newline_locs[4] + 2) : newline_locs[5]]
]
temperature_to_humidity = [
x.strip().split(" ") for x in content[(newline_locs[5] + 2) : newline_locs[6]]
]
humidity_to_location = [
x.strip().split(" ") for x in content[(newline_locs[6] + 2) : len(content)]
]
# Primary utility function which takes a set of rules and builds a dictionary
def process_map(rules, flip=False):
rel_ind = 1 - int(flip)
class MyCustomDict(dict):
def __init__(self, rules, *args, **kwargs):
self.rules = rules
super().__init__(*args, **kwargs)
def __missing__(self, key):
for rule in rules:
if int(rule[rel_ind]) <= int(key) < int(rule[rel_ind]) + int(rule[2]):
return int(key) + int(rule[1 - rel_ind]) - int(rule[rel_ind])
return int(key)
# Create a defaultdict where the default value is the key itself
my_custom_dict = MyCustomDict(rules)
return my_custom_dict
# Build dictionaries for all mappings
sts_map = process_map(seed_to_soil)
stf_map = process_map(soil_to_fertilizer)
ftw_map = process_map(fertilizer_to_water)
wtl_map = process_map(water_to_light)
ltt_map = process_map(light_to_temperature)
tth_map = process_map(temperature_to_humidity)
htl_map = process_map(humidity_to_location)
# Apply to all seeds
all_locs = [
htl_map[tth_map[ltt_map[wtl_map[ftw_map[stf_map[sts_map[int(s)]]]]]]] for s in seeds
]
print(f"Puzzle 5 Part A: {min(all_locs)}")
## Part B
# Now build the reverse dictionaries
sts_map_rev = process_map(seed_to_soil, flip=True)
stf_map_rev = process_map(soil_to_fertilizer, flip=True)
ftw_map_rev = process_map(fertilizer_to_water, flip=True)
wtl_map_rev = process_map(water_to_light, flip=True)
ltt_map_rev = process_map(light_to_temperature, flip=True)
tth_map_rev = process_map(temperature_to_humidity, flip=True)
htl_map_rev = process_map(humidity_to_location, flip=True)
# Utility function that, for a given seed value, checks if its in the range of any of the seed rules
def in_seed_range(seed_value, seed_rules):
for rule in seed_rules:
if int(rule[0]) <= int(seed_value) < int(rule[0]) + int(rule[1]):
return True
return False
# Get the seed rules
seed_rules = [[seeds[i], seeds[i + 1]] for i in range(0, len(seeds), 2)]
# Now we brute force:
i = 0
while True:
# Get the seed value
seed_val = sts_map_rev[
stf_map_rev[ftw_map_rev[wtl_map_rev[ltt_map_rev[tth_map_rev[htl_map_rev[i]]]]]]
]
# If it's in the range... we're done!
if in_seed_range(seed_val, seed_rules):
print(f"Puzzle 5 Part B: {i}")
break
# Otherwise, increment and try again
i += 1
###
# Puzzle 6
###
## Part A
# Read the input data
with open("input6.txt", "r") as f:
content = f.readlines()
# Get the race data
race_data = [x.split()[1:] for x in content]
times = race_data[0]
distances = race_data[1]
# Utility function which finds the winning ways for a given time
def find_winning_ways(time, distance):
distances_traveled = []
# Try each speed possible
for speed in range(1, time):
# We are driving for the remaining time
time_driving = time - speed
# It is a possible record if the time driving times the speed exceeds the distance
if time_driving * speed > distance:
distances_traveled.append(time_driving * speed)
return distances_traveled
# Process each race
race_results = []
for i in range(len(times)):
race_results.append(len(find_winning_ways(int(times[i]), int(distances[i]))))
# Multiply the results together
result = reduce(lambda x, y: x * y, race_results)
print(f"Puzzle 6 Part A: {result}")
## Part B
# Get the new values by merging the strings
new_time = int(reduce(lambda x, y: str(x) + str(y), times))
new_distance = int(reduce(lambda x, y: str(x) + str(y), distances))
# Call our utility function
distances_traveled = find_winning_ways(new_time, new_distance)
# Get the length
result = len(distances_traveled)
print(f"Puzzle 6 Part B: {result}")
###
# Puzzle 7
###
## Part A
# Read the input data
with open("input7.txt", "r") as f:
content = f.readlines()
# Map possible results to scores / strength
strength_map = {
"five_of_kind": 7,
"four_of_kind": 6,
"full_house": 5,
"three_of_kind": 4,
"two_pair": 3,
"one_pair": 2,
"high_card": 1,
}
# Utility function to score a hand
def score_hand(cards):
# Five of a cind
if len(set(cards)) == 1:
return strength_map["five_of_kind"]
elif len(set(cards)) == 2:
# Four of a kind
if cards.count(cards[0]) in [1, 4]:
return strength_map["four_of_kind"]
# Full house
else:
return strength_map["full_house"]
elif len(set(cards)) == 3:
# Check whether we have a three of a kind or two pair
if cards.count(cards[0]) in [1, 3] and cards.count(cards[1]) in [1, 3]:
return strength_map["three_of_kind"]
else:
return strength_map["two_pair"]
elif len(set(cards)) == 4:
return strength_map["one_pair"]
else:
return strength_map["high_card"]
# Define the card values for sorting
card_values = {
"2": "02",
"3": "03",
"4": "04",
"5": "05",
"6": "06",
"7": "07",
"8": "08",
"9": "09",
"T": "10",
"J": "11",
"Q": "12",
"K": "13",
"A": "14",
}
# Utility function that compares two hands
def compare_hands(data, reverse=False):
def compare(a, b):
# Get each character of the string
a_list = list(a)
b_list = list(b)
# Get the numerical values of each card
a_num = [card_values[x] for x in a_list]
b_num = [card_values[x] for x in b_list]
# Combine them into one big number
a_result = int(reduce(lambda x, y: x + y, a_num))
b_result = int(reduce(lambda x, y: x + y, b_num))
# Sort the number to determine which wins
if a_result > b_result:
return 1
elif a_result == b_result:
return 0
else:
return -1
# Get a lambda key from this
custom_sort = cmp_to_key(compare)
return sorted(data, key=custom_sort, reverse=reverse)
# Get the rank order of the strength of hand scores
def rank_order(data):
# Sort the data in ascending order
sorted_data = compare_hands(data)
# Create a dictionary to map each string to its rank
rank_map = {}
for i, item in enumerate(sorted_data):
rank_map[item] = i + 1
# Return the rank order of the original data
return [rank_map[item] for item in data]
# Break any ties among groups of same strength
def break_ties(camel_scores):
# Create a dictionary to group tuples by their second value
grouped_data = defaultdict(list)
for key, value in camel_scores:
grouped_data[value].append((key, value))
# Convert the dictionary values to lists of lists
result = list(grouped_data.values())
# Loop over each list set
final_scores = []
for res in result:
if len(res) == 1:
final_scores.append((res[0][0], res[0][1], 1))
else:
# Get the actual strings associated with each
hands = [camel[x[0]][0] for x in res]
ranks = rank_order(hands)
# Append the rank order to the final scores
for i, r in enumerate(ranks):
final_scores.append((res[i][0], res[i][1], r))
return final_scores
# Utility function to process scores into ranks
def process_scores(camel_scores):
# Sort the scores in ascending order
camel_scores_sorted = sorted(camel_scores, key=lambda x: x[1])
final_results = []
prev_value = -1
cur_rank = 0
# For every sorted score, we check if we have ties
for css in camel_scores_sorted:
if css[1] == prev_value:
cur_rank = cur_rank - 1
# Rank it
prev_value = css[1]
cur_rank += 1
# Append the rank
final_results.append((css[0], cur_rank))
return final_results
# Call the scoring procedure
camel = [x.strip().split() for x in content]
camel_scores = [(i, score_hand(list(x[0]))) for i, x in enumerate(camel)]
camel_scoreranks = process_scores(camel_scores)
camel_fullscores = break_ties(camel_scoreranks)
# Now, let's sort the final list
camel_sorted = sorted(camel_fullscores, key=lambda x: (x[1], x[2]))
# Final winnings
winnings = []
for i, result in enumerate(camel_sorted):
winnings.append((i + 1) * int(camel[result[0]][1]))
print(f"Puzzle 7 Part A: {sum(winnings)}")
## Part B
# Set the new value of Joker to be 1
card_values["J"] = "01"
# Score hands by trying joker values
def score_hand_joker(cards):
cards_set = list(set(cards))
# If there's no joker, same score as before
if "J" not in cards_set:
return score_hand(cards)
# If the hand is ALL jokers, return best score possible
if "J" in cards_set and len(cards_set) == 1:
return strength_map["five_of_kind"]
# Build a list of all possible hands with jokers
possible_cards = [cards]
for card in cards_set:
if card != "J":
# Get a new possible set
new_cards = [card if x == "J" else x for x in cards]
possible_cards.append(new_cards)
# Score them all and take the highest score
all_scores = [score_hand(cards) for cards in possible_cards]
return max(all_scores)
camel_scores = [(i, score_hand_joker(list(x[0]))) for i, x in enumerate(camel)]
camel_scoreranks = process_scores(camel_scores)
camel_fullscores = break_ties(camel_scoreranks)
# Now, let's sort the final list
camel_sorted = sorted(camel_fullscores, key=lambda x: (x[1], x[2]))
# Final winnings
winnings = []
for i, result in enumerate(camel_sorted):
winnings.append((i + 1) * int(camel[result[0]][1]))
print(f"Puzzle 7 Part B: {sum(winnings)}")
###
# Puzzle 8
###
## Part A
# Read the input data
with open("input8.txt", "r") as f:
content = f.readlines()
def get_node_mapping(line):
# Splitting the line at '=' and stripping any whitespace
key, value = [x.strip() for x in line.split("=")]
# Removing parentheses and splitting the value part at ','
values = value.strip("()").split(", ")
# Creating the desired dictionary structure
result = {key: {"L": values[0], "R": values[1]}}
return result
# Read the directions to take
directions = list(content[0].strip())
# Get the node mappings
node_mappings_raw = [get_node_mapping(line) for line in content[2:]]
node_mappings = reduce(lambda a, b: {**a, **b}, node_mappings_raw)
# Begin the traversal
steps = 0
cur_node = "AAA"
while True:
if cur_node == "ZZZ":
print(f"Puzzle 8 Part A: {steps}")
break
# Get the direction that we are taking
direction = directions[steps % len(directions)]
# Get the node we are going to
cur_node = node_mappings[cur_node][direction]
# Increment the number of steps
steps += 1
## Part B
# Track the paths simultaneously
cur_nodes = [x for x in node_mappings.keys() if x.endswith("A")]
cur_result = [{"cur_node": node, "steps": 0, "done": False} for node in cur_nodes]
# Flag for whether we are still lost
lost = True
dir_string = ""
while lost:
# Process each path
for i, node_data in enumerate(cur_result):
# No need to process anything if we've found the shortest path
if cur_result[i]["done"]:
continue
# Get the direction that we are taking
direction = directions[node_data["steps"] % len(directions)]
# Get where we're going next
next_node = node_mappings[node_data["cur_node"]][direction]
# Step there!
cur_result[i] = {
"cur_node": next_node,
"steps": node_data["steps"] + 1,
"done": next_node.endswith("Z"),
}
# End condition: all nodes took a step, all are on an end node
if all([x["done"] for x in cur_result]):
lost = False
break
# Find the LCM of two numbers
def lcm_of_two_numbers(a, b):
return abs(a * b) // math.gcd(a, b)
# Generalize to many numbers
def lcm(numbers):
return reduce(lcm_of_two_numbers, numbers)
# Print the result
print(f"Puzzle 8 Part B: {lcm([x['steps'] for x in cur_result])}")
###
# Puzzle 9
###
## Part A
# Read the input data
with open("input9.txt", "r") as f:
content = f.readlines()
# Clean it up
content = [x.strip() for x in content]
content_split = [[int(y) for y in x.split(" ")] for x in content]
# Process each report one by one
final_scores = []
final_scores_front = []
for report in content_split:
report_differences = [report]
while True:
# Get the new line of interest
cur_report = report_differences[len(report_differences) - 1]
report_difference = []
# Loop to the end, computing the differences between each number
for i in range(len(cur_report) - 1):
report_difference.append(cur_report[i + 1] - cur_report[i])
# Append to our list
report_differences.append(report_difference)
# Check if all report differences are zero
if all([x == 0 for x in report_difference]):
break
# Now we process what the next value is:
final_value = 0
last_value = 0
# And do the same for the front of the sequence
final_value_front = 0
last_value_front = 0
for i in range(len(report_differences) - 2, 0, -1):
# Part A
last_value = last_value + report_differences[i][len(report_differences[i]) - 1]
final_value = (
last_value + report_differences[i - 1][len(report_differences[i - 1]) - 1]
)
# Part B
last_value_front = report_differences[i][0] - last_value_front
final_value_front = report_differences[i - 1][0] - last_value_front
final_scores.append(final_value)
final_scores_front.append(final_value_front)
print(f"Puzzle 9 Part A: {sum(final_scores)}")
print(f"Puzzle 9 Part B: {sum(final_scores_front)}")
###
# Puzzle 10
###
## Part A
# Read the input data
with open("input10.txt", "r") as f:
content = f.readlines()
# Split into a list of lists
content_split = [list(x.strip()) for x in content]
# Find the starting position
starting_coord = (-1, -1)
for j, row in enumerate(content_split):
for i, char in enumerate(row):
# We found the start
if char == "S":
starting_coord = (i, j)
break
# Build a map between directions and their corresponding coordinates
direction_map = {"N": (0, -1), "S": (0, 1), "E": (1, 0), "W": (-1, 0)}
pipe_map = {
"N": ["|", "7", "F"],
"S": ["|", "J", "L"],
"E": ["-", "7", "J"],
"W": ["-", "F", "L"],
}
pipe_map_rev = {
"|": ["N", "S"],
"-": ["E", "W"],
"7": ["S", "W"],
"F": ["S", "E"],
"J": ["N", "W"],
"L": ["N", "E"],
}
avoid_backwards_map = {"N": "S", "S": "N", "E": "W", "W": "E"}
# Utility function to take a single step
def take_step(current_pos, last_step=None):
for direction, offset in direction_map.items():
if (
last_step in avoid_backwards_map
and direction == avoid_backwards_map[last_step]
):
continue
# Get the character we are on, skip if it's not a valid character
cur_char = content_split[current_pos[1]][current_pos[0]]
if cur_char != "S" and direction not in pipe_map_rev[cur_char]:
continue
# Compute the new position
new_pos = (current_pos[0] + offset[0], current_pos[1] + offset[1])
# Check if the new position is valid
if (
new_pos[0] >= 0
and new_pos[0] < len(content_split[0])
and new_pos[1] >= 0
and new_pos[1] < len(content_split)
):
# Now we must check if the new position is a valid character
if content_split[new_pos[1]][new_pos[0]] in pipe_map[direction]:
return new_pos, direction
return new_pos, None
my_polygon = [starting_coord] # For part B later
# Perform the full game
def take_loop():
# Initialize the current position
current_pos = starting_coord
last_step = None
# Loop until we find the end
steps = 0
while True:
# Take a step
current_pos, last_step = take_step(current_pos, last_step)
# Increment the number of steps
steps += 1
# Append to the polygon
my_polygon.append(current_pos)
# Check if we are at the end
if last_step is None:
break
return current_pos, steps
# Print the puzzle result
print(f"Puzzle 10 Part A: {int(take_loop()[1] / 2)}")
## Part B