Skip to content

Commit 86e8333

Browse files
author
Release Manager
committed
sagemathgh-39519: Provide an iterative version on some functions on trees This pull request provide iterative methods on AbstractTrees which work both on OrderedTree and BinaryTree. The problem was that some functions like node_number reach the recursion limit on huge instances. ``` sage: T = OrderedTree([]) sage: for _ in range(10000): ....: T = OrderedTree([]) sage: T.node_number() RecursionError ``` ### 📝 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. - [ ] 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. URL: sagemath#39519 Reported by: Oscfon Reviewer(s): Frédéric Chapoton, Martin Rubey, Oscfon
2 parents dc0984f + c5835a2 commit 86e8333

File tree

1 file changed

+187
-16
lines changed

1 file changed

+187
-16
lines changed

src/sage/combinat/abstract_tree.py

+187-16
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,7 @@ def action(x):
276276
while stack:
277277
node = stack.pop()
278278
action(node)
279-
for i in range(len(node)):
280-
subtree = node[-i - 1]
279+
for subtree in reversed(node):
281280
if not subtree.is_empty():
282281
stack.append(subtree)
283282

@@ -639,15 +638,122 @@ def action(x):
639638
# subtrees, and should not be exploded again, but instead
640639
# should be manipulated and removed from the stack.
641640
stack.append(None)
642-
for i in range(len(node)):
643-
subtree = node[-i - 1]
641+
for subtree in reversed(node):
644642
if not subtree.is_empty():
645643
stack.append(subtree)
646644
else:
647645
stack.pop()
648646
node = stack.pop()
649647
action(node)
650648

649+
def contour_traversal(self, first_action=None, middle_action=None, final_action=None, leaf_action=None):
650+
r"""
651+
Run the counterclockwise countour traversal algorithm (iterative
652+
implementation) and subject every node encountered
653+
to some procedure ``first_action``, ``middle_action`` or ``final_action`` each time it reaches it.
654+
655+
ALGORITHM:
656+
657+
- if the root is a leaf, apply `leaf_action`
658+
- else
659+
- apply `first_action` to the root
660+
- iteratively apply `middle_action` to the root and traverse each subtree
661+
from the leftmost one to the rightmost one
662+
- apply `final_action` to the root
663+
664+
INPUT:
665+
666+
- ``first_action`` -- (optional) a function which takes a node as
667+
input, and does something the first time it is reached during exploration
668+
669+
- ``middle_action`` -- (optional) a function which takes a node as
670+
input, and does something each time it explore one of its children
671+
672+
- ``final_action`` -- (optional) a function which takes a node as
673+
input, and does something the last time it is reached during exploration
674+
675+
- ``leaf_action`` -- (optional) a function which takes a leaf as
676+
input, and does something when it is reached during exploration.
677+
678+
OUTPUT:
679+
680+
``None``. (This is *not* an iterator.)
681+
682+
TESTS::
683+
684+
sage: l = []
685+
sage: t = OrderedTree([[],[[],[],]]).canonical_labelling()
686+
sage: t
687+
1[2[], 3[4[], 5[]]]
688+
sage: t.contour_traversal(lambda node: (l.append(node.label()),l.append('a')),
689+
....: lambda node: (l.append(node.label()),l.append('b')),
690+
....: lambda node: (l.append(node.label()),l.append('c')),
691+
....: lambda node: (l.append(node.label())))
692+
sage: l
693+
[1, 'a', 1, 'b', 2, 1, 'b', 3, 'a', 3, 'b', 4, 3, 'b', 5, 3, 'c', 1, 'c']
694+
695+
sage: l = []
696+
sage: b = BinaryTree([[None,[]],[[[],[]],[]]]).canonical_labelling()
697+
sage: b
698+
3[1[., 2[., .]], 7[5[4[., .], 6[., .]], 8[., .]]]
699+
sage: b.contour_traversal(lambda node: l.append(node.label()),
700+
....: lambda node: l.append(node.label()),
701+
....: lambda node: l.append(node.label()),
702+
....: None)
703+
sage: l
704+
[3, 3, 1, 1, 1, 2, 2, 2, 2, 1, 3, 7, 7, 5, 5, 4, 4, 4, 4, 5, 6, 6, 6, 6, 5, 7, 8, 8, 8, 8, 7, 3]
705+
706+
The following test checks that things do not go wrong if some among
707+
the descendants of the tree are equal or even identical::
708+
709+
sage: u = BinaryTree(None)
710+
sage: v = BinaryTree([u, u])
711+
sage: w = BinaryTree([v, v])
712+
sage: t = BinaryTree([w, w])
713+
sage: t.node_number()
714+
7
715+
sage: l = []
716+
sage: t.contour_traversal(first_action = lambda node: l.append(0))
717+
sage: len(l)
718+
7
719+
"""
720+
if first_action is None:
721+
def first_action(x):
722+
return
723+
if middle_action is None:
724+
def middle_action(x):
725+
return
726+
if final_action is None:
727+
def final_action(x):
728+
return
729+
if leaf_action is None:
730+
def leaf_action(x):
731+
return
732+
stack = []
733+
stack.append(self)
734+
corners = [0, 0]
735+
while stack:
736+
node = stack.pop()
737+
if not node:
738+
leaf_action(node)
739+
corners.pop()
740+
corners[-1] += 1
741+
elif not corners[-1]:
742+
first_action(node)
743+
middle_action(node)
744+
stack.append(node)
745+
stack.append(node[0])
746+
corners.append(0)
747+
elif corners[-1] == len(node):
748+
final_action(node)
749+
corners.pop()
750+
corners[-1] += 1
751+
else:
752+
middle_action(node)
753+
stack.append(node)
754+
stack.append(node[corners[-1]])
755+
corners.append(0)
756+
651757
def breadth_first_order_traversal(self, action=None):
652758
r"""
653759
Run the breadth-first post-order traversal algorithm
@@ -823,12 +929,41 @@ def node_number_at_depth(self, depth):
823929
True
824930
sage: [T.node_number_at_depth(i) for i in range(3)]
825931
[0, 0, 0]
932+
933+
Check that we do not hit a recursion limit::
934+
935+
sage: T = OrderedTree([])
936+
sage: for _ in range(9999):
937+
....: T = OrderedTree([T])
938+
sage: T.node_number_at_depth(2000)
939+
1
826940
"""
827941
if self.is_empty():
828-
return Integer(0)
829-
if depth == 0:
830-
return Integer(1)
831-
return sum(son.node_number_at_depth(depth - 1) for son in self)
942+
return 0
943+
m = 0
944+
945+
def fr_action(node):
946+
nonlocal m, depths, depth
947+
if depths[-1] == depth:
948+
m += 1
949+
950+
def m_action(node):
951+
nonlocal depths
952+
depths.append(depths[-1] + 1)
953+
954+
def fn_action(node):
955+
nonlocal depths
956+
depths.pop()
957+
958+
def lf_action(node):
959+
nonlocal m, depths, depth
960+
if depths[-1] == depth:
961+
m += 1
962+
depths.pop()
963+
964+
depths = [0]
965+
self.contour_traversal(fr_action, m_action, fn_action, lf_action)
966+
return Integer(m)
832967

833968
def paths_to_the_right(self, path):
834969
r"""
@@ -1052,11 +1187,25 @@ def node_number(self):
10521187
2
10531188
sage: BinaryTree([[None, [[], []]], None]).node_number()
10541189
5
1190+
1191+
TESTS:
1192+
1193+
Check that we do not hit a recursion limit::
1194+
1195+
sage: T = OrderedTree([])
1196+
sage: for _ in range(9999):
1197+
....: T = OrderedTree([T])
1198+
sage: T.node_number()
1199+
10000
10551200
"""
1056-
if self.is_empty():
1057-
return Integer(0)
1058-
else:
1059-
return sum((i.node_number() for i in self), Integer(1))
1201+
count = 0
1202+
1203+
def incr(node):
1204+
nonlocal count
1205+
count += 1
1206+
1207+
self.iterative_pre_order_traversal(incr)
1208+
return Integer(count)
10601209

10611210
def depth(self):
10621211
"""
@@ -1079,11 +1228,33 @@ def depth(self):
10791228
0
10801229
sage: BinaryTree([[],[[],[]]]).depth()
10811230
3
1231+
1232+
TESTS:
1233+
1234+
Check that we do not hit a recursion limit::
1235+
1236+
sage: T = OrderedTree([])
1237+
sage: for _ in range(9999):
1238+
....: T = OrderedTree([T])
1239+
sage: T.depth()
1240+
10000
10821241
"""
1083-
if self:
1084-
return Integer(1 + max(i.depth() for i in self))
1085-
else:
1086-
return Integer(0 if self.is_empty() else 1)
1242+
if self.is_empty():
1243+
return 0
1244+
m = []
1245+
1246+
def action(node):
1247+
nonlocal m
1248+
if node.is_empty():
1249+
m.append(-1)
1250+
elif not bool(node):
1251+
m.append(0)
1252+
else:
1253+
mx = max(m.pop() for _ in node)
1254+
m.append(mx + 1)
1255+
1256+
self.contour_traversal(final_action=action, leaf_action=action)
1257+
return Integer(m[0] + 1)
10871258

10881259
def _ascii_art_(self):
10891260
r"""

0 commit comments

Comments
 (0)