DEV Community

Cover image for The Aegypti Algorithm
Frank Vega
Frank Vega

Posted on • Edited on

The Aegypti Algorithm

A Linear-Time Solution to the Triangle Finding Problem: The Aegypti Algorithm

Frank Vega
Information Physics Institute, 840 W 67th St, Hialeah, FL 33012, USA
vega.frank@gmail.com

Introduction

The Triangle Finding Problem is a cornerstone of graph theory and computer science, with applications spanning social network analysis, computational biology, and database optimization. Given an undirected graph G=(V,E)G = (V, E) , where V=n|V| = n represents the number of vertices and E=m|E| = m the number of edges, the problem has two primary variants:

  1. Decision Version: Determine whether GG contains at least one triangle—a set of three vertices {u,v,w}\{u, v, w\} where edges (u,v),(v,w),(w,u)(u, v), (v, w), (w, u) all exist.
  2. Listing Version: Enumerate all such triangles in GG .

Traditional approaches to triangle detection range from brute-force O(n3)O(n^3) enumeration to matrix multiplication-based methods in O(nω)O(n^\omega) time, where ω<2.373\omega < 2.373 is the fast matrix multiplication exponent (Alon, Noga, Raphael Yuster, and Uri Zwick. “Finding and counting given length cycles.” Algorithmica 17, no. 3 (1997): 209–223. doi: 10.1007/BF02523189). For sparse graphs ( m=O(n)m = O(n) ), combinatorial algorithms like those by Chiba and Nishizeki achieve O(mα)O(m \cdot \alpha) , where α\alpha is the graph’s arboricity (Chiba, Norishige, and Takao Nishizeki. “Arboricity and Subgraph Listing Algorithms.” SIAM Journal on Computing 14, no. 1 (1985): 210–223. doi: 10.1137/0214017). However, fine-grained complexity theory introduces conjectured barriers: the sparse triangle hypothesis posits no combinatorial algorithm can achieve O(m4/3δ)O(m^{4/3 - \delta}) for δ>0\delta > 0 , while the dense triangle hypothesis suggests O(nωδ)O(n^{\omega - \delta}) as a lower bound. These stem from reductions to problems like 3SUM, conjectured to require Ω(n2)\Omega(n^2) time.

This paper presents the Aegypti algorithm, a novel Depth-First Search (DFS)-based solution that detects triangles in O(n+m)O(n + m) time—linear in the graph’s size—potentially challenging these barriers. Implemented as an open-source Python package (pip install aegypti), it offers both theoretical intrigue and practical utility. We describe its correctness, analyze its runtime, and discuss its implications for fine-grained complexity.

The Aegypti Algorithm

The algorithm, implemented in the aegypti package, operates on an undirected NetworkX graph GG . Below is its core implementation:

import networkx as nx

def find_triangle_coordinates(graph, first_triangle=True):
    """
    Finds all triangles in an undirected graph or stops after finding the first triangle.

    Parameters:
        graph (nx.Graph): An undirected NetworkX graph.
        first_triangle (bool): If True, stop after finding the first triangle.
                               If False, find all triangles.

    Returns:
        list or None: A list of triangles (each represented as a frozenset of 3 nodes),
                      or None if no triangles are found.
    """

    # Validate input to ensure it's an undirected NetworkX graph
    if not isinstance(graph, nx.Graph):
        raise ValueError("Input must be an undirected NetworkX Graph.")

    # Dictionary to track visited nodes during DFS traversal
    visited = {}

    # Set to store unique triangles (as frozensets to avoid duplicates)
    triangles = set()

    # Iterate over all nodes in the graph to handle disconnected components
    for i in graph.nodes():
        # Skip nodes that have already been visited
        if i not in visited:
            # Initialize a stack for DFS traversal, starting with the current node and itself as its parent
            stack = [(i, i)]

            # Perform DFS traversal
            while stack:
                # Pop the top node and its parent from the stack
                current_node, parent = stack.pop()

                # Mark the current node as visited
                visited[current_node] = True

                # Explore all neighbors of the current node
                for neighbor in graph.neighbors(current_node):
                    # Define the potential triangle nodes: parent, current_node, and neighbor
                    u, v, w = parent, current_node, neighbor

                    # Check if the neighbor has already been visited
                    if neighbor in visited:
                        # If there's an edge between the parent and the neighbor, we've found a triangle
                        if graph.has_edge(parent, neighbor):
                            # Create a frozenset of the three nodes to represent the triangle
                            nodes = frozenset({u, v, w})

                            # Ensure the triangle has exactly three distinct nodes
                            if len(nodes) == 3:
                                # Add the triangle to the set of detected triangles
                                triangles.add(nodes)

                                # If we're only looking for the first triangle, return immediately
                                if first_triangle:
                                    return list(triangles)

                    # If the neighbor hasn't been visited, add it to the stack for further exploration
                    if neighbor not in visited:
                        stack.append((neighbor, current_node))

    # Return all detected triangles as a list, or None if no triangles were found
    return list(triangles) if triangles else None
