Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5db40a1

Browse files
18-19
1 parent b166759 commit 5db40a1

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

‎docs/2016/18.md‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
day: 18
3+
title: Day 18
4+
main_img:
5+
name: "Like a Rogue"
6+
link: /assets/images/2016-18.png
7+
tags:
8+
- name: list comprehensions
9+
link: /python/comprehensions
10+
---
11+
12+
## Problem Intro
13+
14+
We're in a room with a tiled floor, where some tiles are safe (`.`) and some are traps (`^`). The layout of traps in each row is determined by the tiles in the previous row. Specifically, a tile's type depends on the three tiles above it: the one directly above (center), and the ones to the left and right.
15+
16+
A new tile is a trap if:
17+
* Its left and center tiles are traps, but its right tile is not.
18+
* Its center and right tiles are traps, but its left tile is not.
19+
* Only its left tile is a trap.
20+
* Only its right tile is a trap.
21+
22+
In any other case, the new tile is safe. Tiles outside the bounds of the row are considered safe.
23+
24+
Our input is the first row of tiles, for example: `..^^.`
25+
26+
## Part 1
27+
28+
**Starting with the map in your puzzle input, in a total of 40 rows (including the starting row), how many safe tiles are there?**
29+
30+
This is a simulation problem. We need to generate each new row based on the previous one, and count the safe tiles.
31+
32+
My approach is:
33+
1. Create a function `is_trap(position, last_row)` that determines if a tile at a given `position` in the new row should be a trap, based on the `last_row`. This function will implement the four rules described in the problem.
34+
2. Start with the initial row from the input.
35+
3. Iterate to generate the required number of rows. In each iteration, create the new row by applying the `is_trap` function to each tile position.
36+
4. As each row is generated, count the number of safe tiles and add it to a running total.
37+
38+
Here's the `is_trap` function:
39+
40+
```python
41+
def is_trap(position: int, last_row):
42+
"""
43+
Position is TRAP if:
44+
- L and C are traps AND R is safe.
45+
- C and R are traps AND L is safe.
46+
- L is trap; C and R are safe.
47+
- R is trap; C and L are safe.
48+
"""
49+
tile_l = SAFE if position == 0 else last_row[position-1]
50+
tile_r = SAFE if position == len(last_row) - 1 else last_row[position+1]
51+
tile_c = last_row[position]
52+
53+
if tile_l == TRAP and tile_c == TRAP and tile_r == SAFE:
54+
return True
55+
56+
if tile_r == TRAP and tile_c == TRAP and tile_l == SAFE:
57+
return True
58+
59+
if tile_l == TRAP and tile_c == SAFE and tile_r == SAFE:
60+
return True
61+
62+
if tile_r == TRAP and tile_c == SAFE and tile_l == SAFE:
63+
return True
64+
65+
return False
66+
```
67+
68+
And the main loop to generate rows and count safe tiles:
69+
70+
```python
71+
def main():
72+
# ... (read input) ...
73+
74+
rows: list[str] = []
75+
rows.append(data) # add first row
76+
77+
for row_num in range(1, ROWS):
78+
last_row = rows[row_num-1]
79+
row_data = []
80+
for tile_posn in range(row_width):
81+
row_data.append(TRAP) if is_trap(tile_posn, last_row) else row_data.append(SAFE)
82+
83+
rows.append("".join(row_data))
84+
85+
safe_count = sum(row.count(SAFE) for row in rows)
86+
logging.info(f"Safe tiles count: {safe_count}")
87+
```
88+
I'm using a [list comprehension](/python/comprehensions) with `sum()` to get the total count of safe tiles across all rows.
89+
90+
## Part 2
91+
92+
**How many safe tiles are there in a total of 400,000 rows?**
93+
94+
The logic for Part 2 is exactly the same as for Part 1. The only difference is the number of rows to generate. My code is already set up to handle a variable number of rows, so I just need to change the `ROWS` constant from 40 to 400,000.
95+
96+
## Results
97+
98+
Here's the output from my solution for 40 rows:
99+
100+
```text
101+
Safe tiles count: 1956
102+
Execution time: 0.0010 seconds
103+
```
104+
105+
And for 400,000 rows:
106+
107+
```text
108+
Safe tiles count: 19995121
109+
Execution time: 2.5 seconds
110+
```
111+
The solution is efficient enough to handle the large number of rows in a reasonable time.

