Skip to content

Commit

Permalink
gh-36232: Make min_spanning_tree robust to incomparable vertex labels
Browse files Browse the repository at this point in the history
    
Part of #35902.

### 📚 Description

We ensure that method `min_spanning_tree` operates properly even when
vertices and edges are of incomparable types.
We are then able to simplify some code in
`src/sage/topology/simplicial_complex.py`.

### 📝 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.
- [x] 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: #36232
Reported by: David Coudert
Reviewer(s): Frédéric Chapoton
  • Loading branch information
Release Manager committed Sep 15, 2023
2 parents 8d09d15 + 5efb88e commit 683b05b
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 74 deletions.
15 changes: 12 additions & 3 deletions src/sage/graphs/base/boost_graph.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,16 @@ cpdef min_spanning_tree(g,
Traceback (most recent call last):
...
TypeError: float() argument must be a string or a... number...
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)], weighted=True)
sage: E = min_spanning_tree(G, algorithm='Kruskal')
sage: sum(w for _, _, w in E)
3
sage: F = min_spanning_tree(G, algorithm='Prim')
sage: sum(w for _, _, w in F)
3
"""
from sage.graphs.graph import Graph

Expand Down Expand Up @@ -719,9 +729,8 @@ cpdef min_spanning_tree(g,

if <v_index> result.size() != 2 * (n - 1):
return []
else:
edges = [(int_to_vertex[<int> result[2*i]], int_to_vertex[<int> result[2*i + 1]]) for i in range(n - 1)]
return [(min(e[0], e[1]), max(e[0], e[1]), g.edge_label(e[0], e[1])) for e in edges]
edges = [(int_to_vertex[<int> result[2*i]], int_to_vertex[<int> result[2*i + 1]]) for i in range(n - 1)]
return [(u, v, g.edge_label(u, v)) for u, v in edges]


cpdef blocks_and_cut_vertices(g):
Expand Down
116 changes: 71 additions & 45 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4704,17 +4704,23 @@ def min_spanning_tree(self,
sage: len(g.min_spanning_tree())
4
sage: weight = lambda e: 1 / ((e[0] + 1) * (e[1] + 1))
sage: sorted(g.min_spanning_tree(weight_function=weight))
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
sage: sorted(g.min_spanning_tree(weight_function=weight,
....: algorithm='Kruskal_Boost'))
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
sage: E = g.min_spanning_tree(weight_function=weight)
sage: T = Graph(E)
sage: set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree()
True
sage: sum(map(weight, E))
5/12
sage: E = g.min_spanning_tree(weight_function=weight,
....: algorithm='Kruskal_Boost')
sage: Graph(E).is_tree(); sum(map(weight, E))
True
5/12
sage: g = graphs.PetersenGraph()
sage: g.allow_multiple_edges(True)
sage: g.add_edges(g.edge_iterator())
sage: sorted(g.min_spanning_tree())
[(0, 1, None), (0, 4, None), (0, 5, None), (1, 2, None), (1, 6, None),
(3, 8, None), (5, 7, None), (5, 8, None), (6, 9, None)]
sage: T = Graph(g.min_spanning_tree())
sage: set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree()
True

Boruvka's algorithm::

Expand All @@ -4725,15 +4731,13 @@ def min_spanning_tree(self,
Prim's algorithm::

sage: g = graphs.CompleteGraph(5)
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge',
....: starting_vertex=2, weight_function=weight))
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe',
....: starting_vertex=2, weight_function=weight))
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
sage: sorted(g.min_spanning_tree(weight_function=weight,
....: algorithm='Prim_Boost'))
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
sage: for algo in ['Prim_edge', 'Prim_fringe', 'Prim_Boost']:
....: E = g.min_spanning_tree(algorithm=algo, weight_function=weight)
....: T = Graph(E)
....: print(set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree())
True
True
True

NetworkX algorithm::

Expand All @@ -4745,82 +4749,93 @@ def min_spanning_tree(self,
sage: G = Graph([(0, 1, {'name': 'a', 'weight': 1}),
....: (0, 2, {'name': 'b', 'weight': 3}),
....: (1, 2, {'name': 'b', 'weight': 1})])
sage: sorted(G.min_spanning_tree(weight_function=lambda e: e[2]['weight']))
sage: sorted(G.min_spanning_tree(algorithm='Boruvka',
....: weight_function=lambda e: e[2]['weight']))
[(0, 1, {'name': 'a', 'weight': 1}), (1, 2, {'name': 'b', 'weight': 1})]

If the graph is not weighted, edge labels are not considered, even if
they are numbers::

sage: g = Graph([(1, 2, 1), (1, 3, 2), (2, 3, 1)])
sage: sorted(g.min_spanning_tree())
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
[(1, 2, 1), (1, 3, 2)]

In order to use weights, we need either to set variable ``weighted`` to
``True``, or to specify a weight function or set by_weight to ``True``::

sage: g.weighted(True)
sage: sorted(g.min_spanning_tree())
sage: Graph(g.min_spanning_tree()).edges(sort=True)
[(1, 2, 1), (2, 3, 1)]
sage: g.weighted(False)
sage: sorted(g.min_spanning_tree())
sage: Graph(g.min_spanning_tree()).edges(sort=True)
[(1, 2, 1), (1, 3, 2)]
sage: sorted(g.min_spanning_tree(by_weight=True))
sage: Graph(g.min_spanning_tree(by_weight=True)).edges(sort=True)
[(1, 2, 1), (2, 3, 1)]
sage: Graph(g.min_spanning_tree(weight_function=lambda e: e[2])).edges(sort=True)
[(1, 2, 1), (2, 3, 1)]
sage: sorted(g.min_spanning_tree(weight_function=lambda e: e[2]))

Note that the order of the vertices on each edge is not guaranteed and
may differ from an algorithm to the other::

sage: g.weighted(True)
sage: sorted(g.min_spanning_tree())
[(2, 1, 1), (3, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
[(1, 2, 1), (2, 3, 1)]
sage: Graph(g.min_spanning_tree()).edges(sort=True)
[(1, 2, 1), (2, 3, 1)]


TESTS:

Check that, if ``weight_function`` is not provided, then edge weights
are used::

sage: g = Graph(weighted=True)
sage: g.add_edges([[0, 1, 1], [1, 2, 1], [2, 0, 10]])
sage: sorted(g.min_spanning_tree())
sage: Graph(g.min_spanning_tree()).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Filter_Kruskal'))
sage: Graph(g.min_spanning_tree(algorithm='Filter_Kruskal')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Kruskal_Boost'))
sage: Graph(g.min_spanning_tree(algorithm='Kruskal_Boost')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe'))
sage: Graph(g.min_spanning_tree(algorithm='Prim_fringe')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge'))
sage: Graph(g.min_spanning_tree(algorithm='Prim_edge')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_Boost'))
sage: Graph(g.min_spanning_tree(algorithm='Prim_Boost')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='NetworkX')) # needs networkx
sage: Graph(g.min_spanning_tree(algorithm='Boruvka')).edges(sort=True)
[(0, 1, 1), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
sage: Graph(g.min_spanning_tree(algorithm='NetworkX')).edges(sort=True) # needs networkx
[(0, 1, 1), (1, 2, 1)]

Check that, if ``weight_function`` is provided, it overrides edge
weights::

sage: g = Graph([[0, 1, 1], [1, 2, 1], [2, 0, 10]], weighted=True)
sage: weight = lambda e: 3 - e[0] - e[1]
sage: sorted(g.min_spanning_tree(weight_function=weight))
sage: Graph(g.min_spanning_tree(weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Filter_Kruskal', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='Filter_Kruskal', weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Kruskal_Boost', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='Kruskal_Boost', weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='Prim_fringe', weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='Prim_edge', weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Prim_Boost', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='Prim_Boost', weight_function=weight)).edges(sort=True)
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='NetworkX', weight_function=weight)) # needs networkx
[(0, 2, 10), (1, 2, 1)]
sage: sorted(g.min_spanning_tree(algorithm='Boruvka', weight_function=weight))
sage: Graph(g.min_spanning_tree(algorithm='NetworkX', weight_function=weight)).edges(sort=True) # needs networkx
[(0, 2, 10), (1, 2, 1)]

If the graph is directed, it is transformed into an undirected graph::

sage: g = digraphs.Circuit(3)
sage: sorted(g.min_spanning_tree(weight_function=weight))
sage: Graph(g.min_spanning_tree(weight_function=weight)).edges(sort=True)
[(0, 2, None), (1, 2, None)]
sage: sorted(g.to_undirected().min_spanning_tree(weight_function=weight))
sage: Graph(g.to_undirected().min_spanning_tree(weight_function=weight)).edges(sort=True)
[(0, 2, None), (1, 2, None)]

If at least an edge weight is not convertible to a float, an error is
Expand All @@ -4841,6 +4856,17 @@ def min_spanning_tree(self,

sage: graphs.EmptyGraph().min_spanning_tree()
[]

Check that the method is robust to incomparable vertices::

sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = G.min_spanning_tree(algorithm='Prim_Boost', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='Prim_fringe', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='Prim_edge', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='Kruskal_Boost', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='Filter_Kruskal', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='Boruvka', by_weight=True)
sage: E = G.min_spanning_tree(algorithm='NetworkX', by_weight=True) # needs networkx
"""
if not self.order():
return []
Expand Down Expand Up @@ -5136,9 +5162,9 @@ def cycle_basis(self, output='vertex'):
sage: [sorted(c) for c in G.cycle_basis()] # needs networkx
[['Hey', 'Really ?', 'Wuuhuu'], [0, 2], [0, 1, 2]]
sage: [sorted(c) for c in G.cycle_basis(output='edge')] # needs networkx
[[('Hey', 'Wuuhuu', None),
('Really ?', 'Hey', None),
('Wuuhuu', 'Really ?', None)],
[[('Hey', 'Really ?', None),
('Really ?', 'Wuuhuu', None),
('Wuuhuu', 'Hey', None)],
[(0, 2, 'a'), (2, 0, 'b')],
[(0, 2, 'b'), (1, 0, 'c'), (2, 1, 'd')]]

Expand Down
66 changes: 66 additions & 0 deletions src/sage/graphs/spanning_tree.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ def kruskal(G, by_weight=True, weight_function=None, check_weight=False, check=F
Traceback (most recent call last):
...
ValueError: the input graph must be undirected
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = kruskal(G, by_weight=True)
sage: sum(w for _, _, w in E)
3
"""
return list(kruskal_iterator(G, by_weight=by_weight, weight_function=weight_function,
check_weight=check_weight, check=check))
Expand Down Expand Up @@ -313,6 +320,13 @@ def kruskal_iterator(G, by_weight=True, weight_function=None, check_weight=False
Traceback (most recent call last):
...
ValueError: the input graph must be undirected
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = list(kruskal_iterator(G, by_weight=True))
sage: sum(w for _, _, w in E)
3
"""
from sage.graphs.graph import Graph
if not isinstance(G, Graph):
Expand Down Expand Up @@ -382,6 +396,14 @@ def kruskal_iterator_from_edges(edges, union_find, by_weight=True,
sage: union_set = DisjointSet(G)
sage: next(kruskal_iterator_from_edges(G.edges(sort=False), union_set, by_weight=G.weighted()))
(1, 6, 10)
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: union_set = DisjointSet(G)
sage: E = list(kruskal_iterator_from_edges(G.edges(sort=False), union_set, by_weight=True))
sage: sum(w for _, _, w in E)
3
"""
# We sort edges, as specified.
if weight_function is not None:
Expand Down Expand Up @@ -472,6 +494,15 @@ def filter_kruskal(G, threshold=10000, by_weight=True, weight_function=None,
sage: filter_kruskal(Graph(2), check=True)
[]
TESTS:
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = filter_kruskal(G, by_weight=True)
sage: sum(w for _, _, w in E)
3
"""
return list(filter_kruskal_iterator(G, threshold=threshold,
by_weight=by_weight, weight_function=weight_function,
Expand Down Expand Up @@ -563,6 +594,13 @@ def filter_kruskal_iterator(G, threshold=10000, by_weight=True, weight_function=
sage: len(list(filter_kruskal_iterator(graphs.HouseGraph(), threshold=1)))
4
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = list(filter_kruskal_iterator(G, by_weight=True))
sage: sum(w for _, _, w in E)
3
"""
from sage.graphs.graph import Graph
if not isinstance(G, Graph):
Expand Down Expand Up @@ -776,6 +814,13 @@ def boruvka(G, by_weight=True, weight_function=None, check_weight=True, check=Fa
Traceback (most recent call last):
...
ValueError: the input graph must be undirected
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: E = boruvka(G, by_weight=True)
sage: sum(w for _, _, w in E)
3
"""
from sage.graphs.graph import Graph
if not isinstance(G, Graph):
Expand Down Expand Up @@ -985,6 +1030,13 @@ def random_spanning_tree(G, output_as_graph=False, by_weight=False, weight_funct
Traceback (most recent call last):
...
ValueError: works only for non-empty connected graphs
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: T = G.random_spanning_tree(by_weight=True, output_as_graph=True)
sage: T.is_tree()
True
"""
from sage.misc.prandom import randint
from sage.misc.prandom import random
Expand Down Expand Up @@ -1113,6 +1165,12 @@ def spanning_trees(g, labels=False):
Traceback (most recent call last):
...
ValueError: this method is for undirected graphs only
Check that the method is robust to incomparable vertices::
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
sage: len(list(G.spanning_trees(labels=False)))
4
"""
from sage.graphs.graph import Graph
if not isinstance(g, Graph):
Expand Down Expand Up @@ -1257,6 +1315,14 @@ def edge_disjoint_spanning_trees(G, k, by_weight=False, weight_function=None, ch
Traceback (most recent call last):
...
ValueError: this method is for undirected graphs only
Check that the method is robust to incomparable vertices::
sage: G = Graph()
sage: G.add_clique([0, 1, 2, 'a', 'b'])
sage: F = G.edge_disjoint_spanning_trees(k=2)
sage: len(F)
2
"""
if G.is_directed():
raise ValueError("this method is for undirected graphs only")
Expand Down
14 changes: 8 additions & 6 deletions src/sage/matroids/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,26 +572,28 @@ def lift_cross_ratios(A, lift_map=None):

G = Graph([((r, 0), (c, 1), (r, c)) for r, c in A.nonzero_positions()])
# write the entries of (a scaled version of) A as products of cross ratios of A
T = set()
T = Graph()
for C in G.connected_components_subgraphs():
T.update(C.min_spanning_tree())
T.add_edges(C.min_spanning_tree())
# - fix a tree of the support graph G to units (= empty dict, product of 0 terms)
F = {entry[2]: dict() for entry in T}
W = set(G.edge_iterator()) - set(T)
H = G.subgraph(edges=T)
F = {entry: dict() for entry in T.edge_labels()}
W = set(G.edge_iterator()) - set(T.edge_iterator())
H = G.subgraph(edges=T.edge_iterator())
while W:
# - find an edge in W to process, closing a circuit in H which is induced in G
edge = W.pop()
path = H.shortest_path(edge[0], edge[1])
path_s = set(path)
retry = True
while retry:
retry = False
for edge2 in W:
if edge2[0] in path and edge2[1] in path:
if edge2[0] in path_s and edge2[1] in path_s:
W.add(edge)
edge = edge2
W.remove(edge)
path = H.shortest_path(edge[0], edge[1])
path_s = set(path)
retry = True
break
entry = edge[2]
Expand Down
Loading

0 comments on commit 683b05b

Please sign in to comment.