Skip to content

Commit 32cc562

Browse files
committed
WIP: Add an is_chordal algorithm
We implement Tarjan and Yannakakis (1984)'s MCS algorithm, taking inspiration from the existing NetworkX implementation. Everything is done except for example doctests in src/chordality.jl and unit tests in test/chordality.jl. (This PR is part of a new suite of algorithms outlined in issue #431.)
1 parent c001dab commit 32cc562

File tree

4 files changed

+86
-0
lines changed

4 files changed

+86
-0
lines changed

src/Graphs.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ export
205205
# coloring
206206
greedy_color,
207207

208+
# chordality
209+
is_chordal,
210+
208211
# connectivity
209212
connected_components,
210213
strongly_connected_components,
@@ -504,6 +507,7 @@ include("iterators/bfs.jl")
504507
include("iterators/dfs.jl")
505508
include("traversals/eulerian.jl")
506509
include("traversals/all_simple_paths.jl")
510+
include("chordality.jl")
507511
include("connectivity.jl")
508512
include("distance.jl")
509513
include("editdist.jl")

src/chordality.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
is_chordal(g)
3+
4+
Check whether a graph is chordal.
5+
6+
A graph is said to be *chordal* if every cycle of length `≥ 4` has a chord
7+
(i.e., an edge between two nodes not adjacent in the cycle).
8+
9+
### Performance
10+
This algorithm is linear in the number of vertices and edges of the graph (i.e.,
11+
it runs in `O(nv(g) + ne(g))` time).
12+
13+
### Implementation Notes
14+
`g` is chordal if and only if it admits a perfect elimination ordering—that is,
15+
an ordering of the vertices of `g` such that for every vertex `v`, the set of
16+
all neighbors of `v` that come later in the ordering forms a complete graph.
17+
This is precisely the condition checked by the maximum cardinality search
18+
algorithm [1], implemented herein.
19+
20+
We take heavy inspiration here from the existing Python implementation in [2].
21+
22+
Not implemented for directed graphs, graphs with self-loops, or graphs with
23+
parallel edges.
24+
25+
### References
26+
[1] Tarjan, Robert E. and Mihalis Yannakakis. "Simple Linear-Time Algorithms to
27+
Test Chordality of Graphs, Test Acyclicity of Hypergraphs, and Selectively
28+
Reduce Acyclic Hypergraphs." *SIAM Journal on Computing* 13, no. 3 (1984):
29+
566–79. https://doi.org/10.1137/0213035.
30+
[2] NetworkX Developers. "is_chordal." NetworkX 3.5 documentation. NetworkX,
31+
May 29, 2025. Accessed June 2, 2025.
32+
https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.chordal.is_chordal.html.
33+
34+
# Examples
35+
TODO: Add examples
36+
"""
37+
function is_chordal(g::AbstractSimpleGraph)
38+
# The possibility of self-loops is already ruled out by the `AbstractSimpleGraph` type
39+
is_directed(g) && throw(ArgumentError("Graph must be undirected"))
40+
has_self_loops(g) && throw(ArgumentError("Graph must not have self-loops"))
41+
42+
# Every graph of order `< 4` has no cycles of length `≥ 4` and thus is trivially chordal
43+
nv(g) < 4 && return true
44+
45+
unnumbered = Set(vertices(g))
46+
start_vertex = pop!(unnumbered) # The search can start from any arbitrary vertex
47+
numbered = Set(start_vertex)
48+
49+
#= Searching by maximum cardinality ensures that in any possible perfect elimination
50+
ordering of `g`, `purported_clique_nodes` is precisely the set of neighbors of `v` that
51+
come later in the ordering. Hence, if the subgraph induced by `purported_clique_nodes`
52+
in any iteration is not complete, `g` cannot be chordal. =#
53+
while !isempty(unnumbered)
54+
# `v` is the vertex in `unnumbered` with the most neighbors in `numbered`
55+
v = _max_cardinality_node(g, unnumbered, numbered)
56+
delete!(unnumbered, v)
57+
push!(numbered, v)
58+
59+
# A complete subgraph of a larger graph is called a "clique," hence the naming here
60+
purported_clique_nodes = intersect(neighbors(g, v), numbered)
61+
purported_clique = induced_subgraph(g, purported_clique_nodes)
62+
63+
_is_complete_graph(purported_clique) || return false
64+
end
65+
66+
#= That `g` admits a perfect elimination ordering is an "if and only if" condition for
67+
chordality, so if every `purported_clique` was indeed complete, `g` must be chordal. =#
68+
return true
69+
end
70+
71+
function _max_cardinality_node(
72+
g::AbstractSimpleGraph, unnumbered::Set{T}, numbered::Set{T}
73+
) where {T}
74+
cardinality(v::T) = count(in(numbered), neighbors(g, v))
75+
return argmax(cardinality, unnumbered)
76+
end
77+
78+
_is_complete_graph(g::AbstractSimpleGraph) = density(g) == 1

test/chordality.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@testset "Chordality" begin
2+
# TODO: Add tests
3+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ tests = [
9090
"cycles/limited_length",
9191
"cycles/incremental",
9292
"edit_distance",
93+
"chordality",
9394
"connectivity",
9495
"persistence/persistence",
9596
"shortestpaths/utils",

0 commit comments

Comments
 (0)