Table of Contents

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:

  1. Shortest Path Problems
    • Dijkstra's algorithm for finding shortest paths
    • A* search for pathfinding with heuristics
    • Multi-source shortest paths
  2. Merging and Sorting
    • K-way merge of sorted sequences
    • Finding k smallest/largest elements
    • Running median calculation
  3. State Space Search
    • Best-first search in game trees
    • Optimization problems with state prioritization
    • Branch and bound algorithms
  4. 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:

  1. Use the PrioritizedItem class when you need to store additional data with priorities
  2. The PriorityQueue class handles the common case of needing a more object-oriented interface
  3. The entrycount in PriorityQueue ensures stable sorting when priorities are equal
  4. 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

Tool Description Common AoC Usage Documentation
cache Memoization decorator Dynamic programming @functools.cache
reduce Reduce sequence Sequence aggregation functools.reduce
partial Create partial function Callback customization functools.partial

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:

  1. Core functions (range, len, int) appear multiple times in almost every solution
  2. Input processing functions (open, map, split) are used once per solution but are critical
  3. 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
  4. 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

Author: Jason Walsh

jwalsh@nexus

Last Updated: 2025-07-30 13:45:27

build: 2025-12-23 09:12 | sha: e32f33e