Skip to content

Commit 8daac35

Browse files
author
Release Manager
committed
gh-37345: implemented function for acyclic orientations <!-- ^^^^^ Please provide a concise, informative and self-explanatory title. Don't put issue numbers in there, do this in the PR body below. For example, instead of "Fixes #1234" use "Introduce new method to calculate 1+1" --> **Description** This PR introduces an efficient algorithm for generating all acyclic orientations of an undirected graph based on the work by Mathew B. Squire. The algorithm utilizes a recursive approach along with concepts from posets and subsets to improve the efficiency of acyclic orientation generation significantly. **Changes Made** 1. Implemented the reorder_vertices function to efficiently reorder vertices based on a specific criterion, improving subsequent acyclic orientation generation. 2. Created the order_edges function to assign labels to edges based on the reordered vertices, simplifying the acyclic orientation process. 3. Introduced the generate_orientations function to efficiently generate acyclic orientations by considering subsets of edges and checking for upsets in a corresponding poset. 4. Implemented the recursive helper function to generate acyclic orientations for smaller graphs, building up to larger graphs. 5. Modified the main function to use SageMath for graph manipulation and incorporated the new algorithm. Fixes #37253 ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> <!-- If your change requires a documentation PR, please link it appropriately --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!-- Feel free to remove irrelevant items. --> - [x] The title is concise, informative, and self-explanatory. - [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. - [ ] I have updated the documentation accordingly. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on - #12345: short description why this is a dependency - #34567: ... --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> URL: #37345 Reported by: saatvikraoIITGN Reviewer(s): David Coudert, grhkm21, saatvikraoIITGN
2 parents 9991759 + bef9946 commit 8daac35

File tree

3 files changed

+264
-1
lines changed

3 files changed

+264
-1
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5830,6 +5830,11 @@ REFERENCES:
58305830
Proceedings of the American Mathematical Society,
58315831
Volume 90. Number 2, February 1984, pp. 199-202.
58325832
5833+
.. [Sq1998] Matthew B. Squire.
5834+
*Generating the acyclic orientations of a graph*.
5835+
Journal of Algorithms, Volume 26, Issue 2, Pages 275 - 290, 1998.
5836+
(https://doi.org/10.1006/jagm.1997.0891)
5837+
58335838
.. [SS1983] Shorey and Stewart. "On the Diophantine equation a
58345839
x^{2t} + b x^t y + c y^2 = d and pure powers in recurrence
58355840
sequences." Mathematica Scandinavica, 1983.

src/sage/graphs/graph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10134,7 +10134,7 @@ def bipartite_double(self, extended=False):
1013410134
from sage.graphs.tutte_polynomial import tutte_polynomial
1013510135
from sage.graphs.lovasz_theta import lovasz_theta
1013610136
from sage.graphs.partial_cube import is_partial_cube
10137-
from sage.graphs.orientations import strong_orientations_iterator, random_orientation
10137+
from sage.graphs.orientations import strong_orientations_iterator, random_orientation, acyclic_orientations
1013810138
from sage.graphs.connectivity import bridges, cleave, spqr_tree
1013910139
from sage.graphs.connectivity import is_triconnected
1014010140
from sage.graphs.comparability import is_comparability
@@ -10180,6 +10180,7 @@ def bipartite_double(self, extended=False):
1018010180
"lovasz_theta" : "Leftovers",
1018110181
"strong_orientations_iterator" : "Connectivity, orientations, trees",
1018210182
"random_orientation" : "Connectivity, orientations, trees",
10183+
"acyclic_orientations" : "Connectivity, orientations, trees",
1018310184
"bridges" : "Connectivity, orientations, trees",
1018410185
"cleave" : "Connectivity, orientations, trees",
1018510186
"spqr_tree" : "Connectivity, orientations, trees",

src/sage/graphs/orientations.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,263 @@
4141
from sage.graphs.digraph import DiGraph
4242

4343

44+
def acyclic_orientations(G):
45+
r"""
46+
Return an iterator over all acyclic orientations of an undirected graph `G`.
47+
48+
ALGORITHM:
49+
50+
The algorithm is based on [Sq1998]_.
51+
It presents an efficient algorithm for listing the acyclic orientations of a
52+
graph. The algorithm is shown to require O(n) time per acyclic orientation
53+
generated, making it the most efficient known algorithm for generating acyclic
54+
orientations.
55+
56+
The function uses a recursive approach to generate acyclic orientations of the
57+
graph. It reorders the vertices and edges of the graph, creating a new graph
58+
with updated labels. Then, it iteratively generates acyclic orientations by
59+
considering subsets of edges and checking whether they form upsets in a
60+
corresponding poset.
61+
62+
INPUT:
63+
64+
- ``G`` -- an undirected graph.
65+
66+
OUTPUT:
67+
68+
- An iterator over all acyclic orientations of the input graph.
69+
70+
.. NOTE::
71+
72+
The function assumes that the input graph is undirected and the edges are unlabelled.
73+
74+
EXAMPLES:
75+
76+
To count number acyclic orientations for a graph::
77+
78+
sage: g = Graph([(0, 3), (0, 4), (3, 4), (1, 3), (1, 2), (2, 3), (2, 4)])
79+
sage: it = g.acyclic_orientations()
80+
sage: len(list(it))
81+
54
82+
83+
Test for arbitary vertex labels::
84+
85+
sage: g_str = Graph([('abc', 'def'), ('ghi', 'def'), ('xyz', 'abc'), ('xyz', 'uvw'), ('uvw', 'abc'), ('uvw', 'ghi')])
86+
sage: it = g_str.acyclic_orientations()
87+
sage: len(list(it))
88+
42
89+
90+
TESTS:
91+
92+
To count the number of acyclic orientations for a graph with 0 vertices::
93+
94+
sage: list(Graph().acyclic_orientations())
95+
[]
96+
97+
To count the number of acyclic orientations for a graph with 1 vertex::
98+
99+
sage: list(Graph(1).acyclic_orientations())
100+
[]
101+
102+
To count the number of acyclic orientations for a graph with 2 vertices::
103+
104+
sage: list(Graph(2).acyclic_orientations())
105+
[]
106+
107+
Acyclic orientations of a complete graph::
108+
109+
sage: g = graphs.CompleteGraph(5)
110+
sage: it = g.acyclic_orientations()
111+
sage: len(list(it))
112+
120
113+
114+
Graph with one edge::
115+
116+
sage: list(Graph([(0, 1)]).acyclic_orientations())
117+
[Digraph on 2 vertices, Digraph on 2 vertices]
118+
119+
Graph with two edges::
120+
121+
sage: len(list(Graph([(0, 1), (1, 2)]).acyclic_orientations()))
122+
4
123+
124+
Cycle graph::
125+
126+
sage: len(list(Graph([(0, 1), (1, 2), (2, 0)]).acyclic_orientations()))
127+
6
128+
129+
"""
130+
if not G.size():
131+
# A graph without edge cannot be oriented
132+
return
133+
134+
from sage.rings.infinity import Infinity
135+
from sage.combinat.subset import Subsets
136+
137+
def reorder_vertices(G):
138+
n = G.order()
139+
ko = n
140+
k = n
141+
G_copy = G.copy()
142+
vertex_labels = {v: None for v in G_copy.vertices()}
143+
144+
while G_copy.size() > 0:
145+
min_val = float('inf')
146+
uv = None
147+
for u, v, _ in G_copy.edges():
148+
du = G_copy.degree(u)
149+
dv = G_copy.degree(v)
150+
val = (du + dv) / (du * dv)
151+
if val < min_val:
152+
min_val = val
153+
uv = (u, v)
154+
155+
if uv:
156+
u, v = uv
157+
vertex_labels[u] = ko
158+
vertex_labels[v] = ko - 1
159+
G_copy.delete_vertex(u)
160+
G_copy.delete_vertex(v)
161+
ko -= 2
162+
163+
if G_copy.size() == 0:
164+
break
165+
166+
for vertex, label in vertex_labels.items():
167+
if label is None:
168+
vertex_labels[vertex] = ko
169+
ko -= 1
170+
171+
return vertex_labels
172+
173+
def order_edges(G, vertex_labels):
174+
n = len(vertex_labels)
175+
m = 1
176+
edge_labels = {}
177+
178+
for j in range(2, n + 1):
179+
for i in range(1, j):
180+
if G.has_edge(i, j):
181+
edge_labels[(i, j)] = m
182+
m += 1
183+
184+
return edge_labels
185+
186+
def is_upset_of_poset(Poset, subset, keys):
187+
for (u, v) in subset:
188+
for (w, x) in keys:
189+
if (Poset[(u, v), (w, x)] == 1 and (w, x) not in subset):
190+
return False
191+
return True
192+
193+
def generate_orientations(globO, starting_of_Ek, m, k, keys):
194+
# Creating a poset
195+
Poset = {}
196+
for i in range(starting_of_Ek, m - 1):
197+
for j in range(starting_of_Ek, m - 1):
198+
u, v = keys[i]
199+
w, x = keys[j]
200+
Poset[(u, v), (w, x)] = 0
201+
202+
# Create a new graph to determine reachable vertices
203+
new_G = DiGraph()
204+
205+
# Process vertices up to starting_of_Ek
206+
new_G.add_edges([(v, u) if globO[(u, v)] == 1 else (u, v) for u, v in keys[:starting_of_Ek]])
207+
208+
# Process vertices starting from starting_of_Ek
209+
new_G.add_vertices([u for u, _ in keys[starting_of_Ek:]] + [v for _, v in keys[starting_of_Ek:]])
210+
211+
if (globO[(k-1, k)] == 1):
212+
new_G.add_edge(k, k - 1)
213+
else:
214+
new_G.add_edge(k-1, k)
215+
216+
for i in range(starting_of_Ek, m - 1):
217+
for j in range(starting_of_Ek, m - 1):
218+
u, v = keys[i]
219+
w, x = keys[j]
220+
# w should be reachable from u and v should be reachable from x
221+
if w in new_G.depth_first_search(u) and v in new_G.depth_first_search(x):
222+
Poset[(u, v), (w, x)] = 1
223+
224+
# For each subset of the base set of E_k, check if it is an upset or not
225+
upsets = []
226+
for subset in Subsets(keys[starting_of_Ek:m-1]):
227+
if (is_upset_of_poset(Poset, subset, keys[starting_of_Ek:m-1])):
228+
upsets.append(list(subset))
229+
230+
for upset in upsets:
231+
for i in range(starting_of_Ek, m - 1):
232+
u, v = keys[i]
233+
if (u, v) in upset:
234+
globO[(u, v)] = 1
235+
else:
236+
globO[(u, v)] = 0
237+
238+
yield globO.copy()
239+
240+
def helper(G, globO, m, k):
241+
keys = list(globO.keys())
242+
keys = keys[0:m]
243+
244+
if m <= 0:
245+
yield {}
246+
return
247+
248+
starting_of_Ek = 0
249+
for (u, v) in keys:
250+
if u >= k - 1 or v >= k - 1:
251+
break
252+
else:
253+
starting_of_Ek += 1
254+
255+
# s is the size of E_k
256+
s = m - 1 - starting_of_Ek
257+
258+
# Recursively generate acyclic orientations
259+
orientations_G_small = helper(G, globO, starting_of_Ek, k - 2)
260+
261+
# For each orientation of G_k-2, yield acyclic orientations
262+
for alpha in orientations_G_small:
263+
for (u, v) in alpha:
264+
globO[(u, v)] = alpha[(u, v)]
265+
266+
# Orienting H_k as 1
267+
globO[(k-1, k)] = 1
268+
yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
269+
270+
# Orienting H_k as 0
271+
globO[(k-1, k)] = 0
272+
yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
273+
274+
# Reorder vertices based on the logic in reorder_vertices function
275+
vertex_labels = reorder_vertices(G)
276+
277+
# Create a new graph with updated vertex labels using SageMath, Assuming the graph edges are unlabelled
278+
new_G = G.relabel(perm=vertex_labels, inplace=False)
279+
280+
G = new_G
281+
282+
# Order the edges based on the logic in order_edges function
283+
edge_labels = order_edges(G, vertex_labels)
284+
285+
# Create globO array
286+
globO = {uv: 0 for uv in edge_labels}
287+
288+
m = len(edge_labels)
289+
k = len(vertex_labels)
290+
orientations = helper(G, globO, m, k)
291+
292+
# Create a mapping between original and new vertex labels
293+
reverse_vertex_labels = {label: vertex for vertex, label in vertex_labels.items()}
294+
295+
# Iterate over acyclic orientations and create relabeled graphs
296+
for orientation in orientations:
297+
relabeled_graph = DiGraph([(reverse_vertex_labels[u], reverse_vertex_labels[v], label) for (u, v), label in orientation.items()])
298+
yield relabeled_graph
299+
300+
44301
def strong_orientations_iterator(G):
45302
r"""
46303
Return an iterator over all strong orientations of a graph `G`.

0 commit comments

Comments
 (0)