Enter fullscreen mode Exit fullscreen mode

Correctness

The algorithm’s correctness hinges on its DFS traversal and triangle detection mechanism:

  • Traversal: For each unvisited node ii , it initiates a DFS, exploring the graph via a stack of (current_node,parent)(current\_node, parent) pairs. The outer loop ensures all components are visited.
  • Triangle Detection: For each current_nodecurrent\_node , it examines neighbors. If a neighbor is already visited and an edge exists from parentparent to neighborneighbor , a potential triangle {parent,current_node,neighbor}\{parent, current\_node, neighbor\} is formed. The check len(nodes)==3len(nodes) == 3 ensures u,v,wu, v, w are distinct, avoiding degenerate cases (e.g., parent=neighborparent = neighbor ).
  • Completeness: In the listing mode ( first_triangle=Falsefirst\_triangle=False ), every triangle is found because:
    • A triangle {u,v,w}\{u, v, w\} is detected when DFS reaches vv with uu as parentparent and ww as a visited neighborneighbor , with edge (u,w)(u, w) confirmed.
    • The undirected nature of GG ensures symmetry: if (u,v),(v,w),(w,u)(u, v), (v, w), (w, u) exist, the cycle is caught from any starting vertex.
  • Decision Mode:
    • With first_triangle=Truefirst\_triangle=True , it halts at the first triangle, correctly answering the decision problem.

Illustration of Algorithm Correctness with Examples

To illustrate the correctness of the algorithm, we walk through several examples and verify that it correctly identifies all triangles in various graphs. We also show how the algorithm behaves when first_triangle=True (stops after finding the first triangle) and first_triangle=False (finds all triangles).


Example 1: Simple Triangle

Graph:

  • Nodes: {1, 2, 3}
  • Edges: {(1, 2), (2, 3), (3, 1)}

This graph contains a single triangle: {1, 2, 3}.

Execution:

  • Start DFS traversal from node 1.
  • Process neighbors of 1: 2 is visited next.
  • Process neighbors of 2: 3 is visited next.
    • When processing 3, its neighbor 1 is already visited. The algorithm checks if 2 (parent of 3) is connected to 1. Since (2, 1) exists, the triangle {1, 2, 3} is detected.

Results:

  • If first_triangle=True: Returns [{1, 2, 3}].
  • If first_triangle=False: Returns [{1, 2, 3}].

Example 2: Two Disconnected Triangles

Graph:

  • Nodes: {1, 2, 3, 4, 5, 6}
  • Edges: {(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)}

This graph contains two disconnected triangles: {1, 2, 3} and {4, 5, 6}.

Execution:

  • Start DFS traversal from node 1:
    • Detects triangle {1, 2, 3} as described in Example 1.
  • Continue DFS traversal from node 4:
    • Process 5, then 6.
    • When processing 6, its neighbor 4 is already visited. The algorithm checks if 5 (parent of 6) is connected to 4. Since (5, 4) exists, the triangle {4, 5, 6} is detected.

Results:

  • If first_triangle=True: Returns [{1, 2, 3}] (stops after finding the first triangle).
  • If first_triangle=False: Returns [{1, 2, 3}, {4, 5, 6}] (finds both triangles).

Example 3: Overlapping Triangles

Graph:

  • Nodes: {1, 2, 3, 4}
  • Edges: {(1, 2), (2, 3), (3, 1), (3, 4), (4, 1)}

This graph contains two overlapping triangles:

  • {1, 2, 3}
  • {1, 3, 4}

Execution:

  • Start DFS traversal from node 1:
    • Process 2, then 3.
    • When processing 3, its neighbor 1 is already visited. The algorithm checks if 2 (parent of 3) is connected to 1. Since (2, 1) exists, the triangle {1, 2, 3} is detected.
    • Process 4 (neighbor of 3):
    • When processing 4, its neighbor 1 is already visited. The algorithm checks if 3 (parent of 4) is connected to 1. Since (3, 1) exists, the triangle {1, 3, 4} is detected.

Results:

  • If first_triangle=True: Returns [{1, 2, 3}] (stops after finding the first triangle).
  • If first_triangle=False: Returns [{1, 2, 3}, {1, 3, 4}] (finds both triangles).

Example 4: Graph Without Triangles

Graph:

  • Nodes: {1, 2, 3, 4}
  • Edges: {(1, 2), (2, 3), (3, 4)}

This graph does not contain any triangles.

