Skip to content

Conversation

codeflash-ai[bot]
Copy link

@codeflash-ai codeflash-ai bot commented Jul 30, 2025

📄 246% (2.46x) speedup for PathFinder.find_shortest_path in src/dsa/various.py

⏱️ Runtime : 2.74 milliseconds 792 microseconds (best of 903 runs)

📝 Explanation and details

The optimized code achieves a 245% speedup through two key algorithmic improvements:

1. Replaced list-based queue with deque for O(1) operations
The original code used queue.pop(0) on a list, which is O(n) because it shifts all remaining elements. The line profiler shows this operation taking 15.4% of total time (2.327ms out of 15.129ms). The optimized version uses collections.deque with popleft(), which is O(1). This change alone eliminates the most expensive operation in the BFS traversal.

2. Eliminated path copying with parent tracking
The original code maintained complete paths by copying and extending them (new_path = list(path) + new_path.append(neighbor)), taking 19.7% of total time (12.6% + 7.1%). With thousands of nodes visited, this creates substantial memory allocation overhead. The optimized version stores only parent pointers in a dictionary and reconstructs the path once at the end, reducing both time and space complexity.

Performance characteristics by test case type:

  • Large linear graphs (1000+ nodes): Show the most dramatic improvement (400-433% faster) because they maximize the impact of both optimizations - many queue operations and long paths to copy
  • Small graphs (2-5 nodes): Actually perform 17-50% slower due to the overhead of importing deque and managing the parent dictionary, but the absolute difference is negligible (microseconds)
  • Dense/branching graphs: Show moderate improvements (10-96% faster) as they benefit from reduced queue overhead but have shorter average path lengths
  • Disconnected graphs: Benefit significantly (286% faster) when no path exists, as the BFS explores many nodes before terminating

The optimization transforms the algorithm from O(V²) time complexity (due to path copying) to O(V + E), making it scale much better for larger graphs while maintaining identical BFS shortest-path semantics.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 101 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 6 Passed
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
import random
import string
from collections import deque

# imports
import pytest  # used for our unit tests
from src.dsa.various import PathFinder

# unit tests

# ------------------- Basic Test Cases -------------------

