1. Introduction
In this tutorial, we’ll present the recursive and iterative algorithms for generating permutations with repetition. Finding all permutations with repetition is a combinatorial problem like generating all -combinations of a set.
2. Permutations With Repetition
Permutations with repetition of a set are ordered tuples whose elements come from
and may be repeated. If the tuples’ length is
, we call them
-tuples. For example, with
and
, the following are 4-tuples of
:
Our task is to generate all the -tuples of a set
. If
, there are
such tuples.
Here, we’ll take to be of the form
. We don’t lose generality in doing so because we can map each set of
elements to
. So, we can map each
-tuple of
back to the elements of the original set.
To ease notation, we’ll write instead of
.
3. Recursive Algorithm for Generating Permutations with Repetition
We can define permutations with repetition by induction over :
- If
, then the only permutation is the empty one that we’ll denote as
.
- If
, then any
-tuple can be obtained by prepending a number from
to a
-tuple.
These rules give us the logic to follow recursively. is the base case, and for each
we prepend every
to each tuple we got for
.
3.1. Pseudocode
Here’s the pseudocode of the recursive solution:
algorithm Find(n, k):
// INPUT
// n = the set cardinality
// k = the length of permutations with repetition
// OUTPUT
// All the k-tuples of the set {1, 2, ..., n}
if k = 0:
return { [ ] }
else:
tuples_k_minus_1 <- Find(n, k-1)
tuples_k <- empty set
for tuple in tuples_k_minus_1:
for i <- 1 to n:
new_tuple <- prepend i to tuple
tuples_k <- tuples_k + { new_tuple }
return tuples_k
3.2. Example
Let’s see how the function works. Let’s say that
and that we want to find all the 3-tuples. The call
returns
2-tuples:
To get -tuples, we first prepend 1 to all the
2-tuples and get:
Then, we prepend 2:
Then, after doing the same with 3, 4, and 5, we get all the 3-tuples.
4. Iterative Algorithm for Generating Permutations with Repetition
The recursive algorithm makes the -tuples available once it generates them all. That is, we can’t access any
-tuple before generating all of them first. Since there are
of them, the recursive algorithm’s space complexity is
. If our application is memory-constrained, this isn’t the way to go.
Moreover, we usually want to filter the permutations to keep only some of them. So, at any time of execution, we’ll be processing a single -tuple, which is why there’s no need for storing all the permutations. It would be best if we could do something like this:
- ask for a permutation
- process it and go to the previous step to ask for another
-tuple
In that case, we’d need only space, which is a significant reduction compared to the recursive algorithm whose memory complexity grows exponentially with
. We can achieve that with an algorithm that receives a permutation as its input and returns the next permutation in the lexicographic order. Let’s name that algorithm
. Then, we start with the first permutation, and then, by calling
, we iteratively generate and process all the other permutations:
algorithm IterativeAlgorithmForPermutationsWithRepetition(n, k):
// INPUT
// n = the set cardinality
// k = the length of permutations with repetition
// OUTPUT
// All the k-tuples of the set {1,2,...,n} are processed
x <- [1, 1, ..., 1] // make the first permutation
while x != failure:
Process x
x <- Next(x, n, k)
So, should return the next permutation if there is one, and
if we ask it for the last
-tuple’s successor. But, how do we implement
?
4.1. Generating the Next Permutation
This is the main idea. A permutation and its successor
will have a common prefix. For example, with
,
is the successor of
, and their common prefix is
. The suffix that is different starts at the rightmost
lower than
. In this example, that was 4 in
. We increment it by one and get
.
However, this doesn’t cover all the cases. What if there’s an in the suffix we’re about to change? Let’s consider
. The rightmost element lower than
is 3. But, if we increment by 1, we get
, whereas the actual successor is
. So, what’s the correct logic?
The solution is to realize that the operation we should perform on every after
is the circular increment. We increase by one all the numbers except
, which we change to 1. But, if all the numbers are equal to
, then there’s no successor. It’s the last permutation, so we should return
to stop the caller’s loop.
4.2. Pseudocode
Here’s the pseudocode of :
algorithm Next(x, n, k):
// INPUT
// x = the k-tuple of {1,2,...,n} to find the successor of
// n = the set cardinality
// k = the tuples' length
// OUTPUT
// The lexicographic successor of x if it exists; failure if x is the last permutation.
// Find the suffix to change.
j <- k
while j >= 1 and x[j] = n:
j <- j - 1
if j = 0:
return failure // x was the last permutation
// Change the suffix.
for ℓ <- j to k:
if x[ℓ] = n:
x[ℓ] <- 1
else:
x[ℓ] <- x[ℓ] + 1
return x
As we see, apart from the auxiliary variables, doesn’t require more than
memory to find the input’s successor.
4.3. Example
Let’s work out an example. Let’s say that and
.
First, we determine where the suffix to change starts. The rightmost element lower than 7 is 2, so the suffix to change is . Next, we increment 2 by 1 to get 3 and replace all sevens with ones. The permutation we get is
, which is the correct result.
It’s interesting to note that if we used as
instead of
,
would amount to incrementing by 1 modulo
.
5. Conclusion
In this article, we presented two ways to generate all the permutations with repetition of the set . The recursive algorithm returns all the permutations at once, whereas the iterative method processes them one by one and has a lower space complexity.