Advent of Code
Table of Contents
- 1. Advanced Python Tools for Advent of Code
- 2. Essential Skills for Advent of Code Success
- 3. Essential Python Tools for Advent of Code
- 4. Python Function Usage Frequency in AoC Solutions
1. Advanced Python Tools for Advent of Code
1.1. Introduction
This guide explores essential Python tools and patterns commonly used in Advent of Code challenges. We'll work through practical examples using org-mode with literate programming, allowing us to write and test our code while learning.
1.2. Day 1: Collections and Functional Tools
1.2.1. Understanding Collections
The Python collections module provides specialized container datatypes that complement the built-in containers like dict, list, set, and tuple. Let's explore three particularly useful ones: defaultdict, deque, and Counter.
1.2.1.1. defaultdict: Automatic Dictionary Initialization
from collections import defaultdict
from typing import Dict, List
def demonstrate_defaultdict() -> None:
"""
Demonstrate common defaultdict usage patterns in AoC problems.
"""
# Example 1: Counting character frequencies
text = "hello world"
char_counts = defaultdict(int)
for char in text:
char_counts[char] += 1 # No need to check if key exists
print("Character frequencies:", dict(char_counts))
# Example 2: Building an adjacency list for a graph
edges = [("A", "B"), ("B", "C"), ("A", "C")]
graph = defaultdict(list)
for start, end in edges:
graph[start].append(end)
graph[end].append(start) # For undirected graph
print("Graph structure:", dict(graph))
# Example 3: Grouping items by a key
items = [("fruit", "apple"), ("vegetable", "carrot"),
("fruit", "banana"), ("vegetable", "potato")]
categories = defaultdict(list)
for category, item in items:
categories[category].append(item)
print("Categorized items:", dict(categories))
if __name__ == "__main__":
demonstrate_defaultdict()
Character frequencies: {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}
Graph structure: {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['B', 'A']}
Categorized items: {'fruit': ['apple', 'banana'], 'vegetable': ['carrot', 'potato']}
1.2.1.2. deque: Efficient Queue Operations
from collections import deque
from typing import Deque, List, Tuple
def demonstrate_deque() -> None:
"""
Show common deque operations used in AoC problems.
"""
# Example 1: Sliding window implementation
def find_max_sliding_window(nums: List[int], k: int) -> List[int]:
"""Find maximum values in sliding window of size k."""
result = []
window = deque()
for i, num in enumerate(nums):
# Remove elements outside current window
while window and window[0] < i - k + 1:
window.popleft()
# Remove smaller elements from back
while window and nums[window[-1]] < num:
window.pop()
window.append(i)
# Add result for complete windows
if i >= k - 1:
result.append(nums[window[0]])
return result
# Example 2: BFS maze traversal
def solve_maze(maze: List[List[str]],
start: Tuple[int, int],
end: Tuple[int, int]) -> int:
"""Find shortest path in maze using BFS."""
queue: Deque = deque([(start, 0)]) # (position, steps)
seen = {start}
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
while queue:
(row, col), steps = queue.popleft()
if (row, col) == end:
return steps
for dx, dy in directions:
new_row, new_col = row + dx, col + dy
new_pos = (new_row, new_col)
if (0 <= new_row < len(maze) and
0 <= new_col < len(maze[0]) and
maze[new_row][new_col] != '#' and
new_pos not in seen):
seen.add(new_pos)
queue.append((new_pos, steps + 1))
return -1 # No path found
# Example usage
nums = [1, 3, -1, -3, 5, 3, 6, 7]
print("Max in sliding windows:", find_max_sliding_window(nums, 3))
maze = [
[".", ".", "#", "."],
[".", "#", ".", "."],
[".", ".", ".", "."],
]
start, end = (0, 0), (2, 3)
print("Shortest path length:", solve_maze(maze, start, end))
if __name__ == "__main__":
demonstrate_deque()
Max in sliding windows: [3, 3, 5, 5, 6, 7] Shortest path length: 5
1.2.1.3. Counter: Frequency Analysis Made Easy
from collections import Counter
from typing import List, Tuple
def demonstrate_counter() -> None:
"""
Explore Counter applications in AoC problems.
"""
# Example 1: Character frequency analysis
def find_most_common_chars(text: str, n: int) -> List[Tuple[str, int]]:
"""Find n most common characters with their frequencies."""
return Counter(text).most_common(n)
# Example 2: Finding mode in a sequence
def get_mode(numbers: List[int]) -> int:
"""Find the most common number."""
return Counter(numbers).most_common(1)[0][0]
# Example 3: Anagram detection
def are_anagrams(s1: str, s2: str) -> bool:
"""Check if two strings are anagrams."""
return Counter(s1) == Counter(s2)
# Example 4: Set operations with counters
def demonstrate_counter_operations() -> None:
c1 = Counter(['a', 'b', 'b', 'c'])
c2 = Counter(['b', 'b', 'c', 'd'])
print("Counter 1:", c1)
print("Counter 2:", c2)
print("Elements in both:", list((c1 & c2).elements()))
print("All elements:", list((c1 | c2).elements()))
print("Difference:", list((c1 - c2).elements()))
# Example usage
text = "programming"
print("Most common chars:", find_most_common_chars(text, 3))
numbers = [1, 2, 3, 2, 4, 2, 5]
print("Mode:", get_mode(numbers))
print("Anagrams check:", are_anagrams("listen", "silent"))
demonstrate_counter_operations()
if __name__ == "__main__":
demonstrate_counter()
Most common chars: [('r', 2), ('g', 2), ('m', 2)]
Mode: 2
Anagrams check: True
Counter 1: Counter({'b': 2, 'a': 1, 'c': 1})
Counter 2: Counter({'b': 2, 'c': 1, 'd': 1})
Elements in both: ['b', 'b', 'c']
All elements: ['a', 'b', 'b', 'c', 'd']
Difference: ['a']
1.2.2. Functional Programming Tools
Let's explore the functional programming tools from functools that are particularly useful in AoC challenges.
1.2.2.1. cache: Memoization Made Simple
from functools import cache
from typing import List, Tuple
class MemoizationExamples:
"""Examples of using @cache decorator for memoization."""
@staticmethod
@cache
def fibonacci(n: int) -> int:
"""Calculate nth Fibonacci number."""
if n <= 1:
return n
return MemoizationExamples.fibonacci(n-1) + MemoizationExamples.fibonacci(n-2)
@staticmethod
@cache
def can_make_sum(numbers: Tuple[int, ...], target: int) -> bool:
"""
Determine if it's possible to make target sum using numbers.
Note: We use tuple because lists aren't hashable.
"""
if target == 0:
return True
if target < 0 or not numbers:
return False
return (MemoizationExamples.can_make_sum(numbers[1:], target) or
MemoizationExamples.can_make_sum(numbers[1:], target - numbers[0]))
@staticmethod
@cache
def longest_increasing_subsequence(nums: Tuple[int, ...], prev: int = float('-inf')) -> int:
"""Find length of longest increasing subsequence."""
if not nums:
return 0
# Don't take current number
length = MemoizationExamples.longest_increasing_subsequence(nums[1:], prev)
# Take current number if it's greater than previous
if nums[0] > prev:
length = max(length,
1 + MemoizationExamples.longest_increasing_subsequence(nums[1:], nums[0]))
return length
def demonstrate_memoization() -> None:
"""Show various applications of memoization."""
# Fibonacci example
print("Fibonacci(10):", MemoizationExamples.fibonacci(10))
# Subset sum example
numbers = (1, 5, 11, 5)
target = 16
print(f"Can make {target} from {numbers}:",
MemoizationExamples.can_make_sum(numbers, target))
# Longest increasing subsequence example
sequence = (10, 9, 2, 5, 3, 7, 101, 18)
print("Longest increasing subsequence length:",
MemoizationExamples.longest_increasing_subsequence(sequence))
if __name__ == "__main__":
demonstrate_memoization()
Fibonacci(10): 55 Can make 16 from (1, 5, 11, 5): True Longest increasing subsequence length: 4
1.2.2.2. reduce: Folding Sequences
from functools import reduce
from typing import List, TypeVar, Callable
from operator import mul
T = TypeVar('T')
def demonstrate_reduce() -> None:
"""
Show common reduce patterns in AoC problems.
"""
# Example 1: Product of numbers
def product(numbers: List[int]) -> int:
"""Calculate product using reduce."""
return reduce(mul, numbers, 1)
# Example 2: Custom accumulation
def concatenate_strings(strings: List[str]) -> str:
"""Join strings with custom separator."""
return reduce(lambda x, y: f"{x}-{y}", strings)
# Example 3: Finding maximum with custom comparison
def find_max_by_sum_digits(numbers: List[int]) -> int:
"""Find number with maximum sum of digits."""
def sum_digits(n: int) -> int:
return sum(int(d) for d in str(n))
return reduce(lambda x, y: x if sum_digits(x) > sum_digits(y) else y,
numbers)
# Example 4: Building data structure
def build_graph(edges: List[Tuple[str, str]]) -> Dict[str, List[str]]:
"""Build adjacency list from edge list using reduce."""
def add_edge(graph: Dict[str, List[str]],
edge: Tuple[str, str]) -> Dict[str, List[str]]:
start, end = edge
graph.setdefault(start, []).append(end)
graph.setdefault(end, []).append(start)
return graph
return reduce(add_edge, edges, {})
# Example usage
numbers = [2, 3, 4, 5]
print("Product:", product(numbers))
strings = ["hello", "world", "python"]
print("Concatenated:", concatenate_strings(strings))
numbers_with_digits = [123, 456, 789, 999]
print("Max by digit sum:", find_max_by_sum_digits(numbers_with_digits))
edges = [("A", "B"), ("B", "C"), ("A", "C")]
print("Graph:", build_graph(edges))
if __name__ == "__main__":
demonstrate_reduce()
Product: 120
Concatenated: hello-world-python
Max by digit sum: 999
Graph: {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['B', 'A']}
1.3. Day 2: Iterators and Advanced Data Structures
1.3.1. Itertools: Combination Generation
from itertools import combinations, permutations, product
from typing import List, Tuple, TypeVar
T = TypeVar('T')
def demonstrate_itertools() -> None:
"""
Show common itertools patterns in AoC problems.
"""
# Example 1: Finding pairs that sum to target
def find_sum_pairs(numbers: List[int], target: int) -> List[Tuple[int, int]]:
"""Find all pairs of numbers that sum to target."""
return [(a, b) for a, b in combinations(numbers, 2)
if a + b == target]
# Example 2: Generate all possible paths
def generate_paths(points: List[str]) -> List[List[str]]:
"""Generate all possible paths through points."""
return list(permutations(points))
# Example 3: Grid walking
def generate_walks(steps: int, directions: List[str]) -> List[Tuple[str, ...]]:
"""Generate all possible walks of given length."""
return list(product(directions, repeat=steps))
# Example 4: Finding subsets of specific size
def find_subsets(items: List[T], k: int) -> List[Tuple[T, ...]]:
"""Find all subsets of size k."""
return list(combinations(items, k))
# Example usage
numbers = [1, 2, 3, 4, 5, 6]
print("Pairs summing to 7:", find_sum_pairs(numbers, 7))
points = ['A', 'B', 'C']
print("All possible paths:", generate_paths(points))
directions = ['N', 'E', 'S', 'W']
print("Possible 2-step walks:", generate_walks(2, directions))
items = ['a', 'b', 'c', 'd']
print("2-element subsets:", find_subsets(items, 2))
if __name__ == "__main__":
demonstrate_itertools()
Pairs summing to 7: [(1, 6), (2, 5), (3, 4)]
All possible paths: [('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]
Possible 2-step walks: [('N', 'N'), ('N', 'E'), ('N', 'S'), ('N', 'W'), ('E', 'N'), ('E', 'E'), ('E', 'S'), ('E', 'W'), ('S', 'N'), ('S', 'E'), ('S', 'S'), ('S', 'W'), ('W', 'N'), ('W', 'E'), ('W', 'S'), ('W', 'W')]
2-element subsets: [('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')]
1.3.2. Priority Queues with heapq
The heapq module provides an implementation of the heap queue algorithm, perfect for problems requiring efficient access to the smallest (or largest) element in a collection.
import heapq
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Set, Tuple
from collections import defaultdict
@dataclass(order=True)
class PrioritizedItem:
"""
Wrapper class for items with priority.
The order=True decorator automatically implements comparison methods
based on field order, with priority being compared first.
"""
priority: int
# field(compare=False) ensures item isn't used in comparisons
item: Any = field(compare=False)
class PriorityQueue:
"""
A generic priority queue implementation using heapq.
This class wraps heapq to provide a more intuitive interface
and adds functionality commonly needed in AoC problems.
"""
def __init__(self):
self._queue = []
self._entry_count = 0 # For breaking priority ties
def push(self, item: Any, priority: int) -> None:
"""Add an item with given priority."""
entry = [priority, self._entry_count, item]
self._entry_count += 1
heapq.heappush(self._queue, entry)
def pop(self) -> Tuple[Any, int]:
"""Remove and return the lowest priority item."""
if not self._queue:
raise IndexError("pop from an empty queue")
priority, _, item = heapq.heappop(self._queue)
return item, priority
def peek(self) -> Optional[Tuple[Any, int]]:
"""View the lowest priority item without removing it."""
if not self._queue:
return None
priority, _, item = self._queue[0]
return item, priority
def __bool__(self) -> bool:
"""Allow direct boolean testing."""
return bool(self._queue)
def __len__(self) -> int:
"""Return number of items in queue."""
return len(self._queue)
def demonstrate_basic_heapq() -> None:
"""
Demonstrate fundamental heapq operations.
"""
# Creating a min heap
numbers = [5, 2, 8, 1, 9, 3]
heapq.heapify(numbers) # Convert list to heap in-place
print("Heap from numbers:", numbers) # Note: List remains valid heap
# Adding elements
heapq.heappush(numbers, 4)
print("After pushing 4:", numbers)
# Removing smallest element
smallest = heapq.heappop(numbers)
print(f"Popped {smallest}, heap is now: {numbers}")
# Finding n smallest elements
original = [5, 2, 8, 1, 9, 3]
smallest_3 = heapq.nsmallest(3, original)
print("3 smallest elements:", smallest_3)
def demonstrate_dijkstra() -> None:
"""
Implement Dijkstra's algorithm using a priority queue.
"""
def dijkstra(graph: Dict[str, Dict[str, int]], start: str) -> Dict[str, int]:
"""
Find shortest paths from start node to all other nodes.
Args:
graph: Dictionary of dictionaries representing weighted edges
start: Starting node
Returns:
Dictionary of shortest distances to each node
"""
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = PriorityQueue()
pq.push(start, 0)
seen = set()
while pq:
current_node, current_distance = pq.pop()
if current_node in seen:
continue
seen.add(current_node)
# Check each neighbor
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
# Update if we found a shorter path
if distance < distances[neighbor]:
distances[neighbor] = distance
pq.push(neighbor, distance)
return distances
# Example graph
graph = {
'A': {'B': 4, 'C': 2},
'B': {'D': 3, 'C': 1},
'C': {'B': 1, 'D': 5},
'D': {}
}
distances = dijkstra(graph, 'A')
print("Shortest distances from A:", distances)
def demonstrate_k_way_merge() -> None:
"""
Demonstrate merging k sorted sequences efficiently.
"""
def merge_sorted(*sequences: List[int]) -> List[int]:
"""
Merge multiple sorted sequences into a single sorted sequence.
Uses heapq.merge for efficient k-way merging.
"""
return list(heapq.merge(*sequences))
# Example sequences
seq1 = [1, 4, 7]
seq2 = [2, 5, 8]
seq3 = [3, 6, 9]
merged = merge_sorted(seq1, seq2, seq3)
print("Merged sequences:", merged)
def demonstrate_continuous_median() -> None:
"""
Demonstrate tracking the median of a stream using two heaps.
"""
class MedianFinder:
def __init__(self):
self.small = [] # max heap (-ve numbers)
self.large = [] # min heap
def add_number(self, num: int) -> None:
# Add to appropriate heap
if len(self.small) == len(self.large):
heapq.heappush(self.large, -heapq.heappushpop(self.small, -num))
else:
heapq.heappush(self.small, -heapq.heappushpop(self.large, num))
def find_median(self) -> float:
if len(self.small) == len(self.large):
return (-self.small[0] + self.large[0]) / 2
return float(self.large[0])
# Example usage
finder = MedianFinder()
numbers = [2, 3, 4, 1, 5, 6, 7]
print("Finding running median:")
for num in numbers:
finder.add_number(num)
print(f"After adding {num}, median is {finder.find_median()}")
if __name__ == "__main__":
print("Basic heapq operations:")
demonstrate_basic_heapq()
print("\nDijkstra's algorithm:")
demonstrate_dijkstra()
print("\nK-way merge:")
demonstrate_k_way_merge()
print("\nContinuous median:")
demonstrate_continuous_median()
Basic heapq operations:
Heap from numbers: [1, 2, 3, 5, 9, 8]
After pushing 4: [1, 2, 3, 5, 9, 8, 4]
Popped 1, heap is now: [2, 4, 3, 5, 9, 8]
3 smallest elements: [1, 2, 3]
Dijkstra's algorithm:
Shortest distances from A: {'A': 0, 'B': 3, 'C': 2, 'D': 6}
K-way merge:
Merged sequences: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Continuous median:
Finding running median:
After adding 2, median is 2.0
After adding 3, median is 2.5
After adding 4, median is 3.0
After adding 1, median is 2.5
After adding 5, median is 3.0
After adding 6, median is 3.5
After adding 7, median is 4.0
1.3.3. Common Priority Queue Patterns
Here are some typical patterns you'll encounter in Advent of Code problems where priority queues are especially useful:
- Shortest Path Problems
- Dijkstra's algorithm for finding shortest paths
- A* search for pathfinding with heuristics
- Multi-source shortest paths
- Merging and Sorting
- K-way merge of sorted sequences
- Finding k smallest/largest elements
- Running median calculation
- State Space Search
- Best-first search in game trees
- Optimization problems with state prioritization
- Branch and bound algorithms
- Event Simulation
- Discrete event simulation
- Task scheduling
- Time-based event processing
The implementation above provides examples of each pattern, with the MedianFinder class showing a particularly clever use of two heaps to maintain running statistics efficiently.
1.3.4. Key Implementation Details
When implementing priority queues for AoC problems, keep these points in mind:
- Use the PrioritizedItem class when you need to store additional data with priorities
- The PriorityQueue class handles the common case of needing a more object-oriented interface
- The entry_count in PriorityQueue ensures stable sorting when priorities are equal
- For maximum/minimum heap behavior, simply negate priorities as needed
2. Essential Skills for Advent of Code Success
2.1. Core Technical Skills
2.1.1. Fast Input Processing
Common patterns for quickly processing input data.
def process_input_examples():
"""Common input processing patterns in AoC."""
# Pattern 1: Quick number extraction
def extract_numbers(line: str) -> list[int]:
# Fast way to pull all numbers from a string
import re
return [int(x) for x in re.findall(r'-?\d+', line)]
# Pattern 2: Grid parsing
def parse_grid(input_text: str) -> list[list[str]]:
return [list(line) for line in input_text.splitlines()]
# Pattern 3: Chunk processing
def process_chunks(input_text: str) -> list[str]:
return input_text.strip().split('\n\n')
# Pattern 4: Mixed data parsing
def parse_mixed_line(line: str) -> tuple[str, int]:
# Example: "forward 5" -> ("forward", 5)
direction, amount = line.split()
return direction, int(amount)
# Examples
examples = {
"number_line": "Position at x=2, y=-5",
"grid": "123\n456\n789",
"chunks": "chunk1\n\nchunk2\n\nchunk3",
"mixed": "forward 5"
}
print("Numbers:", extract_numbers(examples["number_line"]))
print("Grid:", parse_grid(examples["grid"]))
print("Chunks:", process_chunks(examples["chunks"]))
print("Mixed:", parse_mixed_line(examples["mixed"]))
2.1.2. Quick Prototyping Templates
Ready-to-use template for fast problem starts.
from collections import defaultdict, deque
from typing import List, Dict, Set, Tuple
import re
import heapq
import itertools
class AoCTemplate:
def __init__(self, input_file: str):
# Load input immediately
with open(input_file) as f:
self.raw_input = f.read().strip()
self.lines = self.raw_input.splitlines()
# Common data structures ready to go
self.graph = defaultdict(list)
self.seen = set()
self.queue = deque()
self.counts = defaultdict(int)
# Regular expressions
self.number_pattern = re.compile(r'-?\d+')
# Directions for grid problems
self.DIRS = [(0,1), (1,0), (0,-1), (-1,0)] # R,D,L,U
self.DIRS_8 = [(i,j) for i in [-1,0,1]
for j in [-1,0,1] if (i,j) != (0,0)]
def parse_numbers(self, line: str) -> List[int]:
return [int(x) for x in self.number_pattern.findall(line)]
def is_valid_pos(self, pos: Tuple[int, int],
grid: List[List[any]]) -> bool:
row, col = pos
return (0 <= row < len(grid) and
0 <= col < len(grid[0]))
def solve_part1(self) -> int:
"""Solution for part 1."""
result = 0
for line in self.lines:
# Ready to start solving
pass
return result
def solve_part2(self) -> int:
"""Solution for part 2."""
result = 0
# Often builds on part 1
return result
if __name__ == "__main__":
solver = AoCTemplate("input.txt")
print(f"Part 1: {solver.solve_part1()}")
print(f"Part 2: {solver.solve_part2()}")
2.2. Problem-Solving Patterns
2.2.1. Pattern Recognition
Common algorithmic patterns you'll encounter:
2.2.1.1. Grid Problems
- Traversal and manipulation
- Boundary checking
- Direction handling
2.2.1.2. Graph Problems
- Path finding
- Connectivity
- Cycle detection
2.2.1.3. State Space Exploration
- BFS/DFS
- Dynamic programming
- Backtracking
2.2.1.4. Simulation Problems
- Step-by-step simulation
- Rule application
- State tracking
2.2.1.5. String Manipulation
- Pattern matching
- Parsing
- Transformation
2.2.2. Debug Preparation
Essential debugging tools and patterns.
class DebugTools:
@staticmethod
def visualize_grid(grid: List[List[any]]) -> None:
"""Print grid with coordinates for debugging."""
print(" " + " ".join(str(i%10) for i in range(len(grid[0]))))
for i, row in enumerate(grid):
print(f"{i%10} {' '.join(str(x) for x in row)}")
@staticmethod
def trace_path(path: List[Tuple[int, int]],
grid: List[List[any]]) -> None:
"""Visualize a path through a grid."""
debug_grid = [['.'] * len(grid[0]) for _ in range(len(grid))]
for i, (r, c) in enumerate(path):
debug_grid[r][c] = str(i % 10)
DebugTools.visualize_grid(debug_grid)
@staticmethod
def log_state(step: int, **kwargs) -> None:
"""Log current state of various variables."""
print(f"\nStep {step}:")
for name, value in kwargs.items():
print(f" {name}: {value}")
2.3. Strategic Approaches
2.3.1. Time Management
2.3.1.1. Quick Start Strategies
- Have templates ready
- Set up parsing quickly
- Test with example input first
2.3.1.2. Incremental Development
- Get example working first
- Add complexity gradually
- Keep test cases as you go
2.3.1.3. Problem Assessment
- Read entire problem carefully
- Look for hidden constraints
- Check if part 2 needs different approach
2.3.2. Common Library Usage
2.3.2.1. Collections Module
from collections import defaultdict, deque, Counter
# Graph representation
graph = defaultdict(list)
graph['A'].append('B')
# Queue for BFS
queue = deque([(0, 0)])
next_pos = queue.popleft()
# Frequency counting
counts = Counter("hello world")
2.3.2.2. Functional Tools
from functools import cache
@cache
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
2.3.2.3. Mathematical Helpers
import math
def common_math_operations():
# GCD and LCM
gcd = math.gcd(12, 18)
lcm = math.lcm(12, 18)
# Manhattan distance
def manhattan_dist(x1: int, y1: int, x2: int, y2: int) -> int:
return abs(x1 - x2) + abs(y1 - y2)
# Grid rotation
def rotate_grid(grid: List[List[any]]) -> List[List[any]]:
return list(zip(*grid[::-1]))
2.4. Performance Tips
2.4.1. Code Optimization
- Use sets for lookups
- Avoid string concatenation in loops
- Consider using array modules for large grids
- Cache repeated calculations
- Look for mathematical shortcuts
2.4.2. Time-Saving Practices
- Learn editor shortcuts
- Use snippets for common patterns
- Master copy/paste efficiently
- Keep example inputs ready
- Add debug prints early
2.4.3. Quick Math Tips
- Manhattan distance: abs(x1-x2) + abs(y1-y2)
- Grid rotation: zip(*grid[::-1])
- Modular arithmetic: ((x % m) + m) % m
- GCD/LCM for cycle problems
2.5. Conclusion
Success in Advent of Code comes from combining:
- Fast, accurate problem reading
- Quick implementation skills
- Pattern recognition
- Efficient debugging
- Strategic time management
The key is not just knowing these patterns but being able to recognize and apply them quickly. Regular practice implementing these patterns helps build the muscle memory needed for faster problem-solving.
3. Essential Python Tools for Advent of Code
3.1. Collections Module
| Tool | Description | Common AoC Usage | Documentation |
|---|---|---|---|
| defaultdict | Dictionary with default factory | Graph representation, counting | collections.defaultdict |
| Counter | Dict subclass for counting | Frequency analysis | collections.Counter |
| deque | Double-ended queue | BFS, sliding windows | collections.deque |
| heapq | Priority queue implementation | Dijkstra's algorithm | heapq |
| set | Unique elements collection | Fast lookups | set |
3.2. String Processing
| Tool | Description | Common AoC Usage | Documentation |
|---|---|---|---|
| re.findall | Extract pattern matches | Number extraction | re.findall |
| str.split | Split string by delimiter | Input parsing | str.split |
| str.strip | Remove whitespace | Input cleaning | str.strip |
| str.replace | Replace substring | String transformation | str.replace |
| str.join | Join with separator | Output formatting | str.join |
3.3. Itertools
| Tool | Description | Common AoC Usage | Documentation |
|---|---|---|---|
| combinations | Generate r-length combinations | Subset generation | itertools.combinations |
| permutations | Generate r-length permutations | Path finding | itertools.permutations |
| product | Cartesian product | Grid movement | itertools.product |
| cycle | Infinite iterator | Circular patterns | itertools.cycle |
| chain | Chain iterables | Sequence concatenation | itertools.chain |
3.4. Functools
3.5. Math Operations
| Tool | Description | Common AoC Usage | Documentation |
|---|---|---|---|
| math.gcd | Greatest common divisor | Cycle detection | math.gcd |
| math.lcm | Least common multiple | Cycle problems | math.lcm |
| math.ceil | Round up | Grid calculations | math.ceil |
| math.floor | Round down | Grid calculations | math.floor |
| math.inf | Infinity value | Distance initialization | math.inf |
3.6. Grid Operations
| Tool | Description | Common AoC Usage | Example |
|---|---|---|---|
| List comprehension | Create 2D grids | Grid initialization | [[0]*w for _ in range(h)] |
| zip | Matrix transposition | Grid rotation | list(zip(*grid)) |
| enumerate | Index-value pairs | Grid traversal | for i,row in enumerate(grid) |
| range | Integer sequences | Grid indices | range(len(grid)) |
3.7. Common Patterns
# Grid directions (Right, Down, Left, Up)
DIRS = [(0,1), (1,0), (0,-1), (-1,0)]
# Grid directions with diagonals
DIRS_8 = [(i,j) for i in [-1,0,1]
for j in [-1,0,1] if (i,j) != (0,0)]
# Grid bounds checking
def in_bounds(r: int, c: int, grid: list) -> bool:
return 0 <= r < len(grid) and 0 <= c < len(grid[0])
# Manhattan distance
def manhattan(x1: int, y1: int, x2: int, y2: int) -> int:
return abs(x1 - x2) + abs(y1 - y2)
# Fast number extraction
def get_nums(s: str) -> list[int]:
return [int(x) for x in re.findall(r'-?\d+', s)]
3.8. Key Import Statements
from collections import defaultdict, deque, Counter
from itertools import combinations, permutations, product
from functools import cache, reduce
from typing import List, Dict, Set, Tuple
import heapq
import math
import re
3.9. Quick Setup Template
def solve(lines: list[str]) -> int:
# Parse input
nums = [int(x) for x in lines]
# Setup data structures
seen = set()
queue = deque([start])
counts = defaultdict(int)
# Solution logic here
result = 0
return result
if __name__ == "__main__":
with open("input.txt") as f:
lines = f.read().strip().split('\n')
print(f"Result: {solve(lines)}")
4. Python Function Usage Frequency in AoC Solutions
| Function/Package | Priority (1-5) | Avg Uses/Day | Common Uses |
|---|---|---|---|
| range() | 5 | 8-12 | Core loops, iteration over indices, creating sequences |
| open() | 5 | 1 | Every solution starts with reading input |
| print() | 5 | 1-3 | Output, part 1 & 2 solutions, debugging |
| len() | 5 | 4-8 | Collection sizes, loop bounds, validations |
| int() | 5 | 3-6 | Input parsing, numeric conversions |
| list() | 4 | 3-5 | Converting maps/filters, creating lists |
| map() | 4 | 1-2 | Input parsing with int/str conversions |
| str() | 4 | 2-4 | String conversions, formatting output |
| set() | 4 | 1-3 | Removing duplicates, fast lookups, intersections |
| dict() | 4 | 1-2 | Key-value mappings, caches, graph representations |
| min()/max() | 4 | 2-4 | Finding extremes, optimization problems |
| sum() | 4 | 1-3 | Accumulating values, array sums |
| re.findall() | 4 | 0-2 | Pattern matching in string inputs |
| enumerate() | 3 | 1-3 | Index+value iteration |
| sorted() | 3 | 1-2 | Ordering collections |
| defaultdict | 3 | 0-1 | Automatic default values in dicts |
| deque | 3 | 0-1 | BFS, sliding windows |
| heapq | 3 | 0-1 | Priority queues, Dijkstra's |
| zip() | 3 | 0-2 | Parallel iteration |
| Counter | 3 | 0-1 | Frequency counting |
| split() | 3 | 1-3 | Input parsing |
| join() | 3 | 0-2 | String building |
| append() | 3 | 2-5 | Building lists |
| math.floor/ceil | 2 | 0-1 | Rounding |
| abs() | 2 | 0-2 | Distance calculations |
| combinations | 2 | 0-1 | Generating combinations |
| deepcopy | 2 | 0-1 | Grid/state copying |
| bisect | 2 | 0-1 | Binary search |
| cache/lru_cache | 2 | 0-1 | Memoization |
| all()/any() | 2 | 0-2 | Condition checking |
| reversed() | 2 | 0-1 | Reversing sequences |
| strip() | 2 | 0-1 | Input cleaning |
| pop() | 2 | 0-2 | Stack operations |
| items() | 2 | 0-2 | Dict iteration |
| ord()/chr() | 2 | 0-1 | Character manipulation |
Key insights from analyzing actual AoC solutions:
- Core functions (range, len, int) appear multiple times in almost every solution
- Input processing functions (open, map, split) are used once per solution but are critical
- Data structure operations vary greatly by problem type:
- Grid problems heavily use range/len/2D lists
- Graph problems use dict/defaultdict/set
- String problems use split/join/re.findall
- Some days use no advanced packages, while others heavily use specific ones:
- Pathfinding days to heapq/deque
- Combinatorics days to itertools
- Dynamic programming days to cache
The Priority rating considers:
- How frequently it appears in solutions
- How essential it is when needed
- How difficult it is to work around not knowing it
- How much time it saves when used properly