Skip to content

Commit b849173

Browse files
author
Release Manager
committed
gh-39938: Add is_chordal_bipartite In `sage/graphs/graph.py` <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> Add `is_chordal_bipartitie` in `sage/graphs/graph.py`. This solves issue #38792 . To add `is_chordal_bipartitie`, I also added `is_Gamma_free` in `sage/matrix/matrix_mod2_dense.pyx`. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> This PR depends on open PR #39794. The first ten commits are the same as PR #39794. URL: #39938 Reported by: Yuta Inoue Reviewer(s): David Coudert
2 parents b056eb9 + ecc8e9f commit b849173

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

src/doc/en/reference/references/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3974,6 +3974,10 @@ REFERENCES:
39743974
Diffie-Hellman groups for Internet Key Exchange (IKE)*, in RFC 3526.
39753975
Available at https://www.rfc-editor.org/rfc/rfc3526
39763976
3977+
.. [KKD1995] Ton Kloks, and Dieter Kratsch. "Computing a perfect edge without vertex
3978+
elimination ordering of a chordal bipartite graph."
3979+
Information Processing Letters 55.1 (1995): 11-16.
3980+
39773981
.. [KKMMNN1992] S-J. Kang, M. Kashiwara, K. C. Misra, T. Miwa, T. Nakashima,
39783982
and A. Nakayashiki. *Affine crystals and vertex models*.
39793983
Int. J. Mod. Phys. A, **7** (suppl. 1A), (1992) pp. 449-484.

src/sage/graphs/graph.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,247 @@ def is_path(self):
29202920
return False
29212921
return deg_one_counter == 2 and seen_counter == order
29222922

