Skip to content

Commit bc29440

Browse files
authored
Merge pull request #88 from hmdsefi/ft-clique
Ft clique
2 parents dc2f608 + df1175a commit bc29440

File tree

3 files changed

+573
-0
lines changed

3 files changed

+573
-0
lines changed

partition/bron_kerbosch.go

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
package partition
2+
3+
import (
4+
"container/heap"
5+
"math/bits"
6+
7+
"github.com/hmdsefi/gograph"
8+
)
9+
10+
// MaximalCliques finds all maximal cliques in the input graph using the
11+
// Bron–Kerbosch algorithm with pivot selection, degeneracy ordering, and bitsets.
12+
//
13+
// A **clique** is a subset of vertices where every two distinct vertices are
14+
// connected by an edge. A **maximal clique** is a clique that cannot be extended
15+
// by adding another adjacent vertex.
16+
//
17+
// This implementation is optimized for performance:
18+
// 1. **Degeneracy ordering**: processes vertices in a specific order to reduce
19+
// recursive calls and improve efficiency.
20+
// 2. **Pivot selection**: selects a pivot vertex at each recursive call to
21+
// reduce the number of branches.
22+
// 3. **Bitsets**: represents candidate sets (P, X) efficiently using []uint64
23+
// to speed up set operations on large graphs.
24+
//
25+
// Parameters:
26+
// - g: a gograph.Graph[T] representing the graph. T must be a comparable type.
27+
// Each vertex in the graph can be accessed via g.GetAllVertices() and
28+
// neighbors via Vertex.Neighbors().
29+
//
30+
// Returns:
31+
// - [][]*gograph.Vertex[T]: a slice of maximal cliques. Each clique is a slice
32+
// of pointers to Vertex[T]. Vertices in a clique are guaranteed to be fully
33+
// connected, and no clique is a subset of another.
34+
//
35+
// Complexity:
36+
// - **Time Complexity**: O(3^(n/3)) in the worst case for general graphs,
37+
// where n is the number of vertices. This is the known bound for enumerating
38+
// all maximal cliques. In practice, degeneracy ordering + pivoting reduces
39+
// the number of recursive calls significantly on sparse graphs.
40+
// - **Space Complexity**: O(n^2 / 64) for bitsets plus O(k*n) for storing cliques,
41+
// where k is the number of maximal cliques. Additional recursion stack space
42+
// is O(n) in depth.
43+
//
44+
// Example usage:
45+
//
46+
// g := gograph.New[string]()
47+
// a := g.AddVertexByLabel("A")
48+
// b := g.AddVertexByLabel("B")
49+
// c := g.AddVertexByLabel("C")
50+
// _, _ = g.AddEdge(a, b)
51+
// _, _ = g.AddEdge(b, c)
52+
// _, _ = g.AddEdge(c, a)
53+
//
54+
// cliques := MaximalCliques(g)
55+
// for _, clique := range cliques {
56+
// for _, v := range clique {
57+
// fmt.Print(v.Label(), " ")
58+
// }
59+
// fmt.Println()
60+
// }
61+
//
62+
// Notes:
63+
// - The function returns the actual Vertex pointers from the input graph;
64+
// do not modify the vertices while iterating the results.
65+
// - The order of cliques or vertices within a clique is not guaranteed.
66+
// If deterministic ordering is required, use a normalization function
67+
// (e.g., sort by vertex label).
68+
func MaximalCliques[T comparable](g gograph.Graph[T]) [][]*gograph.Vertex[T] {
69+
vertices := g.GetAllVertices()
70+
n := len(vertices)
71+
if n == 0 {
72+
return nil
73+
}
74+
75+
// label -> index
76+
indexOf := make(map[T]int, n)
77+
for i, v := range vertices {
78+
indexOf[v.Label()] = i
79+
}
80+
81+
// adjacency list (indices) and adjacency bitsets
82+
adj := make([][]int, n)
83+
neighborsBits := make([][]uint64, n)
84+
words := wordLen(n)
85+
for i := 0; i < n; i++ {
86+
neighborsBits[i] = make([]uint64, words)
87+
}
88+
89+
for i, v := range vertices {
90+
for _, nb := range v.Neighbors() {
91+
if j, ok := indexOf[nb.Label()]; ok {
92+
adj[i] = append(adj[i], j)
93+
setBit(neighborsBits[i], j)
94+
}
95+
}
96+
}
97+
98+
// degeneracy ordering (returns vertices removed low-degree first)
99+
order := degeneracyOrder(adj, n)
100+
101+
// posInOrder: index -> position in order (used to split neighbors into P/X)
102+
posInOrder := make([]int, n)
103+
for pos, idx := range order {
104+
posInOrder[idx] = pos
105+
}
106+
107+
// We'll collect cliques as slices of int indices first
108+
var cliquesIdx [][]int
109+
// scratch P/X for top-level calls
110+
P := make([]uint64, words)
111+
X := make([]uint64, words)
112+
113+
// For each vertex v in degeneracy order:
114+
for _, v := range order {
115+
// reset P and X
116+
for i := range P {
117+
P[i] = 0
118+
X[i] = 0
119+
}
120+
// Build P = N(v) ∩ {vertices after v in order}
121+
// Build X = N(v) ∩ {vertices before v in order}
122+
for _, w := range adj[v] {
123+
if posInOrder[w] > posInOrder[v] {
124+
setBit(P, w)
125+
} else {
126+
setBit(X, w)
127+
}
128+
}
129+
130+
// Recurse with R = {v}, cloned P and X
131+
bronKerboschPivot([]int{v}, cloneBitset(P), cloneBitset(X), neighborsBits, n, &cliquesIdx)
132+
133+
// remove v implicitly (degeneracy ensures no duplicates)
134+
}
135+
136+
// convert index cliques to []*Vertex[T]
137+
result := make([][]*gograph.Vertex[T], len(cliquesIdx))
138+
for i, cl := range cliquesIdx {
139+
out := make([]*gograph.Vertex[T], len(cl))
140+
for j, idx := range cl {
141+
out[j] = vertices[idx]
142+
}
143+
result[i] = out
144+
}
145+
return result
146+
}
147+
148+
func wordLen(n int) int { return (n + 63) >> 6 }
149+
150+
func setBit(b []uint64, i int) {
151+
b[i>>6] |= 1 << i & 63
152+
}
153+
154+
func clearBit(b []uint64, i int) {
155+
b[i>>6] &^= 1 << i & 63
156+
}
157+
158+
func cloneBitset(b []uint64) []uint64 {
159+
if b == nil {
160+
return nil
161+
}
162+
c := make([]uint64, len(b))
163+
copy(c, b)
164+
return c
165+
}
166+
167+
func intersectBitset(a, b []uint64) []uint64 {
168+
n := len(a)
169+
if len(b) < n {
170+
n = len(b)
171+
}
172+
res := make([]uint64, n)
173+
for i := 0; i < n; i++ {
174+
res[i] = a[i] & b[i]
175+
}
176+
return res
177+
}
178+
179+
func differenceBitset(a, b []uint64) []uint64 {
180+
n := len(a)
181+
res := make([]uint64, n)
182+
for i := 0; i < n; i++ {
183+
var bi uint64
184+
if i < len(b) {
185+
bi = b[i]
186+
}
187+
res[i] = a[i] &^ bi
188+
}
189+
return res
190+
}
191+
192+
func unionBitset(a, b []uint64) []uint64 {
193+
n := len(a)
194+
if len(b) > n {
195+
n = len(b)
196+
}
197+
res := make([]uint64, n)
198+
for i := 0; i < n; i++ {
199+
var ai, bi uint64
200+
if i < len(a) {
201+
ai = a[i]
202+
}
203+
if i < len(b) {
204+
bi = b[i]
205+
}
206+
res[i] = ai | bi
207+
}
208+
return res
209+
}
210+
211+
func countBits(b []uint64) int {
212+
c := 0
213+
for _, w := range b {
214+
c += bits.OnesCount64(w)
215+
}
216+
return c
217+
}
218+
219+
// forEachSetBit calls fn(i) for every set bit in the bitset b.
220+
// If fn returns true, iteration stops early.
221+
func forEachSetBit(b []uint64, fn func(idx int) (stop bool)) {
222+
for wi, word := range b {
223+
for word != 0 {
224+
t := bits.TrailingZeros64(word)
225+
idx := (wi << 6) + t
226+
if fn(idx) {
227+
return
228+
}
229+
230+
// clear the least significant set bit
231+
word &= word - 1
232+
}
233+
}
234+
}
235+
236+
// ---------------------
237+
// Degeneracy ordering (min-heap approach)
238+
// ---------------------
239+
240+
type heapItem struct {
241+
deg int
242+
v int
243+
// idx field not necessary for this simple push-new-updates approach
244+
}
245+
type minHeap []heapItem
246+
247+
func (h minHeap) Len() int { return len(h) }
248+
func (h minHeap) Less(i, j int) bool { return h[i].deg < h[j].deg }
249+
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
250+
251+
func (h *minHeap) Push(x interface{}) {
252+
*h = append(*h, x.(heapItem)) // nolint
253+
}
254+
255+
func (h *minHeap) Pop() interface{} {
256+
old := *h
257+
n := len(old)
258+
x := old[n-1]
259+
*h = old[:n-1]
260+
return x
261+
}
262+
263+
// degeneracyOrder returns an ordering of vertex indices (low-degree first removed).
264+
func degeneracyOrder(adj [][]int, n int) []int {
265+
deg := make([]int, n)
266+
for i := 0; i < n; i++ {
267+
deg[i] = len(adj[i])
268+
}
269+
270+
h := &minHeap{}
271+
heap.Init(h)
272+
for i := 0; i < n; i++ {
273+
heap.Push(h, heapItem{deg: deg[i], v: i})
274+
}
275+
276+
removed := make([]bool, n)
277+
order := make([]int, 0, n)
278+
279+
for h.Len() > 0 {
280+
it := heap.Pop(h).(heapItem) // nolint
281+
v := it.v
282+
// skip outdated entries (we push updated degs rather than decrease-key)
283+
if removed[v] {
284+
continue
285+
}
286+
removed[v] = true
287+
order = append(order, v)
288+
for _, w := range adj[v] {
289+
if removed[w] {
290+
continue
291+
}
292+
deg[w]--
293+
heap.Push(h, heapItem{deg: deg[w], v: w})
294+
}
295+
}
296+
297+
return order
298+
}
299+
300+
// bronKerboschPivot does recursion; neighborsBits is adjacency bitset per vertex.
301+
// n is number of vertices (for word sizes and potential masking if needed).
302+
func bronKerboschPivot(
303+
r []int,
304+
p []uint64,
305+
x []uint64,
306+
neighborsBits [][]uint64,
307+
n int, // nolint
308+
cliques *[][]int,
309+
) {
310+
// if P and X are empty → R is maximal
311+
if countBits(p) == 0 && countBits(x) == 0 {
312+
c := make([]int, len(r))
313+
copy(c, r)
314+
*cliques = append(*cliques, c)
315+
return
316+
}
317+
318+
// choose pivot u from P ∪ X maximizing |P ∩ N(u)|
319+
unionPX := unionBitset(p, x)
320+
u := -1
321+
best := -1
322+
forEachSetBit(
323+
unionPX, func(idx int) bool {
324+
// compute |P ∩ N(idx)|
325+
cnt := countBits(intersectBitset(p, neighborsBits[idx]))
326+
if cnt > best {
327+
best = cnt
328+
u = idx
329+
}
330+
return false
331+
},
332+
)
333+
334+
// candidates = P \ N(u)
335+
var candidates []uint64
336+
if u >= 0 {
337+
candidates = differenceBitset(p, neighborsBits[u])
338+
} else {
339+
candidates = cloneBitset(p)
340+
}
341+
342+
// iterate over set bits in candidates
343+
// We must iterate over a snapshot (indices) because we'll mutate P/X during loop.
344+
var candidateIndices []int
345+
forEachSetBit(
346+
candidates, func(idx int) bool {
347+
candidateIndices = append(candidateIndices, idx)
348+
return false
349+
},
350+
)
351+
352+
for _, v := range candidateIndices {
353+
// R' = R ∪ {v}
354+
Rp := append(r, v) // nolint
355+
356+
// P' = P ∩ N(v)
357+
Pp := intersectBitset(p, neighborsBits[v])
358+
359+
// X' = X ∩ N(v)
360+
Xp := intersectBitset(x, neighborsBits[v])
361+
362+
// recurse
363+
bronKerboschPivot(Rp, Pp, Xp, neighborsBits, n, cliques)
364+
365+
// move v from P to X in the current frame
366+
clearBit(p, v)
367+
setBit(x, v)
368+
}
369+
}

0 commit comments

Comments
 (0)