This project implements an efficient algorithm for finding the shortest cycle in an undirected graph with non-negative edge weights. The algorithm leverages advanced optimization techniques including Lowest Common Ancestor (LCA) trees with binary lifting, priority queues, aggressive pruning, and smart caching.
To use this algorithm, you need Python 3.6 or higher installed on your system. The project relies on the following dependencies:
networkx
: For graph creation, manipulation, and built-in algorithms.numpy
: For efficient numerical computations.matplotlib
: For visualizing benchmark results (optional).cProfile
andpstats
: For performance profiling (included in standard library).
Install these dependencies using pip:
pip install networkx numpy matplotlib
For optimal performance with the Fibonacci heap, you can install the optional external implementation:
pip install fibonacci-heap-mod
This package is optional and the algorithm will work fine without it. If this package is not installed, the algorithm will automatically fall back to an internal implementation.
Clone this repository to your local machine:
git clone https://github.com/yourusername/shortest-cycle-algorithm.git
cd shortest-cycle-algorithm
The primary function, sota_shortest_cycle
, accepts a NetworkX
graph as input and returns the length of the shortest cycle. Below is a simple example to get you started.
import networkx as nx
from shortest_cycle import sota_shortest_cycle
# Create a sample undirected graph
G = nx.Graph()
G.add_edge(0, 1, weight=1)
G.add_edge(1, 2, weight=2)
G.add_edge(2, 0, weight=3)
# Compute the shortest cycle length
cycle_length = sota_shortest_cycle(G)
print(f"The shortest cycle length is: {cycle_length}")
Output:
The shortest cycle length is: 6.0
This example creates a triangle graph with edges (0, 1)
, (1, 2)
, and (2, 0)
and finds the shortest cycle, which is the perimeter of the triangle (1 + 2 + 3 = 6).
The project also provides additional utility functions:
shortest_cycle_nodes(G)
: Returns the list of nodes that form the shortest cycle.visualize_shortest_cycle(G, cycle)
: Visualizes the graph and highlights a given cycle.create_grid_graph(size, default_weight, cycle_weight)
: Creates a grid graph with a hidden low-weight cycle for testing.create_spatial_graph(n_nodes, radius, default_weight, cycle_weight)
: Creates a random geometric graph with a hidden low-weight cycle.
Example with visualization:
import networkx as nx
from shortest_cycle import sota_shortest_cycle, shortest_cycle_nodes, visualize_shortest_cycle
# Create a sample graph
G = nx.Graph()
G.add_edge(0, 1, weight=1)
G.add_edge(1, 2, weight=2)
G.add_edge(2, 3, weight=1)
G.add_edge(3, 0, weight=1)
G.add_edge(0, 2, weight=2.5)
# Get the shortest cycle length
cycle_length = sota_shortest_cycle(G)
print(f"The shortest cycle length is: {cycle_length}")
# Get the nodes in the shortest cycle
cycle_nodes = shortest_cycle_nodes(G)
print(f"The shortest cycle consists of nodes: {cycle_nodes}")
# Visualize the graph with the shortest cycle highlighted
visualize_shortest_cycle(G, cycle_nodes)
The algorithm supports several optimization options that can be enabled or disabled:
# Default configuration (using standard heap with LCA optimization)
cycle_length = sota_shortest_cycle(G)
# Enable Fibonacci heap (if available)
cycle_length = sota_shortest_cycle(G, use_fib_heap=True, use_lca=True)
# Use LCA optimization with standard binary heap
cycle_length = sota_shortest_cycle(G, use_fib_heap=False, use_lca=True)
# Use traditional approach without LCA optimization
cycle_length = sota_shortest_cycle(G, use_lca=False)
The use_fib_heap
parameter enables the Fibonacci heap implementation for priority queue operations, which can offer better theoretical performance for large graphs. There are two implementations available:
- Internal Implementation: Used by default or if the external package is not installed.
- External Implementation: Used if the
fibonacci-heap-mod
package is installed anduse_fib_heap=True
.
To use the external implementation, first install the package:
pip install fibonacci-heap-mod
Then set use_fib_heap=True
when calling the function. The algorithm will automatically detect if the package is available and use it; otherwise, it will fall back to the internal implementation.
The project includes tools for profiling and analyzing the algorithm's performance. This is particularly useful for understanding bottlenecks, comparing optimization strategies, and analyzing complexity scaling with graph size.
To profile the algorithm's performance:
import cProfile
import pstats
from shortest_cycle import sota_shortest_cycle, create_grid_graph
# Create a test graph
G = create_grid_graph(20, default_weight=10, cycle_weight=1)
# Profile the algorithm with Fibonacci heap optimization (if available)
profiler = cProfile.Profile()
profiler.enable()
result = sota_shortest_cycle(G, use_fib_heap=True, use_lca=True)
profiler.disable()
# Print the profiling results
profiler.dump_stats('profile_data.prof')
p = pstats.Stats('profile_data.prof')
p.strip_dirs().sort_stats('cumulative').print_stats(10)
A dedicated profiling script is provided for more detailed analysis:
# Basic profiling on a grid graph of size 20x20
python profile_algorithm.py --mode profile --graph grid --size 20
# Compare different optimization strategies
python profile_algorithm.py --mode compare --graph grid --size 15
# Analyze scaling with graph size
python profile_algorithm.py --mode scale --graph spatial --use-fib-heap
The profiling script supports the following options:
-
Profiling modes:
profile
: Single profiling run with detailed statscompare
: Compare different optimization combinationsscale
: Analyze how performance scales with graph size
-
Graph types:
grid
: Regular grid graphsspatial
: Random geometric graphs
-
Optimization flags:
--use-fib-heap
: Use Fibonacci heap implementation--use-lca
: Use LCA optimization (default: True)
The profiling script generates visualizations to help understand performance:
- Optimization comparison: Bar chart comparing execution times for different optimization combinations
- Scaling analysis: Line plots showing how execution time and operation count scale with graph size
For more advanced analysis, the profiling script saves raw profiling data in .prof
files that can be analyzed with external tools:
# Use snakeviz for interactive visualization
pip install snakeviz
snakeviz profiling_results/grid_20x20_lca_fib.prof
The algorithm uses several advanced techniques to efficiently find the shortest cycle:
Instead of the traditional approach of removing each edge and running Dijkstra's algorithm, this implementation:
- Runs Dijkstra's algorithm from each node, generating a shortest-path tree.
- Constructs an LCA data structure with binary lifting, allowing O(log n) queries to find the lowest common ancestor of any two nodes.
- For each edge (u, v), uses the LCA to determine the cycle formed with the shortest-path tree in O(log n) time.
- Dijkstra Early Termination: Stops Dijkstra's algorithm once all relevant nodes are processed, using the current shortest cycle length (gamma) as a bound.
- Node Pruning: After processing a node, prunes nodes that cannot possibly contribute to a shorter cycle, reducing work in subsequent iterations.
- Binary Heap: Implements Dijkstra's algorithm with a binary heap for efficient priority queue operations.
- Fibonacci Heap Option: Optional Fibonacci heap implementation for potentially improved performance.
- LCA Caching: Caches LCA query results to avoid redundant computations.
- Ancestor Table: Uses dynamic programming to precompute ancestors for fast LCA queries.
- Time Complexity: O(V·(E + V log V)) in the worst case, but with early termination and pruning, the practical performance is often much better.
- Space Complexity: O(V log V) for the LCA data structure and O(V) for Dijkstra's algorithm.
The algorithm excels on sparse graphs, where aggressive pruning significantly reduces the number of nodes processed in each iteration.
A comprehensive test suite is included to verify the algorithm's correctness across various scenarios. To run the tests, install pytest
and execute:
pip install pytest
pytest test_shortest_cycle.py
The tests cover:
- Standard graph structures (triangles, squares, complete graphs)
- Graphs with hidden low-weight cycles
- Edge cases (empty graphs, disconnected graphs)
- Comparison against traditional approaches for correctness
shortest-cycle-algorithm/
├── shortest_cycle.py # Main algorithm implementation
├── test_shortest_cycle.py # Test suite for verification
├── benchmark.py # Performance benchmarking script
├── profile_algorithm.py # Detailed profiling tool
├── requirements.txt # Project dependencies
└── README.md # Project documentation
- Non-Negative Weights: The algorithm assumes edge weights are non-negative, as Dijkstra's algorithm does not support negative weights.
- Undirected Graphs: Designed specifically for undirected graphs; directed graphs require a different approach.
- Memory Usage: The LCA data structure has slightly higher memory requirements than the traditional approach.
We welcome contributions! To contribute:
- Fork the repository.
- Create a feature or bugfix branch:
git checkout -b feature/your-feature-name
- Commit your changes with descriptive messages:
git commit -m "Add feature X to improve Y"
- Push your branch and open a pull request:
git push origin feature/your-feature-name
Ensure your code passes all tests and follows the project's style guidelines.
This project is released under the MIT License. See the LICENSE file for details.
If you use this algorithm in your work, please cite the original manuscript (update with actual details when available):
[Author Name]. (Year). Finding a minimum weight cycle in undirected graphs using LCA and pruning techniques. [Journal/Conference Name], [Volume(Issue)], [Pages].