def test_simple_direct_path():
    # Graph: A -- B
    graph = {'A': ['B'], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 792ns -> 1.17μs (32.1% slower)

def test_simple_no_path():
    # Graph: A   B (no edge)
    graph = {'A': [], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 541ns -> 791ns (31.6% slower)

def test_three_node_path():
    # Graph: A -- B -- C
    graph = {'A': ['B'], 'B': ['C'], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 1.00μs -> 1.29μs (22.5% slower)

def test_multiple_paths_choose_shortest():
    # Graph: A -- B -- D
    #         \      /
    #          C ----
    graph = {
        'A': ['B', 'C'],
        'B': ['D'],
        'C': ['D'],
        'D': []
    }
    pf = PathFinder(graph)
    # Both A->B->D and A->C->D are length 3, so either is valid
    codeflash_output = pf.find_shortest_path('A', 'D'); result = codeflash_output # 1.17μs -> 1.42μs (17.6% slower)

def test_start_equals_end():
    # Start and end are the same node
    graph = {'A': ['B'], 'B': ['A']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'A') # 416ns -> 792ns (47.5% slower)

def test_disconnected_graph():
    # Graph: A -- B   C -- D
    graph = {'A': ['B'], 'B': [], 'C': ['D'], 'D': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D') # 833ns -> 1.04μs (20.0% slower)

# ------------------- Edge Test Cases -------------------

def test_empty_graph():
    # Empty graph
    graph = {}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 125ns -> 125ns (0.000% faster)

def test_start_not_in_graph():
    # Start node missing
    graph = {'B': ['C'], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 125ns -> 125ns (0.000% faster)

def test_end_not_in_graph():
    # End node missing
    graph = {'A': ['B'], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 125ns -> 166ns (24.7% slower)

def test_graph_with_cycle():
    # Graph: A -- B -- C
    #         ^     |
    #         |_____|
    graph = {
        'A': ['B'],
        'B': ['C'],
        'C': ['A']
    }
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 1.00μs -> 1.33μs (25.0% slower)

def test_graph_with_self_loop():
    # Graph: A -- B, B has self-loop
    graph = {'A': ['B'], 'B': ['B']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 792ns -> 1.17μs (32.1% slower)

def test_path_to_self_with_self_loop():
    # Graph: A has self-loop
    graph = {'A': ['A']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'A') # 416ns -> 833ns (50.1% slower)

def test_unreachable_due_to_direction():
    # Directed graph: A -> B, but not B -> A
    graph = {'A': ['B'], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('B', 'A') # 541ns -> 791ns (31.6% slower)

def test_case_sensitive_nodes():
    # Node names are case-sensitive
    graph = {'a': ['b'], 'b': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 125ns -> 125ns (0.000% faster)

def test_large_branching_factor():
    # A connected to many nodes, only one leads to end
    graph = {'A': [chr(ord('B') + i) for i in range(20)]}
    for i in range(20):
        graph[chr(ord('B') + i)] = []
    graph['T'] = []
    graph['A'].append('T')
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'T') # 4.04μs -> 3.33μs (21.2% faster)

def test_graph_with_isolated_node():
    # D is isolated
    graph = {'A': ['B'], 'B': ['C'], 'C': [], 'D': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D') # 1.08μs -> 1.12μs (3.73% slower)

# ------------------- Large Scale Test Cases -------------------

def test_large_linear_graph():
    # Linear graph: A0 -> A1 -> ... -> A999
    N = 1000
    graph = {f'A{i}': [f'A{i+1}'] for i in range(N-1)}
    graph[f'A{N-1}'] = []
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A0', f'A{N-1}'); result = codeflash_output # 690μs -> 129μs (433% faster)

def test_large_complete_graph():
    # Complete graph of 20 nodes: every node connects to every other node
    N = 20
    nodes = [f'N{i}' for i in range(N)]
    graph = {node: [n for n in nodes if n != node] for node in nodes}
    pf = PathFinder(graph)
    # Shortest path between any two nodes is direct
    codeflash_output = pf.find_shortest_path('N0', 'N1') # 2.50μs -> 2.54μs (1.61% slower)

def test_large_sparse_graph_unreachable():
    # 1000 nodes, but only one edge, so most nodes are unreachable
    N = 1000
    graph = {f'N{i}': [] for i in range(N)}
    graph['N0'] = ['N1']
    pf = PathFinder(graph)
    # Path from N0 to N999 is impossible
    codeflash_output = pf.find_shortest_path('N0', 'N999') # 1.00μs -> 1.08μs (7.66% slower)

def test_large_sparse_graph_path_exists():
    # 1000 nodes, N0->N1->...->N999
    N = 1000
    graph = {f'N{i}': [f'N{i+1}'] if i < N-1 else [] for i in range(N)}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('N0', 'N999'); result = codeflash_output # 655μs -> 130μs (401% faster)

def test_large_branching_tree():
    # Binary tree of depth 9 (511 nodes)
    def make_binary_tree(depth):
        graph = {}
        for i in range(2**depth - 1):
            left = 2*i + 1
            right = 2*i + 2
            children = []
            if left < 2**depth - 1:
                children.append(str(left))
            if right < 2**depth - 1:
                children.append(str(right))
            graph[str(i)] = children
        return graph
    graph = make_binary_tree(9)
    pf = PathFinder(graph)
    # Path from root to deepest leftmost leaf
    path = [str(0)]
    node = 0
    for d in range(8):
        node = 2*node + 1
        path.append(str(node))
    codeflash_output = pf.find_shortest_path('0', path[-1]) # 73.8μs -> 48.7μs (51.4% faster)

def test_large_cyclic_graph():
    # 100 nodes in a cycle
    N = 100
    graph = {f'N{i}': [f'N{(i+1)%N}'] for i in range(N)}
    pf = PathFinder(graph)
    # Path from N0 to N50 is 51 nodes
    codeflash_output = pf.find_shortest_path('N0', 'N50'); result = codeflash_output # 10.9μs -> 8.17μs (33.2% faster)

def test_large_graph_random_sparse():
    # Random sparse graph, ensure function doesn't crash and returns plausible result
    N = 500
    random.seed(42)
    nodes = [f'V{i}' for i in range(N)]
    graph = {node: [] for node in nodes}
    # Add 2*N random edges
    for _ in range(2*N):
        a = random.choice(nodes)
        b = random.choice(nodes)
        if a != b and b not in graph[a]:
            graph[a].append(b)
    pf = PathFinder(graph)
    # Path from V0 to V10 may or may not exist, but function should not crash
    codeflash_output = pf.find_shortest_path('V0', 'V10'); result = codeflash_output # 28.7μs -> 19.7μs (45.8% faster)
    # If path exists, it should start with V0 and end with V10
    if result:
        pass

# ------------------- Mutation Testing Sensitivity -------------------

def test_mutation_shortest_path_not_longest():
    # If function returns longest path instead of shortest, this will fail
    graph = {
        'A': ['B', 'C'],
        'B': ['D'],
        'C': ['D'],
        'D': []
    }
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D'); result = codeflash_output # 1.21μs -> 1.46μs (17.1% slower)

def test_mutation_no_path_vs_wrong_path():
    # If function returns a path when none exists, this will fail
    graph = {'A': [], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 500ns -> 791ns (36.8% slower)

def test_mutation_cycles_handling():
    # If function gets stuck in cycles, this will fail (should terminate)
    graph = {'A': ['B'], 'B': ['A']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 792ns -> 1.17μs (32.1% slower)

def test_mutation_correct_path_nodes():
    # If function returns a path with wrong nodes, this will fail
    graph = {'A': ['B'], 'B': ['C'], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C'); result = codeflash_output # 959ns -> 1.25μs (23.3% slower)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

import pytest  # used for our unit tests
from src.dsa.various import PathFinder

# unit tests

# ----------- BASIC TEST CASES -----------

def test_direct_path():
    # Simple direct connection
    graph = {'A': ['B'], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 750ns -> 1.12μs (33.3% slower)

def test_two_step_path():
    # Path with one intermediate node
    graph = {'A': ['B'], 'B': ['C'], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 958ns -> 1.25μs (23.4% slower)

def test_multiple_paths_shortest_chosen():
    # Multiple paths, shortest should be chosen
    graph = {'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D'); result = codeflash_output # 1.17μs -> 1.38μs (15.2% slower)

def test_self_path():
    # Start and end are the same
    graph = {'A': ['B'], 'B': ['A']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'A') # 375ns -> 750ns (50.0% slower)

def test_disconnected_graph():
    # No path exists between start and end
    graph = {'A': ['B'], 'B': [], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 834ns -> 1.04μs (20.0% slower)

def test_no_neighbors():
    # Node exists but has no neighbors
    graph = {'A': [], 'B': ['A']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 541ns -> 750ns (27.9% slower)

def test_graph_with_cycle():
    # Graph contains a cycle
    graph = {'A': ['B'], 'B': ['C'], 'C': ['A', 'D'], 'D': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D') # 1.21μs -> 1.42μs (14.7% slower)

# ----------- EDGE TEST CASES -----------

def test_empty_graph():
    # Graph is empty
    graph = {}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'B') # 84ns -> 125ns (32.8% slower)

def test_start_not_in_graph():
    # Start node not in graph
    graph = {'A': ['B']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('X', 'B') # 125ns -> 125ns (0.000% faster)

def test_end_not_in_graph():
    # End node not in graph
    graph = {'A': ['B']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'X') # 166ns -> 166ns (0.000% faster)

def test_both_start_and_end_not_in_graph():
    # Both start and end nodes not in graph
    graph = {'A': ['B']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('X', 'Y') # 125ns -> 125ns (0.000% faster)

def test_path_to_self_when_not_in_graph():
    # Start and end are the same, but node not in graph
    graph = {'A': ['B']}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('X', 'X') # 125ns -> 125ns (0.000% faster)

def test_graph_with_self_loop():
    # Node has an edge to itself
    graph = {'A': ['A', 'B'], 'B': []}
    pf = PathFinder(graph)
    # Should return ['A'] for self path
    codeflash_output = pf.find_shortest_path('A', 'A') # 416ns -> 833ns (50.1% slower)
    # Should return ['A', 'B'] for A->B
    codeflash_output = pf.find_shortest_path('A', 'B') # 667ns -> 833ns (19.9% slower)

def test_unreachable_due_to_direction():
    # Directed graph, edge only one way
    graph = {'A': ['B'], 'B': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('B', 'A') # 541ns -> 833ns (35.1% slower)

def test_case_sensitivity():
    # Node names are case-sensitive
    graph = {'a': ['b'], 'b': ['c'], 'c': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'C') # 125ns -> 125ns (0.000% faster)

def test_graph_with_isolated_node():
    # Node exists but is not connected
    graph = {'A': ['B'], 'B': [], 'C': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('C', 'A') # 500ns -> 833ns (40.0% slower)

def test_graph_with_multiple_cycles():
    # Multiple cycles in the graph
    graph = {'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A', 'D'], 'D': []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('A', 'D'); result = codeflash_output # 1.25μs -> 1.50μs (16.7% slower)

def test_path_with_non_string_nodes():
    # Nodes are not strings (should still work if keys are hashable)
    graph = {1: [2], 2: [3], 3: []}
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path(1, 3) # 1.04μs -> 1.42μs (26.4% slower)

# ----------- LARGE SCALE TEST CASES -----------

def test_large_linear_graph():
    # Linear graph with 1000 nodes
    N = 1000
    graph = {str(i): [str(i+1)] for i in range(N-1)}
    graph[str(N-1)] = []
    pf = PathFinder(graph)
    codeflash_output = pf.find_shortest_path('0', str(N-1)); path = codeflash_output # 692μs -> 133μs (419% faster)

def test_large_dense_graph():
    # Dense graph: every node connects to every other node with higher index
    N = 100
    graph = {str(i): [str(j) for j in range(i+1, N)] for i in range(N-1)}
    graph[str(N-1)] = []
    pf = PathFinder(graph)
    # Shortest path from 0 to N-1 should be direct: ['0', '99']
    codeflash_output = pf.find_shortest_path('0', str(N-1)) # 112μs -> 102μs (10.0% faster)

def test_large_graph_no_path():
    # Disconnected large graph
    N = 500
    graph = {str(i): [str(i+1)] for i in range(N-1)}
    graph[str(N-1)] = []
    # Add another disconnected component
    graph['X'] = ['Y']
    graph['Y'] = []
    pf = PathFinder(graph)
    # No path between '0' and 'Y'
    codeflash_output = pf.find_shortest_path('0', 'Y') # 222μs -> 57.8μs (286% faster)

def test_large_graph_with_cycle():
    # Large cycle
    N = 300
    graph = {str(i): [str((i+1)%N)] for i in range(N)}
    pf = PathFinder(graph)
    # Shortest path from '0' to '150' should be length 151
    codeflash_output = pf.find_shortest_path('0', '150'); path = codeflash_output # 42.5μs -> 21.6μs (96.7% faster)

def test_large_branching_graph():
    # Tree-like graph: each node branches to two new nodes
    N = 10  # 2^10 = 1024 nodes, but we only build up to 1000 nodes max
    from collections import deque
    graph = {}
    queue = deque(['0'])
    count = 1
    while queue and count < 1000:
        node = queue.popleft()
        left = str(count)
        right = str(count+1)
        graph[node] = [left, right]
        queue.append(left)
        queue.append(right)
        count += 2
    # Fill leaves
    for node in queue:
        graph[node] = []
    pf = PathFinder(graph)
    # Path from root to a leaf
    leaf = str(count-1)
    codeflash_output = pf.find_shortest_path('0', leaf); path = codeflash_output # 176μs -> 99.0μs (77.8% faster)
    # Path should be valid (each step is a neighbor)
    for i in range(len(path)-1):
        pass
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

from src.dsa.various import PathFinder

def test_PathFinder_find_shortest_path():
    PathFinder.find_shortest_path(PathFinder({'': ['\x00\x00', '\x00\x00'], '\x00': [''], '\x01': []}), '', '\x01')

def test_PathFinder_find_shortest_path_2():
    PathFinder.find_shortest_path(PathFinder({}), '', '')

def test_PathFinder_find_shortest_path_3():
    PathFinder.find_shortest_path(PathFinder({'': []}), '', '')

To edit these changes git checkout codeflash/optimize-PathFinder.find_shortest_path-mdpcrce2 and push.

Codeflash

The optimized code achieves a 245% speedup through two key algorithmic improvements:

**1. Replaced list-based queue with deque for O(1) operations**
The original code used `queue.pop(0)` on a list, which is O(n) because it shifts all remaining elements. The line profiler shows this operation taking 15.4% of total time (2.327ms out of 15.129ms). The optimized version uses `collections.deque` with `popleft()`, which is O(1). This change alone eliminates the most expensive operation in the BFS traversal.

**2. Eliminated path copying with parent tracking**
The original code maintained complete paths by copying and extending them (`new_path = list(path)` + `new_path.append(neighbor)`), taking 19.7% of total time (12.6% + 7.1%). With thousands of nodes visited, this creates substantial memory allocation overhead. The optimized version stores only parent pointers in a dictionary and reconstructs the path once at the end, reducing both time and space complexity.

**Performance characteristics by test case type:**
- **Large linear graphs (1000+ nodes)**: Show the most dramatic improvement (400-433% faster) because they maximize the impact of both optimizations - many queue operations and long paths to copy
- **Small graphs (2-5 nodes)**: Actually perform 17-50% slower due to the overhead of importing deque and managing the parent dictionary, but the absolute difference is negligible (microseconds)
- **Dense/branching graphs**: Show moderate improvements (10-96% faster) as they benefit from reduced queue overhead but have shorter average path lengths
- **Disconnected graphs**: Benefit significantly (286% faster) when no path exists, as the BFS explores many nodes before terminating

The optimization transforms the algorithm from O(V²) time complexity (due to path copying) to O(V + E), making it scale much better for larger graphs while maintaining identical BFS shortest-path semantics.
@codeflash-ai codeflash-ai bot added the ⚡️ codeflash Optimization PR opened by Codeflash AI label Jul 30, 2025
@codeflash-ai codeflash-ai bot requested a review from aseembits93 July 30, 2025 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚡️ codeflash Optimization PR opened by Codeflash AI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants