Table of Contents
- Advanced Python Tools for Advent of Code
- Essential Skills for Advent of Code Success
- Essential Python Tools for Advent of Code
- Python Function Usage Frequency in AoC Solutions
Advanced Python Tools for Advent of Code
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.
Day 1: Collections and Functional Tools
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.
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']}
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
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']
Functional Programming Tools
Let's explore the functional programming tools from functools that are particularly useful in AoC challenges.
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
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']}
Day 2: Iterators and Advanced Data Structures
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')]
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
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.
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 entrycount in PriorityQueue ensures stable sorting when priorities are equal
- For maximum/minimum heap behavior, simply negate priorities as needed
Essential Skills for Advent of Code Success
Core Technical Skills
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"]))
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()}")
Problem-Solving Patterns
Pattern Recognition
Common algorithmic patterns you'll encounter:
Grid Problems
- Traversal and manipulation
- Boundary checking
- Direction handling
Graph Problems
- Path finding
- Connectivity
- Cycle detection
State Space Exploration
- BFS/DFS
- Dynamic programming
- Backtracking
Simulation Problems
- Step-by-step simulation
- Rule application
- State tracking
String Manipulation
- Pattern matching
- Parsing
- Transformation
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}")
Strategic Approaches
Time Management
Quick Start Strategies
- Have templates ready
- Set up parsing quickly
- Test with example input first
Incremental Development
- Get example working first
- Add complexity gradually
- Keep test cases as you go
Problem Assessment
- Read entire problem carefully
- Look for hidden constraints
- Check if part 2 needs different approach
Common Library Usage
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")
Functional Tools
from functools import cache @cache def fibonacci(n: int) -> int: if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2)
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]))
Performance Tips
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
Time-Saving Practices
- Learn editor shortcuts
- Use snippets for common patterns
- Master copy/paste efficiently
- Keep example inputs ready
- Add debug prints early
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
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.
Essential Python Tools for Advent of Code
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 |
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 |
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 |
Functools
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 |
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)) |
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)]
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
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)}")
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/lrucache | 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 → heapq/deque
- Combinatorics days → itertools
- Dynamic programming days → 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