2923+
@doc_index("Graph properties")
2924+
def is_chordal_bipartite(self, certificate=False):
2925+
r"""
2926+
Check whether the given graph is chordal bipartite.
2927+
2928+
A graph `G` is chordal bipartite if it is bipartite and has no induced
2929+
cycle of length at least 6.
2930+
2931+
An edge `(x, y) \in E` is bisimplical if `N(x) \cup N(y)` induces a
2932+
complete bipartite subgraph of `G`.
2933+
2934+
A Perfect Edge Without Vertex Elimination Ordering of a bipartite graph
2935+
`G = (X, Y, E)` is an ordering `e_1,...,e_m` of its edge set such that
2936+
for all `1 \leq i \leq m`, `e_i` is a bisimplical edge of
2937+
`G_i = (X, Y, E_i)` where `E_i` consists of the edges `e_i,e_{i+1}...,e_m`.
2938+
2939+
A graph `G` is chordal bipartite if and only if it has a Perfect Edge
2940+
Without Vertex Elimination Ordering. See Lemma 4 in [KKD1995]_.
2941+
2942+
INPUT:
2943+
2944+
- ``certificate`` -- boolean (default: ``False``); whether to return a
2945+
certificate
2946+
2947+
OUTPUT:
2948+
2949+
When ``certificate`` is set to ``False`` (default) this method only
2950+
returns ``True`` or ``False`` answers. When ``certificate`` is set to
2951+
``True``, the method either returns:
2952+
2953+
* ``(True, pewveo)`` when the graph is chordal bipartite, where ``pewveo``
2954+
is a Perfect Edge Without Vertex Elimination Ordering of edges.
2955+
2956+
* ``(False, cycle)`` when the graph is not chordal bipartite, where
2957+
``cycle`` is an odd cycle or a chordless cycle of length at least 6.
2958+
2959+
ALGORITHM:
2960+
2961+
This algorithm is based on these facts. The first one is trivial.
2962+
2963+
* A reduced adjacnecy matrix
2964+
(:meth:`~sage.graphs.bipartite_graph.BipartiteGraph.reduced_adjacency_matrix`)
2965+
of a bipartite graph has no cycle submatrix if and only if the graph is
2966+
chordal bipartite, where cycle submatrix is 0-1 `n \times n` matrix `n \geq 3`
2967+
with exactly two 1's in each row and column and no proper submatrix satsify
2968+
this property.
2969+
2970+
* A doubly lexical ordering
2971+
(:meth:`~sage.matrix.matrix_mod2_dense.Matrix_mod2_dense.doubly_lexical_ordering`)
2972+
of a 0-1 matrix is `\Gamma`-free
2973+
(:meth:`~sage.matrix.matrix_mod2_dense.Matrix_mod2_dense.is_Gamma_free`) if and
2974+
only if the matrix has no cycle submatrix. See Theorem 5.4 in [Lub1987]_.
2975+
2976+
Hence, checking a doubly lexical ordering of a reduced adjacency matrix
2977+
of a bipartite graph is `\Gamma`-free leads to detecting the graph
2978+
is chordal bipartite. Also, this matrix contains a certificate. Hence,
2979+
if `G` is chordal bipartite, we find a Perfect Edge Without Vertex
2980+
Elimination Ordering of edges by Lemma 10 in [KKD1995]_.
2981+
Otherwise, we can find a cycle submatrix by Theorem 5.2 in [Lub1987]_.
2982+
The time complexity of this algorithm is `O(n^3)`.
2983+
2984+
EXAMPLES:
2985+
2986+
A non-bipartite graph is not chordal bipartite::
2987+
2988+
sage: g = graphs.CycleGraph(5)
2989+
sage: g.is_chordal_bipartite()
2990+
False
2991+
sage: _, cycle = g.is_chordal_bipartite(certificate=True)
2992+
sage: len(cycle) % 2 == 1
2993+
True
2994+
2995+
A 6-cycle graph is not chordal bipartite::
2996+
2997+
sage: g = graphs.CycleGraph(6)
2998+
sage: g.is_chordal_bipartite()
2999+
False
3000+
sage: _, cycle = g.is_chordal_bipartite(certificate=True)
3001+
sage: len(cycle) == 6
3002+
True
3003+
3004+
A `2 \times n` grid graph is chordal bipartite::
3005+
3006+
sage: g = graphs.Grid2dGraph(2, 6)
3007+
sage: result, pewveo = g.is_chordal_bipartite(certificate=True)
3008+
sage: result
3009+
True
3010+
3011+
Let us check the certificate given by Sage is indeed a perfect
3012+
edge without vertex elimination ordering::
3013+
3014+
sage: for e in pewveo:
3015+
....: a = g.subgraph(vertices=g.neighbors(e[0]) + g.neighbors(e[1]))
3016+
....: b = BipartiteGraph(a).complement_bipartite()
3017+
....: if b.edges():
3018+
....: raise ValueError("this should never happen")
3019+
....: g.delete_edge(e)
3020+
3021+
Let us check the certificate given by Sage is indeed a
3022+
chordless cycle of length at least 6::
3023+
3024+
sage: g = graphs.Grid2dGraph(3, 6)
3025+
sage: result, cycle = g.is_chordal_bipartite(certificate=True)
3026+
sage: result
3027+
False
3028+
sage: l = len(cycle); l >= 6
3029+
True
3030+
sage: for i in range(len(cycle)):
3031+
....: if not g.has_edge(cycle[i], cycle[(i+1)%l]):
3032+
....: raise ValueError("this should never happen")
3033+
sage: h = g.subgraph(vertices=cycle)
3034+
sage: h.is_cycle()
3035+
True
3036+
3037+
TESTS:
3038+
3039+
The algorithm works correctly for disconnected graphs::
3040+
3041+
sage: c4 = graphs.CycleGraph(4)
3042+
sage: g = c4.disjoint_union(graphs.CycleGraph(6))
3043+
sage: g.is_chordal_bipartite()
3044+
False
3045+
sage: _, cycle = g.is_chordal_bipartite(certificate=True)
3046+
sage: len(cycle) == 6
3047+
True
3048+
sage: g = c4.disjoint_union(graphs.Grid2dGraph(2, 6))
3049+
sage: g.is_chordal_bipartite()
3050+
True
3051+
sage: _, pewveo = g.is_chordal_bipartite(certificate=True)
3052+
sage: for e in pewveo:
3053+
....: a = g.subgraph(vertices=g.neighbors(e[0]) + g.neighbors(e[1]))
3054+
....: b = BipartiteGraph(a).complement_bipartite()
3055+
....: if b.edges():
3056+
....: raise ValueError("this should never happen")
3057+
....: g.delete_edge(e)
3058+
"""
3059+
self._scream_if_not_simple()
3060+
is_bipartite, bipartite_certificate = self.is_bipartite(certificate=True)
3061+
if not is_bipartite:
3062+
return False if not certificate else (False, bipartite_certificate)
3063+
3064+
# If the graph is not connected, we are computing the result on each
3065+
# component
3066+
if not self.is_connected():
3067+
# If the user wants a certificate, we had no choice but to collect
3068+
# the Perfect Edge Without Vertex Elimination Ordering. But we
3069+
# return a cycle certificate immediately if we find any.
3070+
if certificate:
3071+
pewveo = []
3072+
for gg in self.connected_components_subgraphs():
3073+
b, certif = gg.is_chordal_bipartite(certificate=True)
3074+
if not b:
3075+
return False, certif
3076+
pewveo.extend(certif)
3077+
return True, pewveo
3078+
return all(gg.is_chordal_bipartite() for gg in
3079+
self.connected_components_subgraphs())
3080+
3081+
left = [v for v, c in bipartite_certificate.items() if c == 0]
3082+
right = [v for v, c in bipartite_certificate.items() if c == 1]
3083+
order_left = len(left)
3084+
order_right = len(right)
3085+
3086+
# We set |left| > |right| for optimization, i.e. time complexity of
3087+
# doubly lexical ordering algorithm is O(nm^2) for a n x m matrix.
3088+
if order_left < order_right:
3089+
left, right = right, left
3090+
order_left, order_right = order_right, order_left
3091+
3092+
# create a reduced_adjacency_matrix
3093+
from sage.rings.finite_rings.finite_field_prime_modn import FiniteField_prime_modn
3094+
A = self.adjacency_matrix(vertices=left+right, base_ring=FiniteField_prime_modn(2))
3095+
B = A[range(order_left), range(order_left, order_left + order_right)]
3096+
3097+
# get doubly lexical ordering of reduced_adjacency_matrix
3098+
row_ordering, col_ordering = B.doubly_lexical_ordering(inplace=True)
3099+
3100+
# determine if B is Gamma-free or not
3101+
is_Gamma_free, Gamma_submatrix_indices = B.is_Gamma_free(certificate=True)
3102+
3103+
if not certificate:
3104+
return is_Gamma_free
3105+
3106+
row_ordering_dict = {k-1: v-1 for k, v in row_ordering.dict().items()}
3107+
col_ordering_dict = {k-1: v-1 for k, v in col_ordering.dict().items()}
3108+
row_vertices = [left[row_ordering_dict[i]] for i in range(order_left)]
3109+
col_vertices = [right[col_ordering_dict[i]] for i in range(order_right)]
3110+
3111+
if is_Gamma_free:
3112+
pewveo = dict()
3113+
order = 0
3114+
for i, vi in enumerate(row_vertices):
3115+
for j, vj in enumerate(col_vertices):
3116+
if B[i, j] == 1:
3117+
pewveo[vi, vj] = order
3118+
pewveo[vj, vi] = order
3119+
order += 1
3120+
edges = self.edges(sort=True, key=lambda x: pewveo[x[0], x[1]])
3121+
return True, edges
3122+
3123+
# Find a chordless cycle of length at least 6
3124+
r1, c1, r2, c2 = Gamma_submatrix_indices
3125+
row_indices = [r1, r2]
3126+
col_indices = [c1, c2]
3127+
while not B[row_indices[-1], col_indices[-1]]:
3128+
# find the rightmost column with different value
3129+
# in row_indices[-2] and row_indices[-1]
3130+
col = order_right - 1
3131+
while col > col_indices[-1]:
3132+
if not B[row_indices[-2], col] and B[row_indices[-1], col]:
3133+
break
3134+
col -= 1
3135+
assert col > col_indices[-1]
3136+
3137+
# find the bottommost row with different value
3138+
# in col_indices[-2] and col_indices[-1]
3139+
row = order_left - 1
3140+
while row > row_indices[-1]:
3141+
if not B[row, col_indices[-2]] and B[row, col_indices[-1]]:
3142+
break
3143+
row -= 1
3144+
assert row > row_indices[-1]
3145+
3146+
col_indices.append(col)
3147+
row_indices.append(row)
3148+
3149+
l = len(row_indices)
3150+
cycle = []
3151+
for i in range(l):
3152+
if i % 2 == 0:
3153+
cycle.append(row_vertices[row_indices[i]])
3154+
else:
3155+
cycle.append(col_vertices[col_indices[i]])
3156+
for i in reversed(range(l)):
3157+
if i % 2 == 0:
3158+
cycle.append(col_vertices[col_indices[i]])
3159+
else:
3160+
cycle.append(row_vertices[row_indices[i]])
3161+
3162+
return False, cycle
3163+
29233164
@doc_index("Connectivity, orientations, trees")
29243165
def degree_constrained_subgraph(self, bounds, solver=None, verbose=0,
29253166
*, integrality_tolerance=1e-3):

src/sage/matrix/matrix_mod2_dense.pyx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,6 +2307,96 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse
23072307

23082308
return row_ordering, col_ordering
23092309

2310+
def is_Gamma_free(self, certificate=False):
2311+
r"""
2312+
Return True if the matrix is `\Gamma`-free.
2313+
2314+
A matrix is `\Gamma`-free if it does not contain a 2x2 submatrix
2315+
of the form:
2316+
2317+
.. MATH::
2318+
2319+
\begin{pmatrix}
2320+
1 & 1 \\
2321+
1 & 0
2322+
\end{pmatrix}
2323+
2324+
INPUT:
2325+
2326+
- ``certificate`` -- boolean (default: ``False``); whether to return a
2327+
certificate for no-answers (see OUTPUT section)
2328+
2329+
OUTPUT:
2330+
2331+
When ``certificate`` is set to ``False`` (default) this method only
2332+
returns ``True`` or ``False`` answers. When ``certificate`` is set to
2333+
``True``, the method either returns ``(True, None)`` or ``(False,
2334+
(r1, c1, r2, c2))`` where ``r1``, ``r2``-th rows and ``c1``,
2335+
``c2``-th columns of the matrix constitute the `\Gamma`-submatrix.
2336+
2337+
ALGORITHM:
2338+
2339+
For each 1 entry, the algorithm finds the next 1 in the same row and
2340+
the next 1 in the same column, and check the 2x2 submatrix that contains
2341+
these entries forms `\Gamma` submatrix. The time complexity of
2342+
this algorithm is `O(n \cdot m)` for a `n \times m` matrix.
2343+
2344+
EXAMPLES::
2345+
2346+
sage: A = Matrix(GF(2), [[1, 1],
2347+
....: [0, 0]])
2348+
sage: A.is_Gamma_free()
2349+
True
2350+
sage: B = Matrix(GF(2), [[1, 1],
2351+
....: [1, 0]])
2352+
sage: B.is_Gamma_free(certificate=True)
2353+
(False, (0, 0, 1, 1))
2354+
2355+
TESTS:
2356+
2357+
The algorithm works collectly for larger matrices::
2358+
2359+
sage: A = Matrix(GF(2), [[1, 0, 1],
2360+
....: [0, 0, 0],
2361+
....: [1, 0, 0]])
2362+
sage: A.is_Gamma_free(certificate=True)
2363+
(False, (0, 0, 2, 2))
2364+
sage: B = Matrix(GF(2), [[1, 0, 1],
2365+
....: [0, 0, 0],
2366+
....: [1, 0, 1]])
2367+
sage: B.is_Gamma_free(certificate=True)
2368+
(True, None)
2369+
"""
2370+
cdef int i, j, i_bottom, j_right
2371+
2372+
for i in range(self._nrows):
2373+
j = 0
2374+
while j < self._ncols:
2375+
if mzd_read_bit(self._entries, i, j): # if A[i][j] == 1
2376+
# find the next 1 in the row
2377+
j_right = j + 1
2378+
while j_right < self._ncols and not mzd_read_bit(self._entries, i, j_right):
2379+
j_right += 1
2380+
if j_right < self._ncols:
2381+
# find the next 1 in the column
2382+
i_bottom = i + 1
2383+
while i_bottom < self._nrows and not mzd_read_bit(self._entries, i_bottom, j):
2384+
i_bottom += 1
2385+
if i_bottom < self._nrows and not mzd_read_bit(self._entries, i_bottom, j_right):
2386+
# A[i_bottom][j_right] == 0
2387+
if certificate:
2388+
return False, (i, j, i_bottom, j_right)
2389+
else:
2390+
return False
2391+
j = j_right
2392+
else:
2393+
j += 1
2394+
2395+
if certificate:
2396+
return True, None
2397+
else:
2398+
return True
2399+
23102400
# Used for hashing
23112401
cdef int i, k
23122402
cdef unsigned long parity_table[256]

0 commit comments

Comments
 (0)