Open In App

Find the Number of Cliques in a Graph

Improve
Improve
Like Article
Like
Save
Share
Report

In graph theory, a clique is a subset of vertices of an undirected graph such that every two distinct vertices in the clique are adjacent, that is, they are connected by an edge of the graph. The number of cliques in a graph is the total number of cliques that can be found in the graph.

The Mathematics behind cliques in a graph involves the concept of adjacency matrices and graph theory. An adjacency matrix is a matrix representation of a graph where each row and column corresponds to a vertex in the graph and the elements of the matrix indicate whether there is an edge between the vertices. 

For example, in an undirected graph with 5 vertices, the adjacency matrix would be a 5×5 matrix where a 1 in position (i, j) indicates that there is an edge between vertex i and vertex j, and a 0 indicates that there is no edge.

1 1 1 0 0
1 1 1 0 0
1 1 1 1 1
0 0 1 1 1
0 0 1 1 1

To find the number of cliques in a graph using an adjacency matrix, you can use a graph algorithm such as the Bron–Kerbosch algorithm, which is an efficient method for enumerating all cliques in an undirected graph.

The Bron–Kerbosch algorithm works by iterating over all possible subsets of the vertices and checking if they form a clique. It does this by using the adjacency matrix to check if every pair of vertices in the subset is adjacent (i.e., connected by an edge). If they are, then the subset forms a clique and is added to the list of cliques.

Example 1:

Consider the following graph:

Example 1

This graph has three cliques: {1, 2, 3}, {3, 4, 5}, and {1, 2, 4, 5}.

To find the number of cliques in a graph, graph traversal algorithms such as depth-first search (DFS) or breadth-first search (BFS) can be used to visit all the vertices and check for cliques at each vertex.

For example, you could start at vertex 1 and perform a DFS to explore the graph. When you reach vertex 3, you know that the vertices 1, 2, and 3 form a clique. You can then continue the DFS to explore the remaining vertices and check for cliques at each vertex.

Alternatively, you could use a graph algorithm specifically designed to find cliques, such as the Bron–Kerbosch algorithm. This algorithm is an efficient method for enumerating all cliques in an undirected graph.

Example 2:

Consider a simple undirected graph with 4 vertices and 6 edges, as shown below:

Example 2

To count the number of cliques in this graph, we can use the following formula:

Number of cliques = n * (n – 1) / 2 – m + 1 where n is the number of vertices in the graph and m is the number of edges. Plugging in the values for this graph, we get:
Number of cliques = 4 * (4 – 1) / 2 – 6 + 1 = 2

So there are 2 cliques in this graph.

This formula works because it counts the number of possible pairs of vertices in the graph (n * (n – 1) / 2), and then subtracts the number of edges to account for overcounting. Finally, it adds 1 to account for the fact that a single vertex on its own is also considered a clique.

This formula only works for undirected graphs, and it may not give the correct result for graphs with multiple edges or self-loops. It is also not practical for large graphs, as the time complexity of this approach is O(n^2). However, it can be a useful tool for quickly counting the number of cliques in small graphs

Approaches:

  • Brute-force search: One approach is to simply enumerate all possible cliques in the graph and count them. This approach has a time complexity of O(3^n), where n is the number of vertices in the graph, so it is only practical for very small graphs.
  • Bron-Kerbosch algorithm: The Bron-Kerbosch algorithm is a pivot-based algorithm that uses a recursive approach to find all cliques in a graph. It has a time complexity of O(3^(n/3)), where n is the number of vertices in the graph, and a space complexity of O(n).
  • Tomita algorithm: The Tomita algorithm is another pivot-based algorithm that uses a recursive approach to find all cliques in a graph. It has a time complexity of O(4^(n/4)), where n is the number of vertices in the graph and a space complexity of O(n).
  • Pivot Bron-Kerbosch algorithm: The pivot Bron-Kerbosch algorithm is an improvement over the Bron-Kerbosch algorithm that uses a pivot element to prune the search space and reduce the time complexity. It has a time complexity of O(2^n), where n is the number of vertices in the graph, and a space complexity of O(n^2).
  • Hybrid algorithm: The hybrid algorithm is a combination of the Bron-Kerbosch and Tomita algorithms that uses both pivoting and recursion to find all cliques in a graph. It has a time complexity of O(2^n), where n is the number of vertices in the graph and a space complexity of O(n^2)
  • Approximation algorithms: Another approach is to use approximation algorithms, which are designed to find a good approximation of the number of cliques in a graph in polynomial time. These algorithms may not find all cliques in the graph, but they can be useful in cases where the exact number of cliques is not necessary.
  • Parallelization techniques: It is also possible to improve the performance of the above algorithms by using parallelization techniques, such as multi-threading or distributed computing, to divide the work across multiple processors. However, these approaches may have additional overhead costs and may not be suitable for all types of graphs.

Which approach is best for your specific problem will depend on the size of the graph and the desired time and space complexity. You should choose the approach that best meets your needs and constraints.

Observation behind the Approaches:

The different approaches to finding the number of cliques in a graph are based on different observations and techniques. Here are some observations behind these approaches:

  • Brute-force search: This approach simply enumerates all possible cliques in the graph and counts them. It is based on the observation that a clique is a subset of the vertices of the graph that are all connected to each other.
  • Bron-Kerbosch algorithm: The Bron-Kerbosch algorithm is based on the observation that a clique can be found by starting with a vertex and then expanding to include its neighbors, as long as they are all connected to each other. The algorithm uses a pivot element to prune the search space and reduce the time complexity.
  • Tomita algorithm: The Tomita algorithm is based on the observation that a clique can be found by starting with a vertex and then expanding to include its neighbors, as long as they are all connected to each other. The algorithm uses a pivot element to prune the search space and reduce the time complexity.
  • Pivot Bron-Kerbosch algorithm: The pivot Bron-Kerbosch algorithm is an improvement over the Bron-Kerbosch algorithm that uses a pivot element to prune the search space and reduce the time complexity. It is based on the observation that many cliques in a graph share common vertices, and by using a pivot element to focus the search on these vertices, it is possible to reduce the time complexity of the algorithm.
  • Hybrid algorithm: The hybrid algorithm combines the Bron-Kerbosch and Tomita algorithms and uses both pivoting and recursion to find all cliques in a graph. It is based on the observation that both of these algorithms are effective at finding cliques, and by combining them, it is possible to find all cliques in a graph more efficiently.
  • Approximation algorithms: Approximation algorithms are based on the observation that it is often sufficient to find a good approximation of the number of cliques in a graph, rather than the exact number. These algorithms use techniques such as random sampling or graph partitioning to find a good approximation in polynomial time.
  • Parallelization techniques: Parallelization techniques are based on the observation that it is often possible to improve the performance of an algorithm by dividing the work across multiple processors. These techniques can be used to speed up the execution of the above algorithms by dividing the search space among multiple threads or processes

Here is a simple algorithm in Python to find the number of cliques in an undirected graph:

Python3




# Python code
from collections import defaultdict
 
 
def find_cliques(graph):
    cliques = []
    visited = set()
 
    def dfs(node, clique):
        visited.add(node)
        clique.add(node)
 
        for neighbor in graph[node]:
            if neighbor not in visited:
                dfs(neighbor, clique)
 
    for node in graph:
        if node not in visited:
            clique = set()
            dfs(node, clique)
            if len(clique) > 1:
                cliques.append(clique)
 
    return cliques
 
 
# Example usage
graph = {
    'A': ['B', 'C', 'D'],
    'B': ['A', 'C', 'D'],
    'C': ['A', 'B', 'D'],
    'D': ['A', 'B', 'C'],
    'E': ['F'],
    'F': ['E']
}
 
cliques = find_cliques(graph)
print(f'Number of cliques: {len(cliques)}')
print(f'Cliques: {cliques}')


Java




import java.util.*;
 
public class CliqueFinder {
    // Finds all cliques in a graph represented as a map
    // where the keys are nodes and the values are lists of neighbors
    static List<Set<String>> findCliques(Map<String, List<String>> graph) {
        // List to store the cliques
        List<Set<String>> cliques = new ArrayList<>();
        // Set to keep track of visited nodes
        Set<String> visited = new HashSet<>();
 
        // Iterate over all nodes in the graph
        for (String node : graph.keySet()) {
            // Skip nodes that have already been visited
            if (!visited.contains(node)) {
                // Create a new clique
                Set<String> clique = new HashSet<>();
                // Perform a depth-first search starting at the current node
                dfs(node, graph, visited, clique);
                // Add the clique to the list if it contains more than one node
                if (clique.size() > 1) {
                    cliques.add(clique);
                }
            }
        }
 
        return cliques;
    }
 
    // Performs a depth-first search from the given node
    static void dfs(String node, Map<String, List<String>> graph, Set<String> visited, Set<String> clique) {
        // Mark the current node as visited
        visited.add(node);
        // Add the current node to the clique
        clique.add(node);
 
        // Iterate over the neighbors of the current node
        for (String neighbor : graph.get(node)) {
            // Skip neighbors that have already been visited
            if (!visited.contains(neighbor)) {
                // Perform a depth-first search from the current neighbor
                dfs(neighbor, graph, visited, clique);
            }
        }
    }
 
    public static void main(String[] args) {
        // Example usage
        Map<String, List<String>> graph = new HashMap<>();
        graph.put("A", Arrays.asList("B", "C", "D"));
        graph.put("B", Arrays.asList("A", "C", "D"));
        graph.put("C", Arrays.asList("A", "B", "D"));
        graph.put("D", Arrays.asList("A", "B", "C"));
        graph.put("E", Arrays.asList("F"));
        graph.put("F", Arrays.asList("E"));
 
        List<Set<String>> cliques = findCliques(graph);
        System.out.println("Number of cliques: " + cliques.size());
        System.out.println("Cliques: " + cliques);
    }
}


C++




#include <iostream>
#include <vector>
#include <set>
#include <map>
 
using namespace std;
 
// Finds all cliques in a graph represented as a map
// where the keys are nodes and the values are lists of neighbors
vector<set<string>> findCliques(map<string, vector<string>> graph) {
    // Vector to store the cliques
    vector<set<string>> cliques;
    // Set to keep track of visited nodes
    set<string> visited;
 
    // Iterate over all nodes in the graph
    for (auto node : graph) {
        // Skip nodes that have already been visited
        if (visited.find(node.first) == visited.end()) {
            // Create a new clique
            set<string> clique;
            // Perform a depth-first search starting at the current node
            dfs(node.first, graph, visited, clique);
            // Add the clique to the vector if it contains more than one node
            if (clique.size() > 1) {
                cliques.push_back(clique);
            }
        }
    }
 
    return cliques;
}
 
// Performs a depth-first search from the given node
void dfs(string node, map<string, vector<string>> graph, set<string>& visited, set<string>& clique) {
    // Mark the current node as visited
    visited.insert(node);
    // Add the current node to the clique
    clique.insert(node);
 
    // Iterate over the neighbors of the current node
    for (string neighbor : graph[node]) {
        // Skip neighbors that have already been visited
        if (visited.find(neighbor) == visited.end()) {
            // Perform a depth-first search from the current neighbor
            dfs(neighbor, graph, visited, clique);
        }
    }
}
 
int main() {
    // Example usage
  map<string, vector<string>> graph;
    graph["A"] = {"B", "C", "D"};
    graph["B"] = {"A", "C", "D"};
    graph["C"] = {"A", "B", "D"};
    graph["D"] = {"A", "B", "C"};
    graph["E"] = {"F"};
    graph["F"] = {"E"};
 
    vector<set<string>> cliques = findCliques(graph);
    cout << "Number of cliques: " << cliques.size() << endl;
    cout << "Cliques: [";
    for(auto clique:cliques) {
        for(auto it:clique){
            cout << it << ",";
        }
        cout << "]";
    }
    return 0;
}


C#




using System;
using System.Collections.Generic;
 
class CliqueFinder {
    // Finds all cliques in a graph represented as a dictionary
    // where the keys are nodes and the values are lists of neighbors
    static List<HashSet<string>> FindCliques(Dictionary<string, List<string>> graph) {
        // List to store the cliques
        List<HashSet<string>> cliques = new List<HashSet<string>>();
        // Set to keep track of visited nodes
        HashSet<string> visited = new HashSet<string>();
 
        // Iterate over all nodes in the graph
        foreach (string node in graph.Keys) {
            // Skip nodes that have already been visited
            if (!visited.Contains(node)) {
                // Create a new clique
                HashSet<string> clique = new HashSet<string>();
                // Perform a depth-first search starting at the current node
                Dfs(node, graph, visited, clique);
                // Add the clique to the list if it contains more than one node
                if (clique.Count > 1) {
                    cliques.Add(clique);
                }
            }
        }
 
        return cliques;
    }
 
    // Performs a depth-first search from the given node
    static void Dfs(string node, Dictionary<string, List<string>> graph, HashSet<string> visited, HashSet<string> clique) {
        // Mark the current node as visited
        visited.Add(node);
        // Add the current node to the clique
        clique.Add(node);
 
        // Iterate over the neighbors of the current node
        foreach (string neighbor in graph[node]) {
            // Skip neighbors that have already been visited
            if (!visited.Contains(neighbor)) {
                // Perform a depth-first search from the current neighbor
                Dfs(neighbor, graph, visited, clique);
            }
        }
    }
 
    public static void Main(string[] args) {
        // Example usage
        Dictionary<string, List<string>> graph = new Dictionary<string, List<string>>();
        graph.Add("A", new List<string>() { "B", "C", "D" });
        graph.Add("B", new List<string>() { "A", "C", "D" });
        graph.Add("C", new List<string>() { "A", "B", "D" });
        graph.Add("D", new List<string>() { "A", "B", "C" });
        graph.Add("E", new List<string>() { "F" });
        graph.Add("F", new List<string>() { "E" });
 
        List<HashSet<string>> cliques = FindCliques(graph);
        Console.WriteLine("Number of cliques: " + cliques.Count);
        Console.WriteLine("Cliques: " + cliques);
    }
}


Javascript




function findCliques(graph) {
    // List to store the cliques
    let cliques = [];
    // Set to keep track of visited nodes
    let visited = new Set();
 
    // Iterate over all nodes in the graph
    for (let node in graph) {
        // Skip nodes that have already been visited
        if (!visited.has(node)) {
            // Create a new clique
            let clique = new Set();
            // Perform a depth-first search starting at the current node
            dfs(node, graph, visited, clique);
            // Add the clique to the list if it contains more than one node
            if (clique.size > 1) {
                cliques.push(clique);
            }
        }
    }
 
    return cliques;
}
 
// Performs a depth-first search from the given node
function dfs(node, graph, visited, clique) {
    // Mark the current node as visited
    visited.add(node);
    // Add the current node to the clique
    clique.add(node);
 
    // Iterate over the neighbors of the current node
    for (let neighbor of graph[node]) {
        // Skip neighbors that have already been visited
        if (!visited.has(neighbor)) {
            // Perform a depth-first search from the current neighbor
            dfs(neighbor, graph, visited, clique);
        }
    }
}
 
// Example usage
let graph = {
    'A': ['B', 'C', 'D'],
    'B': ['A', 'C', 'D'],
    'C': ['A', 'B', 'D'],
    'D': ['A', 'B', 'C'],
    'E': ['F'],
    'F': ['E']
};
 
let cliques = findCliques(graph);
console.log(`Number of cliques: ${cliques.length}`);
console.log(`Cliques: ${cliques}`);


Output

Number of cliques: 2
Cliques: [{'D', 'B', 'C', 'A'}, {'E', 'F'}]

The time complexity of the find_cliques function provided in the code is O(n+m), where n is the number of vertices in the graph and m is the number of edges. This is because the function performs a depth-first search (DFS) on the graph, which takes time proportional to the number of vertices and
The auxiliary space of this function is O(n) because it uses a set to store the visited vertices and a list to store the cliques, both of which have sizes proportional to the number of vertices in the graph.

Note: that the time and space complexity of this function may be different if the graph is represented differently, for example as an adjacency matrix instead of an adjacency list. The time and space complexity of an algorithm can also depend on the specific implementation and the specific input.

Time and space complexity of each approach:

Approach Time Complexity Space Complexity
Bron-Kerbosch algorithm

O(3^(n/3))

 O(n)

Tomita algorithm

O(4^(n/4))

O(n)

Pivot Bron-Kerbosch algorithm

O(2^n)

 O(2^n)

Hybrid algorithm

O(2^n)

 O(2^n)

  • The Bron-Kerbosch algorithm and the Tomita algorithm are both pivot-based algorithms that use a recursive approach to find all cliques in a graph. 
  • The Pivot Bron-Kerbosch algorithm is an improvement over the Bron-Kerbosch algorithm that reduces the time complexity by using a pivot element to prune the search space. 
  • The hybrid algorithm is a combination of the Bron-Kerbosch and Tomita algorithms that uses both pivoting and recursion to find all cliques in a graph.
  • It is worth noting that the time complexity of these algorithms can be improved by using parallelization techniques or approximative algorithms, but these approaches may not find all cliques in the graph and may have additional overhead costs.


Last Updated : 16 Feb, 2023
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads