Python Graph Algorithms Library

Logarithma

High-performance graph algorithms in Python — from classic Dijkstra to the cutting-edge Breaking the Sorting Barrier SSSP (Duan et al., 2025). Built on NetworkX. Production-ready, fully typed, comprehensively tested.

v0.6.0 Python 3.8+ MIT License 339 Tests Cython Accelerated

Installation

Install the latest stable version from PyPI:

$ pip install logarithma

To include visualization support (Matplotlib + Plotly):

$ pip install logarithma[viz]

Requirements: Python 3.8+, NumPy ≥ 1.20, NetworkX ≥ 2.6

Install from Source

bash
git clone https://github.com/softdevcan/logarithma.git
cd logarithma
pip install -e ".[dev,viz]"

Quick Start

python
import logarithma as lg
import networkx as nx

# Build a weighted graph
G = nx.Graph()
G.add_edge('A', 'B', weight=4)
G.add_edge('A', 'C', weight=2)
G.add_edge('B', 'C', weight=1)
G.add_edge('B', 'D', weight=5)
G.add_edge('C', 'D', weight=8)

# Dijkstra — shortest distances from A
distances = lg.dijkstra(G, 'A')
print(distances)
# {'A': 0, 'B': 3, 'C': 2, 'D': 8}

# A* — point-to-point with heuristic
result = lg.astar(G, 'A', 'D')
print(result['distance'])  # 8
print(result['path'])      # ['A', 'C', 'B', 'D']

# Bellman-Ford — supports negative weights
DG = nx.DiGraph()
DG.add_edge('X', 'Y', weight=4)
DG.add_edge('Y', 'Z', weight=-2)
result = lg.bellman_ford(DG, 'X')
print(result['distances'])  # {'X': 0, 'Y': 4, 'Z': 2}

# BFS traversal
visited = lg.bfs(G, 'A')
print(visited)  # {'A': 0, 'B': 1, 'C': 1, 'D': 2}

# Kruskal MST
mst = lg.kruskal_mst(G)
print(mst['total_weight'])   # minimum spanning tree weight

# Max Flow
DG = nx.DiGraph()
DG.add_edge('S', 'A', capacity=10)
DG.add_edge('A', 'T', capacity=10)
flow = lg.max_flow(DG, 'S', 'T')
print(flow['max_flow_value'])  # 10

# Tarjan SCC
DG2 = nx.DiGraph([('A','B'), ('B','C'), ('C','A')])
sccs = lg.tarjan_scc(DG2)
print(sccs)   # [['A', 'B', 'C']]

# Floyd-Warshall — all-pairs shortest paths
result = lg.floyd_warshall(G)
print(result['distances']['A']['D'])   # shortest distance A→D

# Johnson's — all-pairs for sparse graphs
result = lg.johnson(G)
print(result['distances']['A']['D'])   # same result, faster on sparse graphs

Algorithms

Dijkstra
O(E + V log V)
Classic single-source shortest path. Non-negative weights. Directed & undirected.
Use for: GPS navigation, network routing, weighted graph distance queries.
A* (A-Star)
O(b^d) — heuristic-guided
Faster point-to-point with Euclidean, Manhattan or Haversine heuristic.
Use for: Game pathfinding, robot navigation, map routing with coordinates.
Bellman-Ford
O(V · E)
Negative-weight edges. Negative-cycle detection with full cycle info.
Use for: Currency arbitrage detection, financial modeling, distributed routing (RIP).
Bidirectional Dijkstra
O(E + V log V) — ~2× faster
Simultaneous forward/backward search. Ideal for large point-to-point queries.
Use for: Long-distance routing, social network distance, large road networks.
Floyd-Warshall
O(V³)
All-pairs shortest paths via dynamic programming. Supports negative weights.
Use for: Dense graphs, transitive closure, graph diameter, distance matrices.
Johnson's
O(V² log V + VE)
All-pairs shortest paths optimized for sparse graphs. Bellman-Ford + Dijkstra.
Use for: Sparse graphs with negative weights, better than Floyd-Warshall when E ≪ V².
Breaking Barrier SSSP
O(m log²/³ n) — beyond sorting barrier
First Python impl. of Duan et al. 2025. Directed SSSP faster than Dijkstra asymptotically.
Use for: Research, large-scale directed sparse graphs, benchmarking against Dijkstra.
BFS
O(V + E)
Level-order traversal. Optimal for unweighted shortest paths.
Use for: Social network degrees of separation, web crawling, shortest hop-count.
DFS
O(V + E)
Depth-first traversal. Cycle detection. Recursive & iterative modes.
Use for: Maze solving, topological ordering, cycle detection, backtracking problems.
Kruskal MST
O(E log E)
Minimum Spanning Tree via Union-Find. Undirected graphs.
Use for: Network cable layout, clustering, circuit design, minimum-cost connectivity.
Prim MST
O(E + V log V)
MST via greedy min-heap expansion. Undirected graphs.
Use for: Dense graphs where Kruskal is slower, real-time MST construction.
Max Flow (Edmonds-Karp)
O(V · E²)
Maximum flow via BFS-based augmenting paths. Directed capacity graphs.
Use for: Supply chain optimization, bipartite matching, image segmentation.
Tarjan SCC
O(V + E)
Strongly connected components. Iterative DFS-based implementation.
Use for: Deadlock detection, compiler optimization, social network analysis.
Topological Sort
O(V + E)
Linear ordering of DAG vertices. DFS and Kahn's algorithm support.
Use for: Build systems (make/npm), task scheduling, course prerequisite planning.

Dijkstra

Classic single-source shortest path algorithm for graphs with non-negative edge weights. Works on both directed and undirected graphs.

🎯
When to use: Your default choice for shortest-path queries when all edge weights are non-negative. Ideal for GPS navigation, network routing protocols (OSPF), and weighted graph distance queries. If you have coordinates and a single target, consider A* instead. If you need all-pairs distances, use Floyd-Warshall or Johnson's.

dijkstra(graph, source)

Returns a dictionary of shortest distances from source to all vertices.

python
import logarithma as lg
import networkx as nx

G = nx.Graph()
G.add_edge('A', 'B', weight=4)
G.add_edge('A', 'C', weight=2)
G.add_edge('B', 'C', weight=1)
G.add_edge('B', 'D', weight=5)

distances = lg.dijkstra(G, 'A')
# {'A': 0, 'C': 2, 'B': 3, 'D': 8}

# Works with directed graphs too
DG = nx.DiGraph()
DG.add_edge('A', 'B', weight=1)
DG.add_edge('B', 'C', weight=2)
distances = lg.dijkstra(DG, 'A')
# {'A': 0, 'B': 1, 'C': 3}

dijkstra_with_path(graph, source, target=None)

Returns both distances and the actual paths.

python
result = lg.dijkstra_with_path(G, 'A', 'D')
print(result['distances']['D'])  # 8
print(result['paths']['D'])      # ['A', 'C', 'B', 'D']

# All paths from source
result = lg.dijkstra_with_path(G, 'A')
for node, path in result['paths'].items():
    print(f"{node}: {path}")

A* (A-Star)

Heuristic-guided shortest path algorithm. Uses an admissible heuristic h(n) to direct the search toward the goal, expanding far fewer nodes than Dijkstra for point-to-point queries.

🎯
When to use: Best for point-to-point queries where you have spatial coordinates (2D/3D positions, lat/lon). Use cases: game pathfinding (NPCs, tiles), robot navigation, map routing with known positions. Falls back to Dijkstra when no heuristic is given.
ℹ️
Optimality guarantee: A* finds the optimal path if and only if the heuristic is admissible — it never overestimates the true distance to the goal. All built-in heuristics satisfy this condition.

astar(graph, source, target, heuristic=None)

python
from logarithma import astar, euclidean_heuristic, manhattan_heuristic

# ── Euclidean heuristic (2D coordinate graphs) ──────────────────
G = nx.Graph()
pos = {'A': (0, 0), 'B': (3, 0), 'C': (3, 4), 'D': (6, 4)}
G.add_edge('A', 'B', weight=3)
G.add_edge('B', 'C', weight=4)
G.add_edge('A', 'C', weight=10)
G.add_edge('C', 'D', weight=3)

h = euclidean_heuristic(pos)
result = astar(G, 'A', 'D', heuristic=h)
print(result['distance'])  # 10
print(result['path'])      # ['A', 'B', 'C', 'D']

# ── Manhattan heuristic (grid graphs) ──────────────────────────
G_grid = nx.grid_2d_graph(5, 5)
for u, v in G_grid.edges():
    G_grid[u][v]['weight'] = 1
pos_grid = {node: (node[1], node[0]) for node in G_grid.nodes()}

h_manhattan = manhattan_heuristic(pos_grid)
result = astar(G_grid, (0, 0), (4, 4), heuristic=h_manhattan)
print(result['distance'])  # 8
print(result['path'])      # [(0,0), (0,1), ..., (4,4)]

# ── No heuristic → equivalent to Dijkstra ──────────────────────
result = astar(G, 'A', 'D')  # heuristic defaults to zero

Haversine — Geographic Graphs

python
from logarithma import astar, haversine_heuristic

# lat/lon coordinates
cities = {
    'Istanbul': (41.008, 28.978),
    'Ankara':   (39.933, 32.859),
    'Izmir':    (38.419, 27.128),
}
G = nx.Graph()
G.add_edge('Istanbul', 'Ankara', weight=450)   # km
G.add_edge('Ankara',   'Izmir',  weight=590)
G.add_edge('Istanbul', 'Izmir',  weight=480)

h = haversine_heuristic(cities)
result = astar(G, 'Istanbul', 'Izmir', heuristic=h)
print(result['distance'])  # 480  (direct is cheaper)
print(result['path'])      # ['Istanbul', 'Izmir']

astar_with_stats — Heuristic Analysis

Compares how many nodes different heuristics expand:

python
from logarithma import astar_with_stats, euclidean_heuristic, zero_heuristic

# Zero heuristic (= Dijkstra)
r_zero = astar_with_stats(G_grid, (0,0), (4,4), heuristic=zero_heuristic)
print(r_zero['nodes_expanded'])   # e.g. 25 — visits all nodes

# Euclidean heuristic
r_euc = astar_with_stats(G_grid, (0,0), (4,4), heuristic=euclidean_heuristic(pos_grid))
print(r_euc['nodes_expanded'])    # e.g. 9 — much fewer nodes visited

Bellman-Ford

The only SSSP algorithm that handles negative-weight edges. Also detects negative-weight cycles reachable from the source.

🎯
When to use: When your graph has negative edge weights (Dijkstra cannot handle these). Key use cases: currency arbitrage detection (negative log-exchange cycles), distributed routing protocols (RIP), and financial cost modeling where costs can decrease along edges.
⚠️
Directed graphs only for negative weights. An undirected graph with any negative-weight edge always contains a negative cycle (the edge itself forms a 2-cycle). Use nx.DiGraph() when working with negative weights.

bellman_ford(graph, source)

python
from logarithma import bellman_ford, NegativeCycleError
import networkx as nx

# ── Graph with negative edge (no cycle) ────────────────────────
G = nx.DiGraph()
G.add_edge('A', 'B', weight=4)
G.add_edge('B', 'C', weight=-3)   # negative but no cycle
G.add_edge('A', 'C', weight=5)
G.add_edge('C', 'D', weight=1)

result = bellman_ford(G, 'A')
print(result['distances'])
# {'A': 0, 'B': 4, 'C': 1, 'D': 2}
# A→B=4, then B→C: 4+(-3)=1, C→D: 1+1=2

# ── Negative cycle detection ────────────────────────────────────
G_cycle = nx.DiGraph()
G_cycle.add_edge('A', 'B', weight=1)
G_cycle.add_edge('B', 'C', weight=-3)
G_cycle.add_edge('C', 'A', weight=1)   # total cycle weight: -1

try:
    bellman_ford(G_cycle, 'A')
except NegativeCycleError as e:
    print("Negative cycle detected!")
    print("Cycle:", e.cycle)   # ['A', 'B', 'C', 'A']

bellman_ford_path(graph, source, target)

python
from logarithma import bellman_ford_path

result = bellman_ford_path(G, 'A', 'D')
print(result['distance'])  # 2
print(result['path'])      # ['A', 'B', 'C', 'D']

# Arbitrage detection example
exchange = nx.DiGraph()
# Negative log-rates as weights: buy low, sell high
exchange.add_edge('USD', 'EUR', weight=-0.05)
exchange.add_edge('EUR', 'GBP', weight=-0.12)
exchange.add_edge('GBP', 'USD', weight=-0.04)
try:
    bellman_ford(exchange, 'USD')
except NegativeCycleError:
    print("Arbitrage opportunity detected!")

Bidirectional Dijkstra

Runs two simultaneous Dijkstra searches — forward from source and backward from target — and terminates when the frontiers meet. Typically ~2× fewer node expansions than standard Dijkstra.

🎯
When to use: When you need a single source-to-target distance on a large graph and don't have coordinate-based heuristics for A*. Ideal for road network routing, social network "degrees of separation", and any large-scale point-to-point query where Dijkstra alone is too slow.

bidirectional_dijkstra(graph, source, target)

python
from logarithma import bidirectional_dijkstra
import networkx as nx

# ── Large road network ─────────────────────────────────────────
G = nx.grid_2d_graph(100, 100)  # 10,000 nodes
for u, v in G.edges():
    G[u][v]['weight'] = 1

result = bidirectional_dijkstra(G, (0, 0), (99, 99))
print(result['distance'])  # 198
print(result['path'][:4])  # [(0,0), (0,1), (0,2), ...]

# ── Directed graph ─────────────────────────────────────────────
DG = nx.DiGraph()
DG.add_edge('S', 'A', weight=1)
DG.add_edge('A', 'B', weight=2)
DG.add_edge('S', 'B', weight=10)   # longer direct path
DG.add_edge('B', 'T', weight=1)

result = bidirectional_dijkstra(DG, 'S', 'T')
print(result['distance'])  # 4  (S→A→B→T)
print(result['path'])      # ['S', 'A', 'B', 'T']

# ── Unreachable target ─────────────────────────────────────────
result = bidirectional_dijkstra(DG, 'T', 'S')  # no reverse path
print(result['distance'])  # inf
print(result['path'])      # []

BFS — Breadth-First Search

Level-order traversal. Finds shortest paths by edge count in unweighted graphs.

🎯
When to use: Unweighted graphs where all edges have equal cost. Use cases: social network degrees of separation, web crawling (pages by link depth), shortest hop-count in network topology, and flood fill algorithms.

bfs(graph, source)

python
from logarithma import bfs, bfs_path
import networkx as nx

G = nx.Graph()
G.add_edges_from([('A','B'), ('B','C'), ('A','D'), ('D','E')])

# Distances (hop count) from A
distances = bfs(G, 'A')
# {'A': 0, 'B': 1, 'D': 1, 'C': 2, 'E': 2}

# Path to a target
result = bfs_path(G, 'A', 'E')
print(result['distances']['E'])  # 2
print(result['paths']['E'])      # ['A', 'D', 'E']

DFS — Depth-First Search

Explores as deep as possible before backtracking. Supports recursive and iterative modes.

🎯
When to use: When you need to explore or classify graph structure rather than find shortest paths. Use cases: cycle detection, maze solving, topological preprocessing, connected component discovery, and backtracking search (puzzles, CSPs).

dfs(graph, source) / detect_cycle(graph)

python
from logarithma import dfs, dfs_path, detect_cycle
import networkx as nx

G = nx.Graph()
G.add_edges_from([('A','B'), ('B','C'), ('C','D'), ('A','D')])

# DFS traversal order
visited = dfs(G, 'A')
# e.g. ['A', 'B', 'C', 'D']

# Iterative mode
visited = dfs(G, 'A', mode='iterative')

# Find any path (not necessarily shortest)
path = dfs_path(G, 'A', 'D')
# ['A', 'D']   (direct edge found first)

# Cycle detection
has_cycle, cycle = detect_cycle(G)
print(has_cycle)  # True  (A-B-C-D-A)
print(cycle)      # ['A', 'B', 'C', 'D', 'A']

# Directed graph
DG = nx.DiGraph()
DG.add_edges_from([('A','B'), ('B','C')])
has_cycle, cycle = detect_cycle(DG)
print(has_cycle)  # False

MST — Minimum Spanning Tree

Both algorithms find a minimum spanning tree of an undirected weighted graph. Passing a directed graph raises UndirectedGraphRequiredError.

🎯
When to use: When you need to connect all nodes at minimum total cost. Use cases: network cable/pipeline layout, clustering (remove heaviest MST edges), circuit design, approximation algorithms for TSP. Kruskal is better for sparse graphs; Prim for dense graphs.

kruskal_mst(graph)

Union-Find based greedy approach. Sorts all edges and adds the cheapest that doesn't form a cycle.

python
import logarithma as lg
import networkx as nx

G = nx.Graph()
G.add_edge('A', 'B', weight=4)
G.add_edge('A', 'C', weight=2)
G.add_edge('B', 'C', weight=1)
G.add_edge('B', 'D', weight=5)
G.add_edge('C', 'D', weight=8)
G.add_edge('C', 'E', weight=10)
G.add_edge('D', 'E', weight=2)

result = lg.kruskal_mst(G)
print(result['total_weight'])   # minimum total weight
print(result['edges'])          # list of (u, v, weight) tuples
print(result['mst_graph'])      # NetworkX Graph of the MST

# Error on directed graphs
from logarithma import UndirectedGraphRequiredError
try:
    lg.kruskal_mst(nx.DiGraph())
except UndirectedGraphRequiredError as e:
    print(e)

prim_mst(graph, start=None)

Min-heap greedy expansion. Grows the MST from a starting node one edge at a time.

python
result = lg.prim_mst(G, start='A')
print(result['total_weight'])   # same as Kruskal on same graph
print(result['edges'])          # list of (u, v, weight) tuples

# start=None → random start node
result = lg.prim_mst(G)

Network Flow

Computes maximum flow through a directed capacity graph using the Edmonds-Karp algorithm (BFS-based Ford-Fulkerson).

🎯
When to use: When you need to find the maximum throughput of a network. Use cases: supply chain / logistics optimization, bipartite matching (job assignment), image segmentation (min-cut), and network capacity planning.

max_flow(graph, source, sink)

python
import logarithma as lg
import networkx as nx

G = nx.DiGraph()
G.add_edge('S', 'A', capacity=10)
G.add_edge('S', 'B', capacity=10)
G.add_edge('A', 'C', capacity=10)
G.add_edge('B', 'C', capacity=10)
G.add_edge('A', 'T', capacity=10)
G.add_edge('C', 'T', capacity=10)

result = lg.max_flow(G, 'S', 'T')
print(result['max_flow_value'])    # 20
print(result['flow_dict'])         # {u: {v: flow_amount, ...}, ...}
print(result['method'])            # 'edmonds_karp'

# Custom capacity attribute name
G2 = nx.DiGraph()
G2.add_edge('X', 'Y', cap=5)
result = lg.max_flow(G2, 'X', 'Y', capacity='cap')

Graph Properties

🎯
When to use: Tarjan SCC — deadlock detection, compiler optimization (live variable analysis), social network community detection. Topological Sort — build systems (make, npm, Gradle), task/job scheduling, course prerequisite planning, dependency resolution.

tarjan_scc(graph)

Finds all strongly connected components of a directed graph. Iterative implementation — no recursion limit issues on large graphs.

python
import logarithma as lg
import networkx as nx

DG = nx.DiGraph()
DG.add_edges_from([('A','B'), ('B','C'), ('C','A'),   # SCC 1
                   ('C','D'), ('D','E'), ('E','D')])   # SCC 2 (D-E)

sccs = lg.tarjan_scc(DG)
print(sccs)   # [['A', 'B', 'C'], ['D', 'E']]

# Condensation DAG — each SCC as a single node
result = lg.tarjan_scc(DG)
for i, scc in enumerate(result):
    print(f"SCC {i}: {scc}")

topological_sort(graph, method='dfs')

Returns a linear ordering of vertices of a DAG such that every directed edge u → v appears before v in the ordering. Raises NotDAGError if the graph has a cycle.

python
from logarithma import topological_sort, NotDAGError
import networkx as nx

DAG = nx.DiGraph()
DAG.add_edges_from([('compile', 'link'), ('link', 'test'),
                    ('compile', 'lint'), ('lint', 'test')])

order = topological_sort(DAG)
print(order)   # e.g. ['compile', 'lint', 'link', 'test']

# Kahn's algorithm variant
order = topological_sort(DAG, method='kahn')

# Raises on cyclic graph
G_cycle = nx.DiGraph([('A','B'), ('B','C'), ('C','A')])
try:
    topological_sort(G_cycle)
except NotDAGError as e:
    print("Cycle found:", e.cycle)

Floyd-Warshall

Dynamic programming algorithm that computes all-pairs shortest paths in O(V³). Supports negative edge weights and detects negative-weight cycles.

🎯
When to use: When you need the distance between every pair of vertices. Best for dense graphs (E ≈ V²). Use cases: graph diameter / eccentricity computation, transitive closure, distance matrix for clustering, and network analysis (average path length). For sparse graphs, prefer Johnson's.

floyd_warshall(graph)

Returns all-pairs distances and a predecessor matrix for path reconstruction.

python
import logarithma as lg
import networkx as nx

# ── Dense graph — all-pairs distances ─────────────────────────
G = nx.DiGraph()
G.add_edge('A', 'B', weight=3)
G.add_edge('A', 'C', weight=8)
G.add_edge('B', 'C', weight=2)
G.add_edge('C', 'D', weight=1)
G.add_edge('B', 'D', weight=7)

result = lg.floyd_warshall(G)

# Distance from A to D: A→B(3)→C(2)→D(1) = 6
print(result['distances']['A']['D'])   # 6.0

# Full distance matrix
for u in G.nodes():
    for v in G.nodes():
        d = result['distances'][u][v]
        print(f"  {u}→{v}: {d}")

# ── Negative weights (no cycle) ───────────────────────────────
G2 = nx.DiGraph()
G2.add_edge('X', 'Y', weight=4)
G2.add_edge('Y', 'Z', weight=-3)   # negative edge, no cycle
G2.add_edge('X', 'Z', weight=5)

result = lg.floyd_warshall(G2)
print(result['distances']['X']['Z'])   # 1  (X→Y→Z: 4+(-3)=1)

# ── Negative cycle detection ──────────────────────────────────
from logarithma import NegativeCycleError

G3 = nx.DiGraph()
G3.add_edge('A', 'B', weight=1)
G3.add_edge('B', 'C', weight=-3)
G3.add_edge('C', 'A', weight=1)   # cycle total: -1

try:
    lg.floyd_warshall(G3)
except NegativeCycleError:
    print("Negative cycle detected!")

floyd_warshall_path(graph, source, target)

Convenience wrapper that returns the shortest path between two vertices.

python
path = lg.floyd_warshall_path(G, 'A', 'D')
print(path)   # ['A', 'B', 'C', 'D']

# Unreachable target → empty list
path = lg.floyd_warshall_path(G, 'D', 'A')
print(path)   # []
⚠️
O(V³) complexity warning: Floyd-Warshall builds a full V×V distance matrix. For sparse graphs (E ≪ V²), Johnson's algorithm is significantly faster at O(V² log V + VE).

Johnson's Algorithm

Computes all-pairs shortest paths efficiently for sparse graphs. Combines Bellman-Ford (for edge reweighting) with V runs of Dijkstra. Handles negative edge weights.

🎯
When to use: All-pairs shortest paths on sparse graphs (E ≪ V²), especially with negative edge weights. At O(V² log V + VE) it outperforms Floyd-Warshall's O(V³) on sparse graphs. Use cases: distance matrices for sparse networks, all-pairs routing, and as a building block for graph centrality computations.

johnson(graph)

Returns all-pairs distances and predecessors, just like Floyd-Warshall.

python
import logarithma as lg
import networkx as nx

# ── Sparse graph with negative edges ──────────────────────────
G = nx.DiGraph()
G.add_edge('A', 'B', weight=4)
G.add_edge('A', 'C', weight=2)
G.add_edge('B', 'C', weight=-3)   # negative edge
G.add_edge('C', 'D', weight=1)
G.add_edge('D', 'B', weight=2)

result = lg.johnson(G)

# A→C: direct=2, via B=4+(-3)=1 → shortest is 1
print(result['distances']['A']['C'])   # 1.0

# All-pairs
for src in G.nodes():
    for tgt in G.nodes():
        d = result['distances'][src][tgt]
        print(f"  {src}→{tgt}: {d}")

# ── Negative cycle detection ──────────────────────────────────
from logarithma import NegativeCycleError

G2 = nx.DiGraph()
G2.add_edge('X', 'Y', weight=-1)
G2.add_edge('Y', 'X', weight=-1)   # 2-node negative cycle

try:
    lg.johnson(G2)
except NegativeCycleError:
    print("Negative cycle detected!")

johnson_path(graph, source, target)

python
path = lg.johnson_path(G, 'A', 'D')
print(path)   # ['A', 'B', 'C', 'D']

Floyd-Warshall vs Johnson — When to pick which?

Floyd-Warshall Johnson's
Time O(V³) O(V² log V + VE)
Best for Dense graphs (E ≈ V²) Sparse graphs (E ≪ V²)
Negative weights Yes (detects cycles) Yes (detects cycles)
Space O(V²) O(V²)
Simpler code Yes (3 nested loops) No (BF + V×Dijkstra)

Breaking the Sorting Barrier SSSP

The first Python implementation of Duan, Mao, Mao, Shu, Yin (2025) — arXiv:2504.17033v2 (STOC 2025 Best Paper). Breaks Dijkstra's classical O(m + n log n) sorting barrier for directed single-source shortest paths.

🎯
When to use: Research and benchmarking on directed sparse graphs with non-negative weights. The algorithm is asymptotically faster than Dijkstra (O(m log²/³ n) vs O(m + n log n)) but has a higher constant factor — the practical crossover requires very large n. Use Dijkstra for production workloads unless graph scale justifies the overhead.

breaking_barrier_sssp(graph, source)

python
import logarithma as lg
import networkx as nx

G = nx.DiGraph()
G.add_edge('s', 'A', weight=2)
G.add_edge('s', 'B', weight=5)
G.add_edge('A', 'C', weight=1)
G.add_edge('B', 'C', weight=2)

distances = lg.breaking_barrier_sssp(G, 's')
print(distances)
# {'s': 0, 'A': 2, 'B': 5, 'C': 3}

# Results match Dijkstra — verify on any graph
dijkstra_dist = lg.dijkstra(G, 's')
assert distances == dijkstra_dist

Cython Acceleration (v0.6.0)

Optional Cython extensions provide 5–7× speedup over pure Python. Falls back automatically when extensions are not compiled.

bash
# Install Cython and compile extensions
pip install "logarithma[fast]"
python setup_ext.py build_ext --inplace

Performance — Breaking Barrier vs Dijkstra

Benchmarks on sparse directed graphs (m ≈ 2n). The ratio shows how many times slower Breaking Barrier is compared to Dijkstra — the gap narrows as n grows, reflecting the asymptotic advantage.

n (nodes) Dijkstra Breaking Barrier (v0.6.0) Ratio v0.5.0 → v0.6.0
500 0.8 ms 61 ms 75× 99× → 75× (~24% faster)
1,000 1.7 ms 47 ms 26× 129× → 26× (~5× faster)
2,000 3.4 ms 74 ms 21× 162× → 21× (~7× faster)
⚠️
Constant factor note: The algorithm's O(m log²/³ n) complexity is asymptotically superior, but the recursive BMSSP framework, BlockHeap data structure, and constant-degree graph transform carry significant overhead. The practical crossover point (where Breaking Barrier becomes faster than Dijkstra) requires very large n. Cython acceleration reduces this gap substantially.

Algorithm Components

Component Paper Reference Description
FindPivots Algorithm 1, §3.1 k-step Bellman-Ford + pivot selection
BaseCase Algorithm 2, §3.1 Mini Dijkstra for k+1 vertices
BMSSP Algorithm 3, §3.1 Recursive bounded multi-source driver
BlockHeap Lemma 3.3 D0/D1 block-linked-list data structure
Constant-degree transform §2, Frederickson 1983 Graph preprocessing for theoretical guarantees
Assumption 2.1 §2 s.4 Lexicographic tiebreaking for deterministic predecessor forest

Utils Module

34 utility functions across 4 categories for graph generation, validation, conversion, and analysis.

Graph Generators

python
from logarithma.utils import (
    generate_random_graph,
    generate_grid_graph,
    generate_scale_free_graph,
    generate_tree_graph,
)

# Erdős-Rényi random graph with weights
G = generate_random_graph(n=100, edge_prob=0.1, weighted=True, seed=42)

# 5×5 grid graph
G_grid = generate_grid_graph(rows=5, cols=5)

# Barabási-Albert scale-free graph (social network model)
G_sf = generate_scale_free_graph(n=200, m=3)

# Random tree
G_tree = generate_tree_graph(n=50)

Validators

python
from logarithma.utils import is_connected, is_dag, has_negative_weights, validate_graph

G = generate_random_graph(50, 0.1)

print(is_connected(G))           # True / False
print(is_dag(nx.DiGraph(G)))     # True if directed acyclic
print(has_negative_weights(G))   # True if any edge weight < 0
print(is_bipartite(G))           # True if graph is bipartite

# Full validation report
report = validate_graph(G)
print(report)

Metrics

python
from logarithma.utils import graph_summary, graph_density, diameter

G = generate_random_graph(100, 0.1, weighted=True)

# Quick summary
summary = graph_summary(G)
print(summary)
# {'nodes': 100, 'edges': 497, 'density': 0.1003,
#  'avg_degree': 9.94, 'is_connected': True, ...}

print(graph_density(G))   # 0.1003
print(diameter(G))        # e.g. 4

Converters

python
from logarithma.utils import (
    to_adjacency_matrix, from_adjacency_matrix,
    to_edge_list, from_edge_list,
)
import numpy as np

# Graph → adjacency matrix
matrix = to_adjacency_matrix(G)   # numpy array

# Adjacency matrix → Graph
G2 = from_adjacency_matrix(matrix)

# Graph → edge list
edges = to_edge_list(G)
# [(0, 1, 2.3), (0, 5, 1.7), ...]

# Edge list → Graph
G3 = from_edge_list(edges)

Complexity Reference

Algorithm Time Space Negative Weights Best For
Dijkstra O(E + V log V) O(V) General SSSP
A* O(b^d) practical O(V) Point-to-point with coordinates
Bellman-Ford O(V · E) O(V) Negative weights, cycle detection
Bidirectional Dijkstra O(E + V log V) O(V) Large point-to-point queries
Floyd-Warshall O(V³) O(V²) All-pairs SP — dense graphs, distance matrices
Johnson's O(V² log V + VE) O(V²) All-pairs SP — sparse graphs with negative weights
BFS O(V + E) O(V) n/a Unweighted shortest path
DFS O(V + E) O(V) n/a Traversal, cycle detection
Kruskal MST O(E log E) O(V) n/a Minimum spanning tree
Prim MST O(E + V log V) O(V) n/a Minimum spanning tree
Max Flow (Edmonds-Karp) O(V · E²) O(V + E) n/a Maximum network flow
Tarjan SCC O(V + E) O(V) n/a Strongly connected components
Topological Sort O(V + E) O(V) n/a DAG linear ordering
Breaking Barrier SSSP 🎯 O(m + n log log n) O(n) Directed SSSP beyond sorting barrier
🎯
Breaking the Sorting Barrier SSSP (Duan et al., 2025 — arXiv:2504.17033v2) is the primary research goal of this library. It breaks the classical O(m + n log n) barrier for directed SSSP. Implemented in v0.5.0, Cython-accelerated in v0.6.0.

Changelog

v0.6.0 — Current
April 2026
  • Cython acceleration for breaking_barrier_sssp
  • block_heap.pyx — cdef classes, C-level double arithmetic
  • breaking_barrier_core.pyx_should_relax as cdef inline nogil, typed memoryviews
  • Pure-Python optimizations: node ID integer mapping, repr() eliminated (~11% runtime)
  • Fallback: pure Python when Cython not compiled
  • 5–7× speedup over v0.5.0 (n=1000: 129× → 26× vs Dijkstra)
  • Floyd-Warshall — O(V³) all-pairs shortest paths, negative weights, cycle detection
  • Johnson's algorithm — O(V² log V + VE) all-pairs SP for sparse graphs
  • "When to use" guidance added for all algorithms
  • 339 unit tests — all passing
v0.5.0
April 2026
  • Breaking the Sorting Barrier SSSP — O(m log²/³ n)
  • First Python implementation of Duan et al. (2025) — arXiv:2504.17033v2
  • BlockHeap (Lemma 3.3), constant-degree transform (Frederickson 1983)
  • Assumption 2.1 lexicographic tiebreaking, W-sweep propagation
  • plot_breaking_barrier_result visualization
  • 99 new unit tests (281 total)
v0.4.0
April 2026
  • Kruskal & Prim Minimum Spanning Tree
  • Max Flow — Edmonds-Karp (BFS-based Ford-Fulkerson)
  • Tarjan SCC — iterative O(V+E) implementation
  • Topological Sort — DFS and Kahn's algorithm
  • Centralized exception hierarchy (7 exception classes + 5 validators)
  • Algorithm-specific visualization: MST steps, flow network, SCC, topo order
v0.3.x
March 2026
  • A* algorithm with Euclidean, Manhattan, Haversine heuristics
  • Bellman-Ford with negative-weight support and cycle detection
  • Bidirectional Dijkstra (~2× speedup for point-to-point)
  • Algorithm-specific visualizations: A*, Bellman-Ford, BiDijkstra, DFS tree
  • Fixed dijkstra_with_path for unreachable nodes
v0.2.0
January 2026
  • BFS and DFS traversal algorithms
  • Utils module: 34 functions (generators, validators, converters, metrics)
  • Visualization module with Matplotlib + Plotly
  • Dijkstra extended with directed graph support
v0.1.0
June 2024
  • Initial release
  • Dijkstra's algorithm with NetworkX integration
  • PyPI package setup

Roadmap

v0.5.0 — Released
April 2026
  • Breaking the Sorting Barrier SSSP
  • O(m log²/³ n) directed SSSP — Duan et al. (2025) ✓
  • BlockHeap, constant-degree transform ✓
  • 281 unit tests ✓
v0.6.0 — Released
April 2026
  • Cython acceleration
  • 5–7× speedup over pure Python ✓
  • Fallback: pure Python when Cython not compiled ✓
v1.0.0
Planned — September 2026
  • Parallelization (multi-threading / NumPy vectorization)
  • Domain modules: logistics, finance, social networks
  • Production-ready stable release