Skip to content

Commit

Permalink
sagemathgh-37345: implemented function for acyclic orientations
Browse files Browse the repository at this point in the history
    
<!-- ^^^^^
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 sagemath#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 sagemath#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
- sagemath#12345: short description why this is a dependency
- sagemath#34567: ...
-->

<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
    
URL: sagemath#37345
Reported by: saatvikraoIITGN
Reviewer(s): David Coudert, grhkm21, saatvikraoIITGN
  • Loading branch information
Release Manager committed Mar 21, 2024
2 parents 9991759 + bef9946 commit 8daac35
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/doc/en/reference/references/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5830,6 +5830,11 @@ REFERENCES:
Proceedings of the American Mathematical Society,
Volume 90. Number 2, February 1984, pp. 199-202.
.. [Sq1998] Matthew B. Squire.
*Generating the acyclic orientations of a graph*.
Journal of Algorithms, Volume 26, Issue 2, Pages 275 - 290, 1998.
(https://doi.org/10.1006/jagm.1997.0891)
.. [SS1983] Shorey and Stewart. "On the Diophantine equation a
x^{2t} + b x^t y + c y^2 = d and pure powers in recurrence
sequences." Mathematica Scandinavica, 1983.
Expand Down
3 changes: 2 additions & 1 deletion src/sage/graphs/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10134,7 +10134,7 @@ def bipartite_double(self, extended=False):
from sage.graphs.tutte_polynomial import tutte_polynomial
from sage.graphs.lovasz_theta import lovasz_theta
from sage.graphs.partial_cube import is_partial_cube
from sage.graphs.orientations import strong_orientations_iterator, random_orientation
from sage.graphs.orientations import strong_orientations_iterator, random_orientation, acyclic_orientations
from sage.graphs.connectivity import bridges, cleave, spqr_tree
from sage.graphs.connectivity import is_triconnected
from sage.graphs.comparability import is_comparability
Expand Down Expand Up @@ -10180,6 +10180,7 @@ def bipartite_double(self, extended=False):
"lovasz_theta" : "Leftovers",
"strong_orientations_iterator" : "Connectivity, orientations, trees",
"random_orientation" : "Connectivity, orientations, trees",
"acyclic_orientations" : "Connectivity, orientations, trees",
"bridges" : "Connectivity, orientations, trees",
"cleave" : "Connectivity, orientations, trees",
"spqr_tree" : "Connectivity, orientations, trees",
Expand Down
257 changes: 257 additions & 0 deletions src/sage/graphs/orientations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,263 @@
from sage.graphs.digraph import DiGraph


def acyclic_orientations(G):
r"""
Return an iterator over all acyclic orientations of an undirected graph `G`.
ALGORITHM:
The algorithm is based on [Sq1998]_.
It presents an efficient algorithm for listing the acyclic orientations of a
graph. The algorithm is shown to require O(n) time per acyclic orientation
generated, making it the most efficient known algorithm for generating acyclic
orientations.
The function uses a recursive approach to generate acyclic orientations of the
graph. It reorders the vertices and edges of the graph, creating a new graph
with updated labels. Then, it iteratively generates acyclic orientations by
considering subsets of edges and checking whether they form upsets in a
corresponding poset.
INPUT:
- ``G`` -- an undirected graph.
OUTPUT:
- An iterator over all acyclic orientations of the input graph.
.. NOTE::
The function assumes that the input graph is undirected and the edges are unlabelled.
EXAMPLES:
To count number acyclic orientations for a graph::
sage: g = Graph([(0, 3), (0, 4), (3, 4), (1, 3), (1, 2), (2, 3), (2, 4)])
sage: it = g.acyclic_orientations()
sage: len(list(it))
54
Test for arbitary vertex labels::
sage: g_str = Graph([('abc', 'def'), ('ghi', 'def'), ('xyz', 'abc'), ('xyz', 'uvw'), ('uvw', 'abc'), ('uvw', 'ghi')])
sage: it = g_str.acyclic_orientations()
sage: len(list(it))
42
TESTS:
To count the number of acyclic orientations for a graph with 0 vertices::
sage: list(Graph().acyclic_orientations())
[]
To count the number of acyclic orientations for a graph with 1 vertex::
sage: list(Graph(1).acyclic_orientations())
[]
To count the number of acyclic orientations for a graph with 2 vertices::
sage: list(Graph(2).acyclic_orientations())
[]
Acyclic orientations of a complete graph::
sage: g = graphs.CompleteGraph(5)
sage: it = g.acyclic_orientations()
sage: len(list(it))
120
Graph with one edge::
sage: list(Graph([(0, 1)]).acyclic_orientations())
[Digraph on 2 vertices, Digraph on 2 vertices]
Graph with two edges::
sage: len(list(Graph([(0, 1), (1, 2)]).acyclic_orientations()))
4
Cycle graph::
sage: len(list(Graph([(0, 1), (1, 2), (2, 0)]).acyclic_orientations()))
6
"""
if not G.size():
# A graph without edge cannot be oriented
return

from sage.rings.infinity import Infinity
from sage.combinat.subset import Subsets

def reorder_vertices(G):
n = G.order()
ko = n
k = n
G_copy = G.copy()
vertex_labels = {v: None for v in G_copy.vertices()}

while G_copy.size() > 0:
min_val = float('inf')
uv = None
for u, v, _ in G_copy.edges():
du = G_copy.degree(u)
dv = G_copy.degree(v)
val = (du + dv) / (du * dv)
if val < min_val:
min_val = val
uv = (u, v)

if uv:
u, v = uv
vertex_labels[u] = ko
vertex_labels[v] = ko - 1
G_copy.delete_vertex(u)
G_copy.delete_vertex(v)
ko -= 2

if G_copy.size() == 0:
break

for vertex, label in vertex_labels.items():
if label is None:
vertex_labels[vertex] = ko
ko -= 1

return vertex_labels

def order_edges(G, vertex_labels):
n = len(vertex_labels)
m = 1
edge_labels = {}

for j in range(2, n + 1):
for i in range(1, j):
if G.has_edge(i, j):
edge_labels[(i, j)] = m
m += 1

return edge_labels

def is_upset_of_poset(Poset, subset, keys):
for (u, v) in subset:
for (w, x) in keys:
if (Poset[(u, v), (w, x)] == 1 and (w, x) not in subset):
return False
return True

def generate_orientations(globO, starting_of_Ek, m, k, keys):
# Creating a poset
Poset = {}
for i in range(starting_of_Ek, m - 1):
for j in range(starting_of_Ek, m - 1):
u, v = keys[i]
w, x = keys[j]
Poset[(u, v), (w, x)] = 0

# Create a new graph to determine reachable vertices
new_G = DiGraph()

# Process vertices up to starting_of_Ek
new_G.add_edges([(v, u) if globO[(u, v)] == 1 else (u, v) for u, v in keys[:starting_of_Ek]])

# Process vertices starting from starting_of_Ek
new_G.add_vertices([u for u, _ in keys[starting_of_Ek:]] + [v for _, v in keys[starting_of_Ek:]])

if (globO[(k-1, k)] == 1):
new_G.add_edge(k, k - 1)
else:
new_G.add_edge(k-1, k)

for i in range(starting_of_Ek, m - 1):
for j in range(starting_of_Ek, m - 1):
u, v = keys[i]
w, x = keys[j]
# w should be reachable from u and v should be reachable from x
if w in new_G.depth_first_search(u) and v in new_G.depth_first_search(x):
Poset[(u, v), (w, x)] = 1

# For each subset of the base set of E_k, check if it is an upset or not
upsets = []
for subset in Subsets(keys[starting_of_Ek:m-1]):
if (is_upset_of_poset(Poset, subset, keys[starting_of_Ek:m-1])):
upsets.append(list(subset))

for upset in upsets:
for i in range(starting_of_Ek, m - 1):
u, v = keys[i]
if (u, v) in upset:
globO[(u, v)] = 1
else:
globO[(u, v)] = 0

yield globO.copy()

def helper(G, globO, m, k):
keys = list(globO.keys())
keys = keys[0:m]

if m <= 0:
yield {}
return

starting_of_Ek = 0
for (u, v) in keys:
if u >= k - 1 or v >= k - 1:
break
else:
starting_of_Ek += 1

# s is the size of E_k
s = m - 1 - starting_of_Ek

# Recursively generate acyclic orientations
orientations_G_small = helper(G, globO, starting_of_Ek, k - 2)

# For each orientation of G_k-2, yield acyclic orientations
for alpha in orientations_G_small:
for (u, v) in alpha:
globO[(u, v)] = alpha[(u, v)]

# Orienting H_k as 1
globO[(k-1, k)] = 1
yield from generate_orientations(globO, starting_of_Ek, m, k, keys)

# Orienting H_k as 0
globO[(k-1, k)] = 0
yield from generate_orientations(globO, starting_of_Ek, m, k, keys)

# Reorder vertices based on the logic in reorder_vertices function
vertex_labels = reorder_vertices(G)

# Create a new graph with updated vertex labels using SageMath, Assuming the graph edges are unlabelled
new_G = G.relabel(perm=vertex_labels, inplace=False)

G = new_G

# Order the edges based on the logic in order_edges function
edge_labels = order_edges(G, vertex_labels)

# Create globO array
globO = {uv: 0 for uv in edge_labels}

m = len(edge_labels)
k = len(vertex_labels)
orientations = helper(G, globO, m, k)

# Create a mapping between original and new vertex labels
reverse_vertex_labels = {label: vertex for vertex, label in vertex_labels.items()}

# Iterate over acyclic orientations and create relabeled graphs
for orientation in orientations:
relabeled_graph = DiGraph([(reverse_vertex_labels[u], reverse_vertex_labels[v], label) for (u, v), label in orientation.items()])
yield relabeled_graph


def strong_orientations_iterator(G):
r"""
Return an iterator over all strong orientations of a graph `G`.
Expand Down

0 comments on commit 8daac35

Please sign in to comment.