A clean, dependency-free Python library for common BFS, DFS, and grid traversal algorithms. Designed as a reliable toolkit for competitive programming, interview prep, and educational use.
pip install graph-algsThen import directly from the top-level package β no need to reference internal subfolders:
from graph_lib import shortest_path, bfs, lee_algorithm, has_cycle_directedgraph_algorithms/
βββ bfs/
β βββ bfs_general_lib.py # BFS traversal and shortest paths
β βββ bfs_undirected_lib.py # Connectivity for undirected graphs
β βββ bfs_directed_lib.py # Weak connectivity for directed graphs
β βββ grid_bfs_lib.py # Grid BFS β islands, pathfinding, distance problems
βββ dfs/
βββ dfs_general_lib.py # DFS traversal
βββ dfs_undirected_lib.py # Cycle detection for undirected graphs
βββ dfs_directed_lib.py # Cycle detection for directed graphs
Understanding these conventions is essential for using the library correctly.
All functions raise exceptions on invalid or degenerate input rather than returning a sentinel value:
ValueErrorβ empty graph or grid ({},[]), invalid board size (n <= 0)KeyErrorβstart/endnode not present in the graph, out-of-bounds grid coordinates, start or end cell being a wall
bfs({}, 'A') # β ValueError: Graph is empty!
bfs(graph, 'Z') # β KeyError: Vertex Z not in graph!
lee_algorithm(grid, (0,0), (9,9)) # β KeyError: (0, 0) or (9, 9) cell not in grid!
count_islands([]) # β ValueError: Grid is empty!When inputs are structurally valid but the goal is unreachable, functions return None:
shortest_path(graph, 'A', 'Z') # β None (Z unreachable from A)
lee_algorithm(grid, (0,0), (3,3)) # β None (blocked)
rotting_oranges(grid) # β None (some fresh oranges can never rot)The library performs only semantic validation β it checks whether inputs make sense for the algorithm, not whether they conform to a type contract.
β Checked:
- Whether the graph or grid is empty
- Whether
start/endnodes exist in the graph - Whether grid coordinates are in bounds
- Whether start/end cells are traversable (value
0) - Whether board size is positive (knight moves)
β Not checked:
- Whether
graphis actually adict - Whether grid rows are all the same length
- Whether node values are hashable
- Whether
start/endare tuples vs. lists in grid functions
If you pass malformed data (e.g., a list instead of a dict, or a grid with jagged rows), you will get a native Python exception.
bfs(graph, start, visited=None) β setReturns the set of all nodes reachable from start. Accepts an optional visited set to continue a traversal across multiple calls.
bfs_order(graph, start, visited=None) β listReturns nodes in BFS visit order. Accepts an optional shared visited set.
bfs_count(graph, start, visited=None) β intReturns the count of nodes reachable from start. Accepts an optional shared visited set.
bfs_distances(graph, start) β dictReturns a dictionary mapping each reachable node to its shortest distance (in number of edges) from start.
shortest_path(graph, start, end) β list | NoneReturns the shortest path as a node list. Returns [start] if start == end. Returns None if no path exists.
shortest_path_len(graph, start, end) β int | NoneReturns the length (number of edges) of the shortest path. Returns 0 if start == end. Returns None if no path exists.
is_connected_undirected(graph) β boolReturns True if the undirected graph is connected.
count_components_undirected(graph) β intReturns the number of connected components.
get_components_order_undirected(graph) β list[list]Returns a list of components, each as an ordered BFS traversal list.
largest_component_size_undirected(graph) β intReturns the size of the largest component.
shortest_path_len_undirected(graph, start, end) β int | NoneFaster shortest path length using bidirectional BFS. Requires both start and end to be present in the graph. Returns None if no path exists.
Connectivity functions for directed graphs use weak connectivity β the graph is normalised to undirected via normalize() before traversal.
normalize(graph) β dictConverts a directed graph into an undirected one by adding reverse edges. Used internally; safe to call directly.
is_connected_directed(graph) β boolReturns True if the directed graph is weakly connected.
count_components_directed(graph) β intReturns the number of weakly connected components.
get_components_order_directed(graph) β list[list]Returns a list of weakly connected components, each as an ordered BFS traversal list.
largest_component_size_directed(graph) β intReturns the size of the largest weakly connected component.
dfs(graph, start, visited=None) β setRecursive DFS. Returns the set of all visited nodes. Accepts an optional shared visited set.
dfs_order(graph, start, visited=None) β listIterative DFS using an explicit stack. Returns nodes in DFS visit order. Accepts an optional shared visited set.
has_cycle_undirected(graph) β boolDetects cycles in an undirected graph using parent tracking.
has_cycle_directed(graph) β boolDetects cycles in a directed graph using the white/gray/black coloring method.
Grid convention:
0= traversable cell,1= wall/obstacle (exceptdist_to_nearest_oneandrotting_orangeswhich use domain-specific values).
count_islands(grid) β intCounts the number of connected regions of 0-cells (4-directional).
get_islands_order(grid) β list[list[tuple]]Returns all islands as lists of (row, col) coordinates in BFS order.
largest_island_area(grid) β intReturns the cell count of the largest island.
lee_algorithm(grid, start, end) β list[tuple] | NoneFinds the shortest path in a grid from start to end. Returns path as a list of (row, col) tuples. Returns None if unreachable.
lee_algorithm_len(grid, start, end) β int | NoneReturns the length of the shortest grid path. Returns 0 if start == end. Returns None if unreachable.
dist_to_nearest_one(grid) β list[list[int]]Multi-source BFS. Returns a grid where each cell contains its distance to the nearest 1-cell. Cells that are 1 have distance 0.
rotting_oranges(grid) β int | NoneSimulates rotting spread (LeetCode 994). Grid values: 0 = empty, 1 = fresh orange, 2 = rotten orange. Returns minutes until all oranges rot, or None if impossible.
knight_moves(n, start, end) β list[tuple] | NoneFinds the shortest path for a chess knight on an nΓn board. Returns path as (row, col) tuples. Returns None if unreachable.
knight_moves_len(n, start, end) β int | NoneReturns the minimum number of moves for a knight to travel from start to end. Returns None if unreachable.
All graph functions expect an adjacency list as a Python dict.
In a directed graph, nodes that have no outgoing edges don't have to appear as explicit keys β they can exist only as values in other nodes' neighbour lists. This is supported via graph.get(node, []) instead of graph[node] everywhere neighbours are iterated, so a missing key is safely treated as having no outgoing edges.
# β
Both forms are equivalent and accepted
# Explicit β sink nodes listed with empty neighbour lists
graph = {
'A': ['B', 'C'],
'B': ['D'],
'C': [], # sink, explicitly listed
'D': [] # sink, explicitly listed
}
# Compact β sink nodes omitted as keys entirely
graph = {
'A': ['B', 'C'],
'B': ['D'],
# C and D are implied by appearing in neighbour lists
}
β οΈ Exception: functions that validatestartby checkingif start not in graphwill raiseKeyErrorif a sink node is passed asstartand is not an explicit key. If you need to start traversal from a sink, include it explicitly with an empty list.
For undirected graphs, all edges must still be listed in both directions:
graph = {
1: [2, 3],
2: [1, 4],
3: [1],
4: [2]
}Nodes can be any hashable type (strings, integers, tuples, etc.).
Grid functions expect a 2D list of integers:
grid = [
[0, 0, 1, 0],
[1, 0, 0, 0],
[0, 0, 1, 1],
[0, 0, 0, 0],
]Coordinates are always (row, col). All movement is 4-directional (up, down, left, right).
from graph_lib import (
shortest_path, bfs_distances, count_components_undirected, count_components_directed,
dfs_order, has_cycle_directed, has_cycle_undirected,
lee_algorithm, rotting_oranges
)
# Shortest path in an undirected graph
graph = {1: [2, 3], 2: [1, 4], 3: [1], 4: [2]}
print(shortest_path(graph, 1, 4)) # β [1, 2, 4]
# Get distances from a start node
graph = {'A': ['B', 'C'], 'B': ['D'], 'C': [], 'D': []}
print(bfs_distances(graph, 'A')) # β {'A': 0, 'B': 1, 'C': 1, 'D': 2}
# Count weakly connected components in a directed graph
dgraph = {'A': ['B'], 'B': [], 'C': ['D'], 'D': []}
print(count_components_directed(dgraph)) # β 2
# Count connected components in an undirected graph
ugraph = {1: [2], 2: [1], 3: [4], 4: [3]}
print(count_components_undirected(ugraph)) # β 2
# Cycle detection
print(has_cycle_directed({'A': ['B'], 'B': ['A']})) # β True
print(has_cycle_undirected({1: [2], 2: [1, 3], 3: [2]})) # β False
# Grid shortest path
grid = [[0, 0, 0], [1, 1, 0], [0, 0, 0]]
print(lee_algorithm(grid, (0, 0), (2, 2))) # β [(0,0), (0,1), (0,2), (1,2), (2,2)]
# Rotting oranges
grid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]]
print(rotting_oranges(grid)) # β 4| Situation | Return Value |
|---|---|
| Invalid / empty input | ValueError or KeyError |
| Valid input, goal unreachable | None |
start == end (path functions) |
[start] or 0 |
| Success | Result value or list (dict) |
Made with β€οΈ and Python