Table of Contents#
- Introduction
- Prerequisites
- Problem Definition
- Key Concepts
- Algorithmic Approaches
- Implementation Details
- Complexity Analysis
- Optimization Techniques
- Example Usage
- Common Pitfalls
- Conclusion
- References
Prerequisites#
- Basic understanding of graph theory (vertices, edges, adjacency)
- Familiarity with prime numbers and basic number theory
- Knowledge of backtracking algorithms and combinatorial optimization
- Programming experience (Python examples provided)
Problem Definition#
Given an undirected graph G = (V, E) with |V| vertices and |E| edges, count all cliques in G where the clique size (number of vertices) is a prime number.
Formal Definition:
- A clique is a subset of vertices where every two distinct vertices are adjacent
- A prime clique is a clique C such that |C| is a prime number
- We need to count all distinct prime cliques in the graph
Key Concepts#
1. Clique Properties#
- Maximal Clique: A clique that cannot be extended by adding adjacent vertices
- Maximum Clique: The largest possible clique in the graph
- Clique Number: Size of the maximum clique (ω(G))
2. Prime Number Properties#
- Prime numbers ≥ 2: 2, 3, 5, 7, 11, 13, ...
- For our problem, clique sizes 0 and 1 are not considered (1 is not prime by definition)
3. Graph Representation#
# Adjacency Matrix
graph = [[0, 1, 1, 0],
[1, 0, 1, 1],
[1, 1, 0, 1],
[0, 1, 1, 0]]
# Adjacency List
graph = {
0: [1, 2],
1: [0, 2, 3],
2: [0, 1, 3],
3: [1, 2]
}Algorithmic Approaches#
Approach 1: Recursive Backtracking for All Cliques#
The problem requires counting all cliques of prime size, including non-maximal ones. For example, in a triangle, both the triangle itself (size 3) and its three edges (size 2) are prime cliques. Bron-Kerbosch only enumerates maximal cliques, missing non-maximal ones like edges within larger cliques. We need an algorithm that exhaustively enumerates every clique.
A recursive backtracking approach can enumerate all cliques by exploring two branches at each step: either include a vertex in the current clique, or exclude it:
def is_prime(n):
"""Check if a number is prime"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
class PrimeCliqueCounter:
def __init__(self, graph):
self.graph = [set(adj) for adj in graph]
self.n = len(graph)
self.prime_clique_count = 0
self.prime_sizes = set()
max_possible_size = self.n
for i in range(2, max_possible_size + 1):
if is_prime(i):
self.prime_sizes.add(i)
def count_prime_cliques(self):
"""Count all prime cliques (including non-maximal) via backtracking"""
vertices = list(range(self.n))
self.prime_clique_count = 0
self._backtrack([], set(vertices))
return self.prime_clique_count
def _backtrack(self, current_clique, candidates):
"""Recursively enumerate all cliques via backtracking"""
if len(current_clique) > 0:
if len(current_clique) in self.prime_sizes:
self.prime_clique_count += 1
if len(candidates) == 0:
return
for i, v in enumerate(list(candidates)):
new_candidates = candidates - self.graph[v]
self._backtrack(current_clique + [v], new_candidates)Approach 2: Bron-Kerbosch Algorithm with Prime Filtering#
The Bron-Kerbosch algorithm is the standard approach for enumerating all maximal cliques. We can modify it to count maximal cliques of prime sizes. Note that this approach only counts maximal prime cliques, not all prime cliques.
Approach 2: Dynamic Programming with Bitmasking#
For smaller graphs (n ≤ 20-25), we can use bitmasking to efficiently enumerate all cliques.
def count_prime_cliques_bitmask(adj_matrix):
n = len(adj_matrix)
max_mask = 1 << n
prime_count = 0
# Precompute primes
primes = set()
for i in range(2, n + 1):
if is_prime(i):
primes.add(i)
# DP array to track cliques
is_clique = [False] * max_mask
is_clique[0] = True # Empty set is a clique
for mask in range(1, max_mask):
# Find the vertex with smallest index in mask
v = 0
while not (mask & (1 << v)):
v += 1
# Check if adding v maintains clique property
prev_mask = mask ^ (1 << v)
if is_clique[prev_mask]:
# Check if v is connected to all vertices in prev_mask
valid = True
for u in range(n):
if (prev_mask & (1 << u)) and not adj_matrix[v][u]:
valid = False
break
if valid:
is_clique[mask] = True
size = bin(mask).count('1')
if size in primes:
prime_count += 1
return prime_countImplementation Details#
Efficient Prime Checking#
def sieve_of_eratosthenes(max_n):
"""Generate primes up to max_n using Sieve of Eratosthenes"""
is_prime = [True] * (max_n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(max_n**0.5) + 1):
if is_prime[i]:
for j in range(i*i, max_n + 1, i):
is_prime[j] = False
primes = set(i for i in range(2, max_n + 1) if is_prime[i])
return primesOptimized Bron-Kerbosch with Degeneracy Ordering#
def degeneracy_ordering(graph):
"""Compute degeneracy ordering of vertices"""
n = len(graph)
degrees = [len(adj) for adj in graph]
ordering = []
used = [False] * n
for _ in range(n):
# Find vertex with minimum degree among unused vertices
min_degree = float('inf')
min_vertex = -1
for v in range(n):
if not used[v] and degrees[v] < min_degree:
min_degree = degrees[v]
min_vertex = v
ordering.append(min_vertex)
used[min_vertex] = True
# Update degrees of neighbors
for neighbor in graph[min_vertex]:
if not used[neighbor]:
degrees[neighbor] -= 1
return ordering
def optimized_bron_kerbosch(graph, degeneracy_order):
"""Bron-Kerbosch with degeneracy ordering for better performance"""
n = len(graph)
prime_cliques = 0
primes = sieve_of_eratosthenes(n)
# Convert to neighbor sets for faster operations
neighbors = [set(adj) for adj in graph]
for i in range(n):
v = degeneracy_order[i]
# Only consider vertices that come later in ordering
candidates = set(degeneracy_order[i+1:])
candidates = candidates.intersection(neighbors[v])
r = {v}
p = candidates.intersection(neighbors[v])
x = set()
# Recursive call
def bk_recursive(r, p, x):
nonlocal prime_cliques
if not p and not x:
if len(r) in primes:
prime_cliques += 1
return
# Choose pivot
pivot_set = p.union(x)
pivot = next(iter(pivot_set))
for v in p.difference(neighbors[pivot]):
new_r = r.union({v})
new_p = p.intersection(neighbors[v])
new_x = x.intersection(neighbors[v])
bk_recursive(new_r, new_p, new_x)
p.remove(v)
x.add(v)
bk_recursive(r, p, x)
return prime_cliquesComplexity Analysis#
Time Complexity#
- Bron-Kerbosch: O(3^(n/3)) in worst case for n vertices
- Bitmask DP: O(2^n × n²) - practical only for n ≤ 25
- Space Complexity: O(2^n) for DP, O(n) for Bron-Kerbosch
Practical Considerations#
- For sparse graphs, Bron-Kerbosch performs significantly better
- Bitmask approach is only feasible for very small graphs
- Real-world graphs often have structure that can be exploited
Optimization Techniques#
1. Pruning Strategies#
def bron_kerbosch_with_pruning(r, p, x, primes, best_possible, neighbors):
"""Bron-Kerbosch with additional pruning"""
if len(p) == 0 and len(x) == 0:
return 1 if len(r) in primes else 0
if len(r) + len(p) < min(primes):
return 0
count = 0
pivot = next(iter(p.union(x)))
for v in p.difference(neighbors[pivot]):
if len(r) + len(p) < min(primes):
break
count += bron_kerbosch_with_pruning(
r.union({v}),
p.intersection(neighbors[v]),
x.intersection(neighbors[v]),
primes,
best_possible,
neighbors
)
p.remove(v)
x.add(v)
return count2. Parallel Processing#
For large graphs, divide the search space and process in parallel:
from concurrent.futures import ProcessPoolExecutor
import math
def parallel_prime_clique_count(graph, num_processes=4):
"""Parallel implementation for large graphs"""
n = len(graph)
primes = sieve_of_eratosthenes(n)
neighbors = [set(adj) for adj in graph]
# Divide vertices among processes
vertices_per_process = math.ceil(n / num_processes)
def process_subgraph(start_vertex):
count = 0
for i in range(start_vertex, min(start_vertex + vertices_per_process, n)):
# Process cliques starting from vertex i
p = set(range(i+1, n)).intersection(neighbors[i])
count += bron_kerbosch_with_pruning(
{i}, p, set(), primes, n
)
return count
with ProcessPoolExecutor(max_workers=num_processes) as executor:
results = list(executor.map(process_subgraph,
range(0, n, vertices_per_process)))
return sum(results)Example Usage#
Complete Example#
def main():
# Example graph: 5-vertex graph with known cliques
graph = [
[1, 2, 3], # Vertex 0
[0, 2, 4], # Vertex 1
[0, 1, 3, 4], # Vertex 2
[0, 2, 4], # Vertex 3
[1, 2, 3] # Vertex 4
]
counter = PrimeCliqueCounter(graph)
result = counter.count_prime_cliques()
print(f"Number of prime cliques: {result}")
# Using bitmask approach for smaller graph
adj_matrix = [
[0, 1, 1, 0],
[1, 0, 1, 1],
[1, 1, 0, 1],
[0, 1, 1, 0]
]
result_bitmask = count_prime_cliques_bitmask(adj_matrix)
print(f"Bitmask result: {result_bitmask}")
if __name__ == "__main__":
main()Expected Output#
Number of prime cliques: 6
Bitmask result: 3
Common Pitfalls#
- Incorrect Prime Definition: Remember that 1 is not prime
- Duplicate Counting: Ensure each clique is counted exactly once
- Memory Management: Bitmask approach can exhaust memory for large graphs
- Graph Representation: Choose appropriate representation (adjacency list vs matrix)
- Pruning Over-aggression: Avoid pruning valid prime cliques
Conclusion#
Counting prime cliques is a challenging problem that combines graph theory with number theory. The Bron-Kerbosch algorithm with appropriate optimizations provides the most practical solution for real-world graphs. For very small graphs, bitmask dynamic programming can be effective.
Key takeaways:
- Precompute primes for efficient size checking
- Use degeneracy ordering and pruning for better performance
- Consider parallel processing for large graphs
- Choose the right algorithm based on graph size and density
This problem finds applications in network analysis, bioinformatics, and social network studies where prime-sized cohesive subgroups may have special significance.
References#
- Bron, C., & Kerbosch, J. (1973). "Algorithm 457: finding all cliques of an undirected graph." Communications of the ACM.
- Tomita, E., Tanaka, A., & Takahashi, H. (2006). "The worst-case time complexity for generating all maximal cliques." Theoretical Computer Science.
- Eppstein, D., Löffler, M., & Strash, D. (2010). "Listing all maximal cliques in sparse graphs in near-optimal time." Algorithms and Computation.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). "Introduction to Algorithms." MIT Press.
- Hardy, G. H., & Wright, E. M. (2008). "An Introduction to the Theory of Numbers." Oxford University Press.