Let n be a positive integer, and let S be a set of n-tuples of non-negative integers. (For example, if n is 3, then S could be {(1,2,0), (1,0,1)}.)
Let's define an operation called "promote" that takes an n-tuple of non-negative integers (t1, t2, …, tn), an index i such that ti > 0, and an index j that's less than i, and returns the same n-tuple, except that the value at index j has been incremented and the value at index i has been decremented. For example, if we have the tuple (1,2,3,4,5), i = 5, and j = 2, then "promote" will return (1,3,3,4,4).
I want to compute the closure of "promote" over my set S, meaning the set of all tuples that I can reach by starting with an element from S and making zero or more calls to "promote" (with some choice of i and j for any given call). For example, if S is {(1,2,0), (1,0,1)}, then I want to compute the set {(2,0,0), (1,1,0), (1,0,1), (3,0,0), (2,1,0), (1,2,0)}. (For example, I can get (2,1,0) by starting with (1,2,0) and calling "promote" with i = 2 and j = 1, so (2,1,0).)
How can I do this efficiently?
Ideas
One idea is to initialize my result set with S, apply as many promote moves starting from tuples in this set as I can, add these to the set, union these new tuples with my result set so far, and repeat. However, there's a lot of overlap with this approach?
Another idea is instead of applying promote moves to all the tuples all at once, to only apply a promote move to the minimal tuple with respect to some ordering. But how could one parallelize that approach?
I was particularly curious if there was a clean way to code this in a functional programming way. However, I'm ultimately concerned with just time efficiency.
EDIT: One could think of this as a digraph whose vertices are tuples in the closure with an edge between two tuples if there is a promote operation from one to the other. Then the first bullet above becomes "perform BFS starting from the tuples in the original set". (I added some tags to reflect this).
Side Remark: If anyone can think of a more descriptive title please change it or let me know so I can change it!
If you picture the integers in these tuples as representing piles of objects, then you can think of promotion as "moving an object leftward" from one pile to another.
So, given two n-tuples t and u of nonnegative integers with the same sum, you can reach u from t by a sequence of promotions if and only if, for each i from 1 to n, the sum of the last i elements of u is less than or equal to the sum of the last i elements of t.
For example, suppose t is (1,2,0). Its suffix sums form the sequence [1+2+0,2+0,0], which is [3,2,0]. So any tuple reachable from (1,2,0) will have suffix sums that form a nonincreasing sequence whose first element is 3 and whose ith element is at most equal to the ith element of [3,2,0]. So in lexicographic order, the possible suffix sum sequences are [3,0,0], [3,1,0], and [3,2,0], meaning that the possible tuples are (3,0,0), (2,1,0), and (1,2,0). (Each element of the tuple, except the last, is the difference between successive elements of the suffix-sum sequence — and the last element of the tuple is the same as the last element of the suffix-sum sequence — so each suffix-sum sequence uniquely determines a tuple.)
In your example, the elements of S have distinct sums, so they're essentially unrelated; you can compute their closures independently without worrying about overlap.
In the more general case where some elements of S might have the same sum, you can break S up into subsets with equal sums, and handle each group independently. So in what follows, I'll assume that all elements of S have the same sum.
What you can do is:
If you're using functional programming for this, then you'll probably want to store the suffix-sum sequences that you're building in reverse order; for example, [3,0,…] can be represented as …::0::3::nil, so that you can efficient "append" by putting new elements at the head of the list. You can then fix that order at the same time as you compute the resulting tuples. Additionally, you'll likely want to build your set of results as a linked list as well, by passing it as a list in your recursive calls, and having your recursive calls return an augmented copy of it.
I don't have experience with parallelizing FP algorithms, but if you're comfortable doing that in general, then I think you'll have no difficulty doing it for the above. One good place to split into concurrent paths might be immediately after you've validated a given value of a given element of a suffix-sum sequence: one path can proceed recursively to the next element, while another path proceeds to the next-greater value of the same element. (But I think you'll need a different data structure for your results — maybe some sort of branching tree, that you then post-process into a list?)