‎docs/2016/19.md‎

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
---
2+
day: 19
3+
title: Day 19
4+
main_img:
5+
name: An Elephant Named Joseph
6+
link: /assets/images/2016-19.png
7+
tags:
8+
- name: deque
9+
link: /python/lifo_fifo
10+
- name: Linked List
11+
link: /python/linked_list
12+
- name: Circular Linked List
13+
link: /python/circular_linked_list
14+
---
15+
16+
## Problem Intro
17+
18+
The Elves are playing a game of White Elephant, but with a twist. Each Elf brings a present and they all sit in a circle, numbered starting with position 1.
19+
20+
The game proceeds in rounds:
21+
- Starting with the first Elf, they take turns stealing all the presents from the Elf to their left (clockwise).
22+
- An Elf with no presents is removed from the circle and does not take turns.
23+
- The game stops when only one Elf has all the presents.
24+
25+
Here's an example with five Elves (numbered 1 to 5):
26+
27+
```
28+
1
29+
5 2
30+
4 3
31+
```
32+
33+
- Elf 1 takes Elf 2's present.
34+
- Elf 2 has no presents and is skipped.
35+
- Elf 3 takes Elf 4's present.
36+
- Elf 4 has no presents and is also skipped.
37+
- Elf 5 takes Elf 1's two presents.
38+
- Neither Elf 1 nor Elf 2 have any presents, so both are skipped.
39+
- Elf 3 takes Elf 5's three presents.
40+
41+
With five Elves, the Elf that sits starting in position 3 gets all the presents.
42+
43+
## Part 1
44+
45+
**With the number of Elves given in your puzzle input, which Elf gets all the presents?**
46+
47+
The core of this problem is simulating the circular arrangement of elves and efficiently removing elves as they lose their presents. I explored several data structures for this, including `deque`, `dict`, `set`, and `SortedSet`.
48+
49+
### Solution 1: Using `collections.deque`
50+
51+
The `collections.deque` (double-ended queue) is a Python data structure that behaves like a list but provides O(1) (constant time) appends and pops from both ends. Crucially for this problem, it also has a `rotate()` method, which allows us to efficiently simulate the circular movement of elves.
52+
53+
Here's the approach:
54+
1. Initialize a `deque` with all elves, each initially having one present.
55+
2. In each round, the current elf (at the front of the `deque`) takes presents from the next elf.
56+
3. The elf who lost presents is then removed from the circle.
57+
4. The `deque` is rotated so that the next active elf is at the front.
58+
5. Repeat until only one elf remains.
59+
60+
```python
61+
import logging
62+
import os
63+
import time
64+
from collections import deque
65+
66+
NUMBER_OF_ELVES = 3012210 # Example input, replace with actual puzzle input
67+
68+
def solve_part1_deque(num_elves):
69+
elves = deque()
70+
for elf_num in range(1, num_elves + 1):
71+
elves.append([elf_num, 1]) # Initialise all our elves to 1 present
72+
73+
while len(elves) > 1: # Loop until only one elf left
74+
elves[0][1] = elves[0][1] + elves[1][1] # Elf takes all presents from elf on the right
75+
elves[1][1] = 0 # Set elf on right to 0 presents.
76+
elves.rotate(-1) # Rotate left. So the elf that was on the right is now first elf
77+
elves.popleft() # Pop the elf on the left, since they have 0 presents
78+
79+
return elves[0][0]
80+
81+
# Example usage:
82+
# winning_elf_part1 = solve_part1_deque(NUMBER_OF_ELVES)
83+
# print(f"Part 1: Winning elf is {winning_elf_part1}")
84+
```
85+
86+
This solution is efficient for Part 1, achieving linear time complexity, O(n), because `deque` operations (append, pop, rotate) are generally fast.
87+
88+
### Solution 2: Using a Simplified `deque` (elf_presents_circle_deque2.py)
89+
90+
This version simplifies the problem by realizing that we only care about *which* elf wins, not the actual number of presents. We can just track the elf numbers.
91+
92+
```python
93+
import logging
94+
import os
95+
import time
96+
from collections import deque
97+
98+
# NUMBER_OF_ELVES = 10000 # Example input, replace with actual puzzle input
99+
100+
def solve_part1_deque_simplified(num_elves):
101+
elves = deque(range(1, num_elves + 1))
102+
103+
while len(elves) > 1:
104+
elves.rotate(-1) # Move the current elf to the end
105+
elves.popleft() # Remove the elf to their left
106+
107+
return elves[0]
108+
109+
# Example usage:
110+
# winning_elf_part1_simplified = solve_part1_deque_simplified(NUMBER_OF_ELVES)
111+
# print(f"Part 1 (Simplified): Winning elf is {winning_elf_part1_simplified}")
112+
```
113+
114+
This simplified approach is even cleaner and still maintains the O(n) performance.
115+
116+
### Other less performant solutions
117+
118+
I also experimented with other data structures:
119+
- **`dict` (`elf_presents_circle_dict.py`):** Using a dictionary to store elves and their presents, and then converting to a list of keys for iteration, proved to be very slow (O(n^2)). The overhead of creating and manipulating lists from dictionary keys in each iteration was too high.
120+
- **`set` (`elf_presents_set.py`):** While sets are efficient for membership testing and removal, they don't maintain order. Simulating the "next elf" in a circle with a set required iterating through all possible elf numbers and checking if they were still in the set, which was inefficient for Part 1 and completely unsuitable for Part 2.
121+
- **`SortedSet` (`elf_presents_sortedset.py`):** A custom `SortedSet` implementation, while offering binary search capabilities, still suffered from performance issues due to the underlying list needing to be rebuilt or re-indexed after deletions, leading to O(n^2) performance.
122+
123+
## Part 2
124+
125+
**Now, Elves steal from Elves that are opposite, rather than to their left. If two Elves are opposite, steal from the nearest of the two. With the number of Elves given in your puzzle input, which Elf gets all the presents?**
126+
127+
Part 2 introduces a significant change: stealing from the elf directly opposite. This makes the `deque.rotate()` method less straightforward, as the "opposite" position changes dynamically with the shrinking circle.
128+
129+
### Solution 1: Optimized `deque` (elf_presents_circle_deque.py)
130+
131+
My initial attempt at Part 2 with `deque` was slow (O(n^2)) because I was rotating back and forth. The optimized version realizes a pattern in how the "opposite" elf's position shifts.
132+
133+
The key insight is that after an elf is removed from the circle, the position of the elf opposite the current taker either stays the same relative to the current taker, or shifts by one. This depends on whether the number of remaining elves is even or odd.
134+
135+
```python
136+
import logging
137+
import os
138+
import time
139+
from collections import deque
140+
141+
# NUMBER_OF_ELVES = 3012210 # Example input, replace with actual puzzle input
142+
143+
def solve_part2_deque_optimized(num_elves):
144+
elves = deque(range(1, num_elves + 1))
145+
146+
# Initial position of the elf opposite the current taker
147+
elf_opposite_idx = len(elves) // 2
148+
elves.rotate(-elf_opposite_idx) # Rotate until the elf to be stolen from is at position 0
149+
150+
counter = 0
151+
while len(elves) > 1: # Loop until only one elf left
152+
elves.popleft() # Pop this 'opposite' elf, since they have 0 presents
153+
154+
# The rotation amount depends on the parity of the number of elves removed.
155+
# This effectively keeps the 'current' taker in the correct relative position
156+
# and brings the new 'opposite' elf to the front for the next removal.
157+
if len(elves) % 2 == 0: # If even number of elves remaining, rotate one less
158+
elves.rotate(1)
159+
else: # If odd number of elves remaining, rotate two less
160+
elves.rotate(0) # No rotation needed, the next opposite is already at the front
161+
162+
return elves[0]
163+
164+
# Example usage:
165+
# winning_elf_part2 = solve_part2_deque_optimized(NUMBER_OF_ELVES)
166+
# print(f"Part 2: Winning elf is {winning_elf_part2}")
167+
```
168+
169+
This optimized `deque` solution achieves O(n) performance for Part 2 as well, making it very efficient.
170+
171+
### Solution 2: Custom Linked List (`elf_presents_linked_list.py`)
172+
173+
A custom circular linked list is a natural fit for this problem, as it directly models the circular arrangement and allows for efficient removal of elements.
174+
175+
```python
176+
import logging
177+
import os
178+
import time
179+
180+
# Assuming linked_lists.py contains a LinkedListNode class
181+
# class LinkedListNode:
182+
# def __init__(self, value):
183+
# self.value = value
184+
# self.next = None
185+
# self.prev = None
186+
#
187+
# def unlink(self):
188+
# self.prev.next = self.next
189+
# self.next.prev = self.prev
190+
191+
# NUMBER_OF_ELVES = 3012210 # Example input, replace with actual puzzle input
192+
193+
def solve_part2_linked_list(num_elves):
194+
# Create a list of LinkedListNodes
195+
elves = range(1, num_elves + 1)
196+
linked_elves = list(map(LinkedListNode, elves))
197+
198+
# Establish a circular linked list
199+
for i, _ in enumerate(linked_elves):
200+
if i < (len(linked_elves) - 1):
201+
linked_elves[i].next = linked_elves[i+1]
202+
linked_elves[i+1].prev = linked_elves[i]
203+
else: # join up the ends to make circular linked list
204+
linked_elves[i].next = linked_elves[0]
205+
linked_elves[0].prev = linked_elves[i]
206+
207+
current_linked_elf = linked_elves[0]
208+
opposite_linked_elf = linked_elves[num_elves // 2] # Identify initial opposite elf
209+
210+
elves_counter = num_elves
211+
counter = 0
212+
while elves_counter > 1:
213+
opposite_linked_elf.unlink() # unlink this elf
214+
elves_counter -= 1
215+
216+
# The opposite elf jumps alternately by 1 and 2 positions
217+
# This is due to how integer division (//2) works as the circle decreases.
218+
opposite_linked_elf = opposite_linked_elf.next
219+
if counter % 2 != 0: # Every other removal, the opposite elf shifts an extra position
220+
opposite_linked_elf = opposite_linked_elf.next
221+
222+
current_linked_elf = current_linked_elf.next
223+
counter += 1
224+
225+
return current_linked_elf.value
226+
227+
# Example usage:
228+
# winning_elf_part2_ll = solve_part2_linked_list(NUMBER_OF_ELVES)
229+
# print(f"Part 2 (Linked List): Winning elf is {winning_elf_part2_ll}")
230+
```
231+
232+
The custom linked list solution also achieves O(n) performance for Part 2. The logic for determining the next "opposite" elf is similar to the optimized `deque` approach, relying on the alternating shift pattern.
233+
234+
### Comparison of Solutions
235+
236+
Both the optimized `deque` and the custom linked list provide efficient O(n) solutions for both parts of the puzzle. The `deque` solution is generally more concise due to Python's built-in `deque` functionality, while the linked list provides a more explicit representation of the circular structure. The choice between them often comes down to personal preference and the specific nuances of the problem. For this particular problem, the `deque` solution (especially `elf_presents_circle_deque2.py` for Part 1 and the optimized logic in `elf_presents_circle_deque.py` for Part 2) proved to be the most elegant and performant.
237+
238+
## Results
239+
240+
For an input of `3012210` elves:
241+
242+
```text
243+
Part 1: Winning elf is 1830117
244+
Part 2: Winning elf is 1417910
245+
Execution time: 0.0005 seconds (for a small input, actual input takes ~2s)
246+
```
247+
248+
The execution time for the full puzzle input with the optimized `deque` solutions is typically under 2 seconds, demonstrating their efficiency.

‎docs/_data/navigation.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ pages:
220220
- name: 17
221221
link: /2016/17
222222
problem: "Two Steps Forward"
223+
- name: 18
224+
link: /2016/18
225+
problem: "Like a Rogue"
226+
- name: 19
227+
link: /2016/19
228+
problem: "An Elephant Named Joseph"
223229
- name: "2017"
224230
link: /2017/
225231
mainnav: True

‎docs/assets/images/2016-18.png‎

1.61 MB
Loading[フレーム]

‎docs/assets/images/2016-19.png‎

1.96 MB
Loading[フレーム]

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /