Skip to content

Commit 683b05b

Browse files
author
Release Manager
committed
gh-36232: Make `min_spanning_tree` robust to incomparable vertex labels 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
2 parents 8d09d15 + 5efb88e commit 683b05b

File tree

5 files changed

+164
-74
lines changed

5 files changed

+164
-74
lines changed

src/sage/graphs/base/boost_graph.pyx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,16 @@ cpdef min_spanning_tree(g,
686686
Traceback (most recent call last):
687687
...
688688
TypeError: float() argument must be a string or a... number...
689+
690+
Check that the method is robust to incomparable vertices::
691+
692+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)], weighted=True)
693+
sage: E = min_spanning_tree(G, algorithm='Kruskal')
694+
sage: sum(w for _, _, w in E)
695+
3
696+
sage: F = min_spanning_tree(G, algorithm='Prim')
697+
sage: sum(w for _, _, w in F)
698+
3
689699
"""
690700
from sage.graphs.graph import Graph
691701

@@ -719,9 +729,8 @@ cpdef min_spanning_tree(g,
719729

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

726735

727736
cpdef blocks_and_cut_vertices(g):

src/sage/graphs/generic_graph.py

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4704,17 +4704,23 @@ def min_spanning_tree(self,
47044704
sage: len(g.min_spanning_tree())
47054705
4
47064706
sage: weight = lambda e: 1 / ((e[0] + 1) * (e[1] + 1))
4707-
sage: sorted(g.min_spanning_tree(weight_function=weight))
4708-
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
4709-
sage: sorted(g.min_spanning_tree(weight_function=weight,
4710-
....: algorithm='Kruskal_Boost'))
4711-
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
4707+
sage: E = g.min_spanning_tree(weight_function=weight)
4708+
sage: T = Graph(E)
4709+
sage: set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree()
4710+
True
4711+
sage: sum(map(weight, E))
4712+
5/12
4713+
sage: E = g.min_spanning_tree(weight_function=weight,
4714+
....: algorithm='Kruskal_Boost')
4715+
sage: Graph(E).is_tree(); sum(map(weight, E))
4716+
True
4717+
5/12
47124718
sage: g = graphs.PetersenGraph()
47134719
sage: g.allow_multiple_edges(True)
47144720
sage: g.add_edges(g.edge_iterator())
4715-
sage: sorted(g.min_spanning_tree())
4716-
[(0, 1, None), (0, 4, None), (0, 5, None), (1, 2, None), (1, 6, None),
4717-
(3, 8, None), (5, 7, None), (5, 8, None), (6, 9, None)]
4721+
sage: T = Graph(g.min_spanning_tree())
4722+
sage: set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree()
4723+
True
47184724

47194725
Boruvka's algorithm::
47204726

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

47274733
sage: g = graphs.CompleteGraph(5)
4728-
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge',
4729-
....: starting_vertex=2, weight_function=weight))
4730-
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
4731-
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe',
4732-
....: starting_vertex=2, weight_function=weight))
4733-
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
4734-
sage: sorted(g.min_spanning_tree(weight_function=weight,
4735-
....: algorithm='Prim_Boost'))
4736-
[(0, 4, None), (1, 4, None), (2, 4, None), (3, 4, None)]
4734+
sage: for algo in ['Prim_edge', 'Prim_fringe', 'Prim_Boost']:
4735+
....: E = g.min_spanning_tree(algorithm=algo, weight_function=weight)
4736+
....: T = Graph(E)
4737+
....: print(set(g) == set(T) and T.order() == T.size() + 1 and T.is_tree())
4738+
True
4739+
True
4740+
True
47374741

47384742
NetworkX algorithm::
47394743

@@ -4745,82 +4749,93 @@ def min_spanning_tree(self,
47454749
sage: G = Graph([(0, 1, {'name': 'a', 'weight': 1}),
47464750
....: (0, 2, {'name': 'b', 'weight': 3}),
47474751
....: (1, 2, {'name': 'b', 'weight': 1})])
4748-
sage: sorted(G.min_spanning_tree(weight_function=lambda e: e[2]['weight']))
4752+
sage: sorted(G.min_spanning_tree(algorithm='Boruvka',
4753+
....: weight_function=lambda e: e[2]['weight']))
47494754
[(0, 1, {'name': 'a', 'weight': 1}), (1, 2, {'name': 'b', 'weight': 1})]
47504755

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

47544759
sage: g = Graph([(1, 2, 1), (1, 3, 2), (2, 3, 1)])
4755-
sage: sorted(g.min_spanning_tree())
4760+
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
47564761
[(1, 2, 1), (1, 3, 2)]
47574762

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

47614766
sage: g.weighted(True)
4762-
sage: sorted(g.min_spanning_tree())
4767+
sage: Graph(g.min_spanning_tree()).edges(sort=True)
47634768
[(1, 2, 1), (2, 3, 1)]
47644769
sage: g.weighted(False)
4765-
sage: sorted(g.min_spanning_tree())
4770+
sage: Graph(g.min_spanning_tree()).edges(sort=True)
47664771
[(1, 2, 1), (1, 3, 2)]
4767-
sage: sorted(g.min_spanning_tree(by_weight=True))
4772+
sage: Graph(g.min_spanning_tree(by_weight=True)).edges(sort=True)
4773+
[(1, 2, 1), (2, 3, 1)]
4774+
sage: Graph(g.min_spanning_tree(weight_function=lambda e: e[2])).edges(sort=True)
47684775
[(1, 2, 1), (2, 3, 1)]
4769-
sage: sorted(g.min_spanning_tree(weight_function=lambda e: e[2]))
4776+
4777+
Note that the order of the vertices on each edge is not guaranteed and
4778+
may differ from an algorithm to the other::
4779+
4780+
sage: g.weighted(True)
4781+
sage: sorted(g.min_spanning_tree())
4782+
[(2, 1, 1), (3, 2, 1)]
4783+
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
4784+
[(1, 2, 1), (2, 3, 1)]
4785+
sage: Graph(g.min_spanning_tree()).edges(sort=True)
47704786
[(1, 2, 1), (2, 3, 1)]
47714787

4788+
47724789
TESTS:
47734790

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

47774794
sage: g = Graph(weighted=True)
47784795
sage: g.add_edges([[0, 1, 1], [1, 2, 1], [2, 0, 10]])
4779-
sage: sorted(g.min_spanning_tree())
4796+
sage: Graph(g.min_spanning_tree()).edges(sort=True)
47804797
[(0, 1, 1), (1, 2, 1)]
4781-
sage: sorted(g.min_spanning_tree(algorithm='Filter_Kruskal'))
4798+
sage: Graph(g.min_spanning_tree(algorithm='Filter_Kruskal')).edges(sort=True)
47824799
[(0, 1, 1), (1, 2, 1)]
4783-
sage: sorted(g.min_spanning_tree(algorithm='Kruskal_Boost'))
4800+
sage: Graph(g.min_spanning_tree(algorithm='Kruskal_Boost')).edges(sort=True)
47844801
[(0, 1, 1), (1, 2, 1)]
4785-
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe'))
4802+
sage: Graph(g.min_spanning_tree(algorithm='Prim_fringe')).edges(sort=True)
47864803
[(0, 1, 1), (1, 2, 1)]
4787-
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge'))
4804+
sage: Graph(g.min_spanning_tree(algorithm='Prim_edge')).edges(sort=True)
47884805
[(0, 1, 1), (1, 2, 1)]
4789-
sage: sorted(g.min_spanning_tree(algorithm='Prim_Boost'))
4806+
sage: Graph(g.min_spanning_tree(algorithm='Prim_Boost')).edges(sort=True)
47904807
[(0, 1, 1), (1, 2, 1)]
4791-
sage: sorted(g.min_spanning_tree(algorithm='NetworkX')) # needs networkx
4808+
sage: Graph(g.min_spanning_tree(algorithm='Boruvka')).edges(sort=True)
47924809
[(0, 1, 1), (1, 2, 1)]
4793-
sage: sorted(g.min_spanning_tree(algorithm='Boruvka'))
4810+
sage: Graph(g.min_spanning_tree(algorithm='NetworkX')).edges(sort=True) # needs networkx
47944811
[(0, 1, 1), (1, 2, 1)]
47954812

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

47994816
sage: g = Graph([[0, 1, 1], [1, 2, 1], [2, 0, 10]], weighted=True)
48004817
sage: weight = lambda e: 3 - e[0] - e[1]
4801-
sage: sorted(g.min_spanning_tree(weight_function=weight))
4818+
sage: Graph(g.min_spanning_tree(weight_function=weight)).edges(sort=True)
48024819
[(0, 2, 10), (1, 2, 1)]
4803-
sage: sorted(g.min_spanning_tree(algorithm='Filter_Kruskal', weight_function=weight))
4820+
sage: Graph(g.min_spanning_tree(algorithm='Filter_Kruskal', weight_function=weight)).edges(sort=True)
48044821
[(0, 2, 10), (1, 2, 1)]
4805-
sage: sorted(g.min_spanning_tree(algorithm='Kruskal_Boost', weight_function=weight))
4822+
sage: Graph(g.min_spanning_tree(algorithm='Kruskal_Boost', weight_function=weight)).edges(sort=True)
48064823
[(0, 2, 10), (1, 2, 1)]
4807-
sage: sorted(g.min_spanning_tree(algorithm='Prim_fringe', weight_function=weight))
4824+
sage: Graph(g.min_spanning_tree(algorithm='Prim_fringe', weight_function=weight)).edges(sort=True)
48084825
[(0, 2, 10), (1, 2, 1)]
4809-
sage: sorted(g.min_spanning_tree(algorithm='Prim_edge', weight_function=weight))
4826+
sage: Graph(g.min_spanning_tree(algorithm='Prim_edge', weight_function=weight)).edges(sort=True)
48104827
[(0, 2, 10), (1, 2, 1)]
4811-
sage: sorted(g.min_spanning_tree(algorithm='Prim_Boost', weight_function=weight))
4828+
sage: Graph(g.min_spanning_tree(algorithm='Prim_Boost', weight_function=weight)).edges(sort=True)
48124829
[(0, 2, 10), (1, 2, 1)]
4813-
sage: sorted(g.min_spanning_tree(algorithm='NetworkX', weight_function=weight)) # needs networkx
4814-
[(0, 2, 10), (1, 2, 1)]
4815-
sage: sorted(g.min_spanning_tree(algorithm='Boruvka', weight_function=weight))
4830+
sage: Graph(g.min_spanning_tree(algorithm='NetworkX', weight_function=weight)).edges(sort=True) # needs networkx
48164831
[(0, 2, 10), (1, 2, 1)]
48174832

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

48204835
sage: g = digraphs.Circuit(3)
4821-
sage: sorted(g.min_spanning_tree(weight_function=weight))
4836+
sage: Graph(g.min_spanning_tree(weight_function=weight)).edges(sort=True)
48224837
[(0, 2, None), (1, 2, None)]
4823-
sage: sorted(g.to_undirected().min_spanning_tree(weight_function=weight))
4838+
sage: Graph(g.to_undirected().min_spanning_tree(weight_function=weight)).edges(sort=True)
48244839
[(0, 2, None), (1, 2, None)]
48254840

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

48424857
sage: graphs.EmptyGraph().min_spanning_tree()
48434858
[]
4859+
4860+
Check that the method is robust to incomparable vertices::
4861+
4862+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
4863+
sage: E = G.min_spanning_tree(algorithm='Prim_Boost', by_weight=True)
4864+
sage: E = G.min_spanning_tree(algorithm='Prim_fringe', by_weight=True)
4865+
sage: E = G.min_spanning_tree(algorithm='Prim_edge', by_weight=True)
4866+
sage: E = G.min_spanning_tree(algorithm='Kruskal_Boost', by_weight=True)
4867+
sage: E = G.min_spanning_tree(algorithm='Filter_Kruskal', by_weight=True)
4868+
sage: E = G.min_spanning_tree(algorithm='Boruvka', by_weight=True)
4869+
sage: E = G.min_spanning_tree(algorithm='NetworkX', by_weight=True) # needs networkx
48444870
"""
48454871
if not self.order():
48464872
return []
@@ -5136,9 +5162,9 @@ def cycle_basis(self, output='vertex'):
51365162
sage: [sorted(c) for c in G.cycle_basis()] # needs networkx
51375163
[['Hey', 'Really ?', 'Wuuhuu'], [0, 2], [0, 1, 2]]
51385164
sage: [sorted(c) for c in G.cycle_basis(output='edge')] # needs networkx
5139-
[[('Hey', 'Wuuhuu', None),
5140-
('Really ?', 'Hey', None),
5141-
('Wuuhuu', 'Really ?', None)],
5165+
[[('Hey', 'Really ?', None),
5166+
('Really ?', 'Wuuhuu', None),
5167+
('Wuuhuu', 'Hey', None)],
51425168
[(0, 2, 'a'), (2, 0, 'b')],
51435169
[(0, 2, 'b'), (1, 0, 'c'), (2, 1, 'd')]]
51445170

src/sage/graphs/spanning_tree.pyx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ def kruskal(G, by_weight=True, weight_function=None, check_weight=False, check=F
241241
Traceback (most recent call last):
242242
...
243243
ValueError: the input graph must be undirected
244+
245+
Check that the method is robust to incomparable vertices::
246+
247+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
248+
sage: E = kruskal(G, by_weight=True)
249+
sage: sum(w for _, _, w in E)
250+
3
244251
"""
245252
return list(kruskal_iterator(G, by_weight=by_weight, weight_function=weight_function,
246253
check_weight=check_weight, check=check))
@@ -313,6 +320,13 @@ def kruskal_iterator(G, by_weight=True, weight_function=None, check_weight=False
313320
Traceback (most recent call last):
314321
...
315322
ValueError: the input graph must be undirected
323+
324+
Check that the method is robust to incomparable vertices::
325+
326+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
327+
sage: E = list(kruskal_iterator(G, by_weight=True))
328+
sage: sum(w for _, _, w in E)
329+
3
316330
"""
317331
from sage.graphs.graph import Graph
318332
if not isinstance(G, Graph):
@@ -382,6 +396,14 @@ def kruskal_iterator_from_edges(edges, union_find, by_weight=True,
382396
sage: union_set = DisjointSet(G)
383397
sage: next(kruskal_iterator_from_edges(G.edges(sort=False), union_set, by_weight=G.weighted()))
384398
(1, 6, 10)
399+
400+
Check that the method is robust to incomparable vertices::
401+
402+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
403+
sage: union_set = DisjointSet(G)
404+
sage: E = list(kruskal_iterator_from_edges(G.edges(sort=False), union_set, by_weight=True))
405+
sage: sum(w for _, _, w in E)
406+
3
385407
"""
386408
# We sort edges, as specified.
387409
if weight_function is not None:
@@ -472,6 +494,15 @@ def filter_kruskal(G, threshold=10000, by_weight=True, weight_function=None,
472494
473495
sage: filter_kruskal(Graph(2), check=True)
474496
[]
497+
498+
TESTS:
499+
500+
Check that the method is robust to incomparable vertices::
501+
502+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
503+
sage: E = filter_kruskal(G, by_weight=True)
504+
sage: sum(w for _, _, w in E)
505+
3
475506
"""
476507
return list(filter_kruskal_iterator(G, threshold=threshold,
477508
by_weight=by_weight, weight_function=weight_function,
@@ -563,6 +594,13 @@ def filter_kruskal_iterator(G, threshold=10000, by_weight=True, weight_function=
563594
564595
sage: len(list(filter_kruskal_iterator(graphs.HouseGraph(), threshold=1)))
565596
4
597+
598+
Check that the method is robust to incomparable vertices::
599+
600+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
601+
sage: E = list(filter_kruskal_iterator(G, by_weight=True))
602+
sage: sum(w for _, _, w in E)
603+
3
566604
"""
567605
from sage.graphs.graph import Graph
568606
if not isinstance(G, Graph):
@@ -776,6 +814,13 @@ def boruvka(G, by_weight=True, weight_function=None, check_weight=True, check=Fa
776814
Traceback (most recent call last):
777815
...
778816
ValueError: the input graph must be undirected
817+
818+
Check that the method is robust to incomparable vertices::
819+
820+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
821+
sage: E = boruvka(G, by_weight=True)
822+
sage: sum(w for _, _, w in E)
823+
3
779824
"""
780825
from sage.graphs.graph import Graph
781826
if not isinstance(G, Graph):
@@ -985,6 +1030,13 @@ def random_spanning_tree(G, output_as_graph=False, by_weight=False, weight_funct
9851030
Traceback (most recent call last):
9861031
...
9871032
ValueError: works only for non-empty connected graphs
1033+
1034+
Check that the method is robust to incomparable vertices::
1035+
1036+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
1037+
sage: T = G.random_spanning_tree(by_weight=True, output_as_graph=True)
1038+
sage: T.is_tree()
1039+
True
9881040
"""
9891041
from sage.misc.prandom import randint
9901042
from sage.misc.prandom import random
@@ -1113,6 +1165,12 @@ def spanning_trees(g, labels=False):
11131165
Traceback (most recent call last):
11141166
...
11151167
ValueError: this method is for undirected graphs only
1168+
1169+
Check that the method is robust to incomparable vertices::
1170+
1171+
sage: G = Graph([(1, 2, 10), (1, 'a', 1), ('a', 'b', 1), ('b', 2, 1)])
1172+
sage: len(list(G.spanning_trees(labels=False)))
1173+
4
11161174
"""
11171175
from sage.graphs.graph import Graph
11181176
if not isinstance(g, Graph):
@@ -1257,6 +1315,14 @@ def edge_disjoint_spanning_trees(G, k, by_weight=False, weight_function=None, ch
12571315
Traceback (most recent call last):
12581316
...
12591317
ValueError: this method is for undirected graphs only
1318+
1319+
Check that the method is robust to incomparable vertices::
1320+
1321+
sage: G = Graph()
1322+
sage: G.add_clique([0, 1, 2, 'a', 'b'])
1323+
sage: F = G.edge_disjoint_spanning_trees(k=2)
1324+
sage: len(F)
1325+
2
12601326
"""
12611327
if G.is_directed():
12621328
raise ValueError("this method is for undirected graphs only")

src/sage/matroids/utilities.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -572,26 +572,28 @@ def lift_cross_ratios(A, lift_map=None):
572572

573573
G = Graph([((r, 0), (c, 1), (r, c)) for r, c in A.nonzero_positions()])
574574
# write the entries of (a scaled version of) A as products of cross ratios of A
575-
T = set()
575+
T = Graph()
576576
for C in G.connected_components_subgraphs():
577-
T.update(C.min_spanning_tree())
577+
T.add_edges(C.min_spanning_tree())
578578
# - fix a tree of the support graph G to units (= empty dict, product of 0 terms)
579-
F = {entry[2]: dict() for entry in T}
580-
W = set(G.edge_iterator()) - set(T)
581-
H = G.subgraph(edges=T)
579+
F = {entry: dict() for entry in T.edge_labels()}
580+
W = set(G.edge_iterator()) - set(T.edge_iterator())
581+
H = G.subgraph(edges=T.edge_iterator())
582582
while W:
583583
# - find an edge in W to process, closing a circuit in H which is induced in G
584584
edge = W.pop()
585585
path = H.shortest_path(edge[0], edge[1])
586+
path_s = set(path)
586587
retry = True
587588
while retry:
588589
retry = False
589590
for edge2 in W:
590-
if edge2[0] in path and edge2[1] in path:
591+
if edge2[0] in path_s and edge2[1] in path_s:
591592
W.add(edge)
592593
edge = edge2
593594
W.remove(edge)
594595
path = H.shortest_path(edge[0], edge[1])
596+
path_s = set(path)
595597
retry = True
596598
break
597599
entry = edge[2]

0 commit comments

Comments
 (0)