diff --git a/docs/make.jl b/docs/make.jl index 0eb9a74a0..522ad31ff 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -54,6 +54,7 @@ pages_files = [ "Algorithms API" => [ "algorithms/biconnectivity.md", "algorithms/centrality.md", + "algorithms/chordality.md", "algorithms/community.md", "algorithms/connectivity.md", "algorithms/cut.md", diff --git a/docs/src/algorithms/chordality.md b/docs/src/algorithms/chordality.md new file mode 100644 index 000000000..dc3d3ef8b --- /dev/null +++ b/docs/src/algorithms/chordality.md @@ -0,0 +1,17 @@ +# Degeneracy + +*Graphs.jl* provides functionality for checking whether a graph is [chordal](https://en.wikipedia.org/wiki/Chordal_graph). + +## Index + +```@index +Pages = ["chordality.md"] +``` + +## Full docs + +```@autodocs +Modules = [Graphs] +Pages = ["chordality.jl"] + +``` diff --git a/src/Graphs.jl b/src/Graphs.jl index f9ae18680..db0bbd473 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -210,6 +210,9 @@ export # coloring greedy_color, + # chordality + is_chordal, + # connectivity connected_components, connected_components!, @@ -520,6 +523,7 @@ include("iterators/bfs.jl") include("iterators/dfs.jl") include("traversals/eulerian.jl") include("traversals/all_simple_paths.jl") +include("chordality.jl") include("connectivity.jl") include("distance.jl") include("editdist.jl") diff --git a/src/chordality.jl b/src/chordality.jl new file mode 100644 index 000000000..c3a11d6ff --- /dev/null +++ b/src/chordality.jl @@ -0,0 +1,103 @@ +""" + is_chordal(g) + +Check whether a graph is chordal. + +A graph is said to be *chordal* if every cycle of length `≥ 4` has a chord +(i.e., an edge between two vertices not adjacent in the cycle). + +### Performance +This algorithm is linear in the number of vertices and edges of the graph (i.e., +it runs in `O(nv(g) + ne(g))` time). + +### Implementation Notes +`g` is chordal if and only if it admits a perfect elimination ordering—that is, +an ordering of the vertices of `g` such that for every vertex `v`, the set of +all neighbors of `v` that come later in the ordering forms a complete graph. +This is precisely the condition checked by the maximum cardinality search +algorithm [1], implemented herein. + +We take heavy inspiration here from the existing Python implementation in [2]. + +Not implemented for directed graphs, graphs with self-loops, or graphs with +parallel edges. + +### References +[1] Tarjan, Robert E. and Mihalis Yannakakis. "Simple Linear-Time Algorithms to + Test Chordality of Graphs, Test Acyclicity of Hypergraphs, and Selectively + Reduce Acyclic Hypergraphs." *SIAM Journal on Computing* 13, no. 3 (1984): + 566–79. https://doi.org/10.1137/0213035. +[2] NetworkX Developers. "is_chordal." NetworkX 3.5 documentation. NetworkX, + May 29, 2025. Accessed June 2, 2025. + https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.chordal.is_chordal.html. + +### Examples +```jldoctest +julia> using Graphs + +julia> is_chordal(cycle_graph(3)) +true + +julia> is_chordal(cycle_graph(4)) +false + +julia> g = cycle_graph(4); add_edge!(g, 1, 3); + +julia> is_chordal(g) +true + +``` +""" +function is_chordal end + +@traitfn function is_chordal(g::AG::(!IsDirected)) where {AG<:AbstractGraph} + # The `AbstractGraph` interface does not support parallel edges, so no need to check + if has_self_loops(g) + throw(ArgumentError("Graph must not have self-loops")) + end + + # Every graph of order `< 4` has no cycles of length `≥ 4` and thus is trivially chordal + if nv(g) < 4 + return true + end + + unnumbered = Set(vertices(g)) + start_vertex = pop!(unnumbered) # The search can start from any arbitrary vertex + numbered = Set(start_vertex) + + #= Searching by maximum cardinality ensures that in any possible perfect elimination + ordering of `g`, `subsequent_neighbors` is precisely the set of neighbors of `v` that + come later in the ordering. Therefore, if the subgraph induced by `subsequent_neighbors` + in any iteration is not complete, `g` cannot be chordal. =# + while !isempty(unnumbered) + # `v` is the vertex in `unnumbered` with the most neighbors in `numbered` + v = _max_cardinality_vertex(g, unnumbered, numbered) + delete!(unnumbered, v) + push!(numbered, v) + subsequent_neighbors = filter(in(numbered), collect(neighbors(g, v))) + + if !_induces_clique(subsequent_neighbors, g) + return false + end + end + + #= A perfect elimination ordering is an "if and only if" condition for chordality, so if + every `subsequent_neighbors` set induced a complete subgraph, `g` must be chordal. =# + return true +end + +function _max_cardinality_vertex( + g::AbstractGraph{T}, unnumbered::Set{T}, numbered::Set{T} +) where {T} + return argmax(v -> count(in(numbered), neighbors(g, v)), unnumbered) +end + +function _induces_clique(vertex_subset::Vector{T}, g::AbstractGraph{T}) where {T} + for (i, u) in enumerate(vertex_subset), v in Iterators.drop(vertex_subset, i) + if !has_edge(g, u, v) + return false + end + end + + return true +end diff --git a/test/Project.toml b/test/Project.toml index 5ae3fa603..ec93d1bdc 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,6 +6,7 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/test/chordality.jl b/test/chordality.jl new file mode 100644 index 000000000..89c6a299c --- /dev/null +++ b/test/chordality.jl @@ -0,0 +1,3 @@ +@testset "Chordality" begin + # TODO: Add tests +end diff --git a/test/runtests.jl b/test/runtests.jl index 4045eb60b..e8d9c130f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,7 @@ using Random using Logging: NullLogger, with_logger using Statistics: mean, std using StableRNGs +using IGraphs using Pkg using Unitful @@ -93,6 +94,7 @@ tests = [ "cycles/limited_length", "cycles/incremental", "edit_distance", + "chordality", "connectivity", "persistence/persistence", "shortestpaths/utils",