-3

The max-heaps can have arbitrary branching factors within the same tree itself.

For instance given [8,5,3,1] some of the heaps generated could be

  8      or       8       or    8        
 /|\              |             |
5 1 3             5             5           and so on.....
 (a)             / \            |
                3   1           3
                 (b)            |
                                1
                               (c)

For my purposes , I consider tree (a) above the same as trees (d) and (e) below

  8              8
 /|\            /|\              and so on.....
3 5 1          1 5 3 
 (d)            (e)

Edit :

(1) I have tried an algorithm to generate all possible trees and then filter based on the max-heap property. But clearly that is exponential and hence even a list with greater than 10 elements takes a while in Python.

(2) I would like to construct thus only those trees that obey the max-heap property rather than filtering but am unable to formulate a recursive sub-problem out of this.

Haskar P
  • 420
  • 4
  • 9
  • What have you tried so far? – Kevin W. Sep 04 '18 at 20:03
  • I have tried an algorithm to generate all possible trees and then filter based on the max-heap property. But clearly that is exponential and hence even a list with greater than 10 elements takes a while in Python. – Haskar P Sep 04 '18 at 20:06
  • Why so many downvotes? – Haskar P Sep 04 '18 at 20:22
  • Not a downvoter, but this question lacks effort, please post what you have tried even if it doesn't work. Also this question is kinda broad as it stands, and it feels like a "give me the codes" type of question which is generally not well received. My suggestion to improve this question, is to post the code you have tried, and explain what's wrong with it in a [MCVE] form – MooingRawr Sep 04 '18 at 20:29
  • It's not immediately obvious that the number of valid heaps isn't exponential in the input size, so an exponential time algorithm to find them isn't necessarily inefficient. (If all you want to do is compute the number, rather than enumerate them, then something sub-exponential might be possible.) – chepner Sep 04 '18 at 20:33
  • Alright. I shall modify the question accordingly. – Haskar P Sep 04 '18 at 23:00
  • @chepner Yes, I believe the number of valid heaps is also exponential but down from the number of trees by quite a significant factor , since given a max heap structure , any permutation of the nodes/labels remains a tree but only select few are max-heaps. I wish to iterate on them , I assume using a generator would be the most logical option given the sheer magnitude of possibilities. – Haskar P Sep 04 '18 at 23:03
  • My end objective is to select a subset of these max-heaps based on some additionally imposed constraints as defined by me. Again , I feel this quite obviously is better done during the generation of trees/heaps , rather than after. – Haskar P Sep 04 '18 at 23:05
  • I am unable to code this logic into my recursive function. Anyways I shall re-frame the question with more context and detail. soon. – Haskar P Sep 04 '18 at 23:07
  • You need to define what you consider a heap. If it must meet the shape property and the heap property (a node is greater than or equal to any of its children), then you're restricted to [d-ary heap](https://en.wikipedia.org/wiki/D-ary_heap), and your example (b) doesn't apply. If you only have to meet the heap property, then the number of possibilities increases quite a bit. But without a complete definition of what you consider a max-heap, your question is unanswerable. – Jim Mischel Sep 04 '18 at 23:35
  • 1
    See https://www.geeksforgeeks.org/number-ways-form-heap-n-distinct-integers/, which you can extend to d-ary heaps. For your non-standard heap types (your example b), you're on your own. – Jim Mischel Sep 04 '18 at 23:37
  • Yes. As mentioned in the description , the heaps can have arbitrary branching factor within the heap itself. So it would not be a d-ary heap. – Haskar P Sep 04 '18 at 23:37
  • The link you provided seem to consider binary heaps. I hope the natural extension to variable-ary heaps is trivial. – Haskar P Sep 04 '18 at 23:39
  • The extension to d-ary heaps is pretty easy. Extending to variable-ary heaps is non-trivial. And then there is [Pairing heap](https://en.wikipedia.org/wiki/Pairing_heap), which results in even more possible arrangements, especially if you represent it with a [left-child right-sibling](https://en.wikipedia.org/wiki/Left-child_right-sibling_binary_tree) binary tree. – Jim Mischel Sep 05 '18 at 02:35
  • The number of heaps to generate is still going to be at least exponential in the size of the input. You should find a different way to solve your underlying problem. – user2357112 Sep 05 '18 at 03:51
  • @user2357112: it's factorial, which is slightly worse than exponential. – rici Sep 05 '18 at 03:53
  • @rici: Huh. I see your answer has a proof. It's a lot simpler than I was thinking. – user2357112 Sep 05 '18 at 03:56

1 Answers1

2

This one is a lot simpler than the unrestricted tree generator.

It's interesting to observe that for k elements, there are exactly (k-1)! possible general heaps. (If we were generating forests of heaps, there would be k! possible forests, which is equivalent to generating a single heap with a new node as root.)

The key insight is that the heap property guarantees that the largest element of any subtree is the root of that subtree (and consequently the largest element is the root of the tree). Since we don't care about the order of children, we can agree to place the children in descending order at each node, which will guarantee that the second-largest element in a subtree will be exactly the leftmost child of the root of that subtree.

So we can just place the elements in decreasing order, iterating over all the possible placements. After we make the largest element the root of the tree, each subsequent element in turn can be made the last (or only) child of any previously placed element. (All previously placed children are larger than the new element, so putting it at the first position maintains the canonical child order. And of course, since all previously placed elements are larger, the new element can be a child of any of them.)

With that procedure, at the step where i elements have already been placed, there are exactly i possible placements for the next element. Hence the formula (k-1)!.

Implementing the above is quite straight-forward, although it is anything but a functional solution: the candidate tree is modified at every step. (That means that you would need to do a complete copy of a yielded tree if you intended to modify it or keep it for future reference.)

# This differs from the tree object in the other answer because
# in this tree, the kids are modified and must therefore be lists
class tree(object):
    def __init__(self, root, kids=()):
        self.root = root
        self.kids = list(kids)
    def __str__(self):
        if self.kids:
            return "(%s %s)" % (self.root,
                                ' '.join(str(kid) for kid in self.kids))
        else:
            return self.root

# The main function turns each label into a singleton (leaf) node, and
# then calls the helper function to recursively stitch them together into
# heaps
def allheaps(labels):
    if labels:
        yield from genheaps(list(map(tree, labels)), 1)

def genheaps(nodes, i):
    if i == len(nodes): yield nodes[0]
    else:
        # For each possible position for node i:
        # Place it, recurse the rest of the nodes, restore the parent.
        for j in range(i):
            nodes[j].kids.append(nodes[i])
            yield from genheaps(nodes, i+1)
            nodes[j].kids.pop()

Here's the six heaps built from 8, 5, 3, 1:

>>> print('\n'.join(map(str,allheaps("8531"))))
(8 5 3 1)
(8 (5 1) 3)
(8 5 (3 1))
(8 (5 3) 1)
(8 (5 3 1))
(8 (5 (3 1)))

Or, diagrammatically (done by hand)

(8 5 3 1) (8 (5 1) 3) (8 5 (3 1)) (8 (5 3) 1) (8 (5 3 1)) (8 (5 (3 1)))
    8          8           8           8           8            8
  / | \      /   \       /   \       /   \         |            |
 5  3  1    5     3     5     3     5      1       5            5
            |                 |     |            /   \          |
            1                 1     3           3     1         3
                                                                |
                                                                1

The fact that the number of heaps is the factorial of the number of non-root nodes suggests that there is an isomorphism between heaps and permutations. And indeed there is, as can be seen with the help of the diagrams above.

We can turn a heap into a permutation by doing a post-order depth-first traverse of the tree. The post-order traverse guarantees that the last node in the walk will be the root.

To go the other way, from a permutation ending with the root label to a heap, we initialise an empty stack and scan the permutation left-to-right. We push each label onto the stack, after first populating its child list by popping any smaller elements from the top of the stack. If the permutation ends with the largest element, it will be the only element on the stack at the end of the scan. (If we allowed arbitrary permutations, we would get the n! heap forests instead of the (n-1)! rooted heaps.)

This suggests that we could enumerate heaps by using any convenient method of enumerating permutations and constructing the heap from the permutation:

from itertools import permutations
from functools import reduce
def putlabel(stk, label):
    kids=[]
    while stk and stk[-1].root < label: kids.append(stk.pop())
    stk.append(tree(label, kids))
    return stk

def permtoheap(root, perm):
    '''Construct a heap with root 'root' using the non-root nodes from
       a postorder walk. 'root' should be larger than max(perm); otherwise,
       the result will not satisfy the heap property.
    '''
    return tree(root, reduce(putlabel, perm, []))

def allheaps2(labels):
    if labels:
        yield from (permtoheap(labels[0], p) for p in permutations(labels[1:]))
rici
  • 234,347
  • 28
  • 237
  • 341
  • You might want to make a copy of the tree before you yield it. This code yields the same object over and over, so trying to collect the results into a list or do anything to store the trees causes things to break. – user2357112 Sep 05 '18 at 04:02
  • @user2357112: Yeah, that might be good. But it's only intended to show the algorithm. It's also possible to do it functionally, but Python isn't really a functional language. (Also, if most heaps will be rejected immediately, the copy would be a waste of time; better to copy only the ones you need.) – rici Sep 05 '18 at 04:35