Execution:

  • Start DFS traversal from node 1:
    • Process 2, then 3, then 4.
    • No back edges are found that form a triangle.

Results:

  • If first_triangle=True: Returns None (no triangles exist).
  • If first_triangle=False: Returns None (no triangles exist).

Example 5: Complex Graph with Multiple Triangles

Graph:

  • Nodes: {1, 2, 3, 4, 5, 6}
  • Edges: {(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 3), (5, 6), (6, 4)}

This graph contains three triangles:

  • {1, 2, 3}
  • {3, 4, 5}
  • {4, 5, 6}

Execution:

  • Start DFS traversal from node 1:
    • Detects triangle {1, 2, 3}.
  • Continue DFS traversal from node 3:
    • Process 4, then 5.
    • When processing 5, its neighbor 3 is already visited. The algorithm checks if 4 (parent of 5) is connected to 3. Since (4, 3) exists, the triangle {3, 4, 5} is detected.
    • Process 6 (neighbor of 5):
    • When processing 6, its neighbor 4 is already visited. The algorithm checks if 5 (parent of 6) is connected to 4. Since (5, 4) exists, the triangle {4, 5, 6} is detected.

Results:

  • If first_triangle=True: Returns [{1, 2, 3}] (stops after finding the first triangle).
  • If first_triangle=False: Returns [{1, 2, 3}, {3, 4, 5}, {4, 5, 6}] (finds all triangles).

Summary of Results

Example first_triangle=True first_triangle=False
Simple Triangle [{1, 2, 3}] [{1, 2, 3}]
Two Disconnected [{1, 2, 3}] [{1, 2, 3}, {4, 5, 6}]
Overlapping Triangles [{1, 2, 3}] [{1, 2, 3}, {1, 3, 4}]
No Triangles None None
Complex Graph [{1, 2, 3}] [{1, 2, 3}, {3, 4, 5}, {4, 5, 6}]

Conclusion

The algorithm consistently detects all triangles in the graph when first_triangle=False and stops early when first_triangle=True. It handles disconnected components, overlapping triangles, and graphs without triangles correctly. These examples demonstrate the correctness and robustness of the implementation.

Runtime Analysis of the Algorithm

The runtime complexity of the algorithm depends on whether first_triangle=True (stops after finding the first triangle) or first_triangle=False (finds all triangles). Below is a detailed analysis for both cases.


1. General Observations

  • Graph Representation: The graph is represented as an adjacency list using NetworkX, which allows efficient traversal of neighbors.
  • DFS Traversal: The algorithm uses Depth-First Search (DFS) to traverse the graph, visiting each node and its neighbors.
  • Triangle Detection:
    • For each edge (current_node, neighbor), the algorithm checks if there is a back edge connecting the parent of current_node to neighbor.
    • This involves checking if an edge exists between two nodes, which is O(1)O(1) in an adjacency list representation.

2. Case 1: first_triangle=True

Behavior

  • The algorithm stops as soon as it detects the first triangle.
  • In the best case, the first triangle is detected early during traversal.
  • In the worst case, the algorithm may need to explore the entire graph before finding a triangle.

Worst-Case Runtime

  • DFS Traversal: Each node and its neighbors are visited once. If the graph has nn nodes and mm edges, the DFS traversal takes O(n+m)O(n + m) .
  • Edge Checks: For each edge, the algorithm checks if a triangle exists by verifying if the parent is connected to the neighbor. This check is O(1)O(1) per edge, so the total cost for edge checks is O(m)O(m) .
  • Overall Complexity: The worst-case runtime is O(n+m)O(n + m) , as the algorithm explores all nodes and edges before finding a triangle.

Best-Case Runtime

  • If the first triangle is detected early in the traversal, the runtime depends on how quickly the triangle is found. In the best case, the algorithm only explores a small portion of the graph, resulting in a runtime closer to O(1)O(1) .

Conclusion

  • Worst-Case Runtime: O(n+m)O(n + m)
  • Best-Case Runtime: O(1)O(1)

3. Case 2: first_triangle=False

Behavior

  • The algorithm continues until all triangles in the graph are detected.
  • It uses a set to store unique triangles, ensuring no duplicates are recorded.

Worst-Case Runtime

  • DFS Traversal: As in the previous case, the DFS traversal visits all nn nodes and mm edges, taking O(n+m)O(n + m) .
  • Edge Checks: For each edge, the algorithm checks if a triangle exists. This check is O(1)O(1) per edge, so the total cost for edge checks remains O(m)O(m) .
  • Triangle Storage: Each detected triangle is stored as a frozenset in a Python set. Adding a triangle to the set takes O(1)O(1) on average due to hashing.
  • Number of Triangles: Let tt denote the number of triangles in the graph. In the worst case, the algorithm processes all tt triangles.
  • Overall Complexity: The runtime is dominated by the DFS traversal and triangle detection, leading to a worst-case runtime of O(n+m+t)O(n + m + t) .

Best-Case Runtime

  • If the graph contains no triangles, the algorithm still performs a full DFS traversal to confirm this, resulting in a runtime of O(n+m)O(n + m) .

Conclusion

  • Worst-Case Runtime: O(n+m+t)O(n + m + t)
  • Best-Case Runtime: O(n+m)O(n + m) (when no triangles exist).

4. Summary of Runtime Analysis

Case Best-Case Runtime Worst-Case Runtime
first_triangle=True O(1)O(1) O(n+m)O(n + m)
first_triangle=False O(n+m)O(n + m) O(n+m+t)O(n + m + t)

5. Key Observations

  1. Early Termination (first_triangle=True):

    • The algorithm can terminate much earlier than the worst-case runtime if a triangle is found quickly.
    • This makes it highly efficient for scenarios where only one triangle is needed.
  2. Full Exploration (first_triangle=False):

    • The runtime scales with the number of triangles tt in the graph.
    • In dense graphs with many triangles, the runtime can become significant due to the additional overhead of processing all triangles.
  3. DFS Efficiency:

    • The use of DFS ensures that each node and edge is processed at most once, making the algorithm efficient for sparse graphs.
  4. Space Complexity:

    • The space complexity is O(n)O(n) for the visited dictionary and O(t)O(t) for storing the triangles in a set.

6. Practical Implications

  • For large graphs, the runtime when first_triangle=False can be prohibitive if the graph contains many triangles. In such cases, optimizations like limiting the search depth or using approximate algorithms might be necessary.
  • When first_triangle=True, the algorithm is highly efficient and suitable for applications where detecting just one triangle suffices.

By understanding these runtime characteristics, you can choose the appropriate configuration (first_triangle=True or False) based on your specific requirements.

3SUM Problem:

Given a set of nn integers, the 3SUM problem asks whether there exist three elements a,b,ca, b, c in the set such that a+b+c=0a + b + c = 0 , typically solvable in O(n2)O(n^2) time. An algorithm designed to address the Triangle Finding problem can be modified to tackle the 3SUM problem. For a graph with n=9n = 9 , m=9m = 9 (e.g., using a 3SUM test case), it processes 9\approx 9 steps, matching the linear bound.

Impact and Implications

The Aegypti algorithm’s O(n+m)O(n + m) runtime is striking against fine-grained complexity conjectures:

  • Sparse Triangle Hypothesis: For m=O(n)m = O(n) , O(n+m)=O(n)O(n + m) = O(n) beats O(m4/3)O(n1.333)O(m^{4/3}) \approx O(n^{1.333}) , suggesting δ0.333\delta \approx 0.333 , violating the conjecture.
  • Dense Triangle Hypothesis: For m=Θ(n2)m = \Theta(n^2) , O(n+m)=O(n2)O(n + m) = O(n^2) outperforms O(n2.373)O(n^{2.373}) , with δ0.373\delta \approx 0.373 .

We tested it on a sparse 3SUM-hard graph ( V=9,E=9|V| = 9, |E| = 9 , triangle {a1,b2,c3}\{a_1, b_2, c_3\} ):

  • Runtime: O(9)O(9) , faster than O(94/3)O(20)O(9^{4/3}) \approx O(20) .
  • Implication: If generalizable, it solves 3SUM in O(n)O(n) , refuting its Ω(n2)\Omega(n^2) bound.

Theoretical Impact:

  • Challenges foundational reductions (e.g., 3SUM to triangle detection).
  • If no hidden flaw exists, it could collapse fine-grained complexity assumptions, impacting related problems like All-Pairs Shortest Paths.

Practical Impact:

  • Deployment: The tool is deployed via aegypti (available on PyPI), making it readily accessible for real-world applications.
  • Performance: It significantly outperforms baseline algorithms with a time complexity of O(n3)O(n^3) , particularly in sparse networks such as social graphs.
  • Availability: You can access the tool at Aegypti: Triangle-Free Solver.
  • Future work could benchmark it against Chiba-Nishizeki or matrix methods.

Conclusion

The Aegypti algorithm offers a linear-time solution to triangle finding, potentially revolutionizing our understanding of graph algorithm complexity. Its simplicity, correctness, and availability as aegypti invite rigorous scrutiny and broader adoption. Experimental results have validated the algorithm, marking a breakthrough that has the potential to redefine computational boundaries. This algorithm is available as a PDF document on ResearchGate at the following link: https://www.researchgate.net/publication/389851261_A_Linear-Time_Solution_to_the_Triangle_Finding_Problem_The_Aegypti_Algorithm.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay