Algorithm to find cliques of a given size k【O(n^k) time complexity】


Reading time: 35 minutes

A clique is a subset of vertices of an undirected graph G such that every two distinct vertices in the clique are adjacent; that is, its induced subgraph is complete. Cliques are one of the basic concepts of graph theory and are used in many other mathematical problems and constructions on graphs. So we can say that a clique in an undirected graph is a subgraph that is complete.

A maximal clique is a clique that cannot be extended by including one more adjacent vertex, that is, a clique which does not exist exclusively within the vertex set of a larger clique. Maximal cliques can be very small. A graph may contain a non-maximal clique with many vertices and a separate clique of size 2 which is maximal. While a maximum (i.e., largest) clique is necessarily maximal, the converse does not hold.

A clique of size k in a graph G is a clique of graph G containing k vertices, i.e. the degree of each vertex is k-1 in that clique.
So particularly, if there is a subset of k vertices that are connected to each other in the graph G, we say that graph contains a k-clique.
A k-clique can be a maximal clique or can be a subset of a maximal clique, so if a graph contains a clique of size more than k then it definitely contains a clique of size k.
For example the graph shown below:
k_cliques

Algorithm

We can find all the 2-cliques by simply enumerating all the edges.
To find k+1-cliques, we can use the previous results. Compare all the pairs of k-cliques. If the two subgraphs have k-1 vertices in common and graph contains the missing edge, we can form a k+1-clique.

def k_cliques(graph):
    # 2-cliques
    cliques = [{i, j} for i, j in graph.edges() if i != j]
    k = 2

    while cliques:
        # result
        yield k, cliques

        # merge k-cliques into (k+1)-cliques
        cliques_1 = set()
        for u, v in combinations(cliques, 2):
            w = u ^ v
            if len(w) == 2 and graph.has_edge(*w):
                cliques_1.add(tuple(u | w))

        # remove duplicates
        cliques = list(map(set, cliques_1))
        k += 1

The above algorithm of finding k-clique in a graph G takes polinomial time for its execution. The algorithm starts from 2-clique pairs and use this as base data to find 3-cliques and more.
To generate 3-cliques from 2-cliques we take each combination pair of 2-cliques and take intersection of the pair, if the intersection is an edge and it is present in the graph then the union of the pair is a clique of size 3. By doing intersection of the pair we find the missing edge so that the 2-clique can be extended to 3-clique, and if the edge is present in the graph then we extend the 2-clique pair into 3-clique and store it. In similar way we generate k+1-clique from k-clique.
Let's understand with it with a graph with 4 vertices:
2-3_clique
To find k-cliques we iterate the same method O(k) times. The method which finds the p+1-clique from p-clique takes O(n) time where n is number of vertices. So in overall the algorithm takes O(nk) time in the worst case.

Implementation

Code in Python3

from itertools import combinations
import networkx as nx


def k_cliques(graph):
    # 2-cliques
    cliques = [{i, j} for i, j in graph.edges() if i != j]
    k = 2

    while cliques:
        # result
        yield k, cliques

        # merge k-cliques into (k+1)-cliques
        cliques_1 = set()
        for u, v in combinations(cliques, 2):
            w = u ^ v
            if len(w) == 2 and graph.has_edge(*w):
                cliques_1.add(tuple(u | w))

        # remove duplicates
        cliques = list(map(set, cliques_1))
        k += 1


def print_cliques(graph, size_k):
    for k, cliques in k_cliques(graph):
        if k == size_k:
            print('%d-cliques = %d, %s.' % (k, len(cliques), cliques))


nodes, edges = 6, 10
size_k = 3
graph = nx.Graph()
graph.add_nodes_from(range(nodes))
graph.add_edge(1, 2)
graph.add_edge(1, 3)
graph.add_edge(1, 5)
graph.add_edge(2, 3)
graph.add_edge(2, 4)
graph.add_edge(2, 6)
graph.add_edge(3, 4)
graph.add_edge(3, 6)
graph.add_edge(4, 5)
graph.add_edge(4, 6)

print_cliques(graph, size_k)

Output

3-cliques = 5, [{3, 4, 6}, {2, 3, 6}, {2, 4, 6}, {1, 2, 3}, {2, 3, 4}].

Complexity


Time Complexity

  • The k-clique algorithm takes O(nk) (i.e. polynomial) time in the worst case.

Space Complexity

  • The k-clique algoorithm takes O(n2) auxiliary space in the worst case.

References