Skip to content

Commit

Permalink
Refactor "Permutations" into an iterator method
Browse files Browse the repository at this point in the history
  • Loading branch information
atifaziz committed Nov 27, 2023
1 parent 4272b2b commit 0e154ef
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 211 deletions.
9 changes: 8 additions & 1 deletion MoreLinq.Test/PermutationsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,14 @@ public void TestPermutationsAreIndependent()
var permutedSets = set.Permutations();

var listPermutations = new List<IList<int>>();
listPermutations.AddRange(permutedSets);
foreach (var ps in permutedSets)
{
Assert.That(ps, Is.Not.All.Negative);
listPermutations.Add(ps);
for (var i = 0; i < ps.Count; i++)
ps[i] = -1;
}

Assert.That(listPermutations, Is.Not.Empty);

for (var i = 0; i < listPermutations.Count; i++)
Expand Down
1 change: 0 additions & 1 deletion MoreLinq/Extensions.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4644,7 +4644,6 @@ public static TResult Partition<TKey, TElement, TResult>(this IEnumerable<IGroup
[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
public static partial class PermutationsExtension
{

/// <summary>
/// Generates a sequence of lists that represent the permutations of the original sequence.
/// </summary>
Expand Down
52 changes: 0 additions & 52 deletions MoreLinq/NestedLoops.cs

This file was deleted.

243 changes: 86 additions & 157 deletions MoreLinq/Permutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,165 +18,11 @@
namespace MoreLinq
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

public static partial class MoreEnumerable
{
/// <summary>
/// The private implementation class that produces permutations of a sequence.
/// </summary>

sealed class PermutationEnumerator<T> : IEnumerator<IList<T>>
{
// NOTE: The algorithm used to generate permutations uses the fact that any set
// can be put into 1-to-1 correspondence with the set of ordinals number (0..n).
// The implementation here is based on the algorithm described by Kenneth H. Rosen,
// in Discrete Mathematics and Its Applications, 2nd edition, pp. 282-284.
//
// There are two significant changes from the original implementation.
// First, the algorithm uses lazy evaluation and streaming to fit well into the
// nature of most LINQ evaluations.
//
// Second, the algorithm has been modified to use dynamically generated nested loop
// state machines, rather than an integral computation of the factorial function
// to determine when to terminate. The original algorithm required a priori knowledge
// of the number of iterations necessary to produce all permutations. This is a
// necessary step to avoid overflowing the range of the permutation arrays used.
// The number of permutation iterations is defined as the factorial of the original
// set size minus 1.
//
// However, there's a fly in the ointment. The factorial function grows VERY rapidly.
// 13! overflows the range of a Int32; while 28! overflows the range of decimal.
// To overcome these limitations, the algorithm relies on the fact that the factorial
// of N is equivalent to the evaluation of N-1 nested loops. Unfortunately, you can't
// just code up a variable number of nested loops ... this is where .NET generators
// with their elegant 'yield return' syntax come to the rescue.
//
// The methods of the Loop extension class (For and NestedLoops) provide the implementation
// of dynamic nested loops using generators and sequence composition. In a nutshell,
// the two Repeat() functions are the constructor of loops and nested loops, respectively.
// The NestedLoops() function produces a composition of loops where the loop counter
// for each nesting level is defined in a separate sequence passed in the call.
//
// For example: NestedLoops( () => DoSomething(), new[] { 6, 8 } )
//
// is equivalent to: for( int i = 0; i < 6; i++ )
// for( int j = 0; j < 8; j++ )
// DoSomething();

readonly IList<T> valueSet;
readonly int[] permutation;
readonly IEnumerable<Action> generator;

IEnumerator<Action> generatorIterator;
bool hasMoreResults;

IList<T>? current;

public PermutationEnumerator(IEnumerable<T> valueSet)
{
this.valueSet = valueSet.ToArray();
this.permutation = new int[this.valueSet.Count];
// The nested loop construction below takes into account the fact that:
// 1) for empty sets and sets of cardinality 1, there exists only a single permutation.
// 2) for sets larger than 1 element, the number of nested loops needed is: set.Count-1
this.generator = NestedLoops(NextPermutation, Generate(2UL, n => n + 1).Take(Math.Max(0, this.valueSet.Count - 1)));
Reset();
}

[MemberNotNull(nameof(generatorIterator))]
public void Reset()
{
this.current = null;
this.generatorIterator?.Dispose();
// restore lexographic ordering of the permutation indexes
for (var i = 0; i < this.permutation.Length; i++)
this.permutation[i] = i;
// start a new iteration over the nested loop generator
this.generatorIterator = this.generator.GetEnumerator();
// we must advance the nested loop iterator to the initial element,
// this ensures that we only ever produce N!-1 calls to NextPermutation()
_ = this.generatorIterator.MoveNext();
this.hasMoreResults = true; // there's always at least one permutation: the original set itself
}

public IList<T> Current
{
get
{
Debug.Assert(this.current is not null);
return this.current;
}
}

object IEnumerator.Current => Current;

public bool MoveNext()
{
this.current = PermuteValueSet();
// check if more permutation left to enumerate
var prevResult = this.hasMoreResults;
this.hasMoreResults = this.generatorIterator.MoveNext();
if (this.hasMoreResults)
this.generatorIterator.Current(); // produce the next permutation ordering
// we return prevResult rather than m_HasMoreResults because there is always
// at least one permutation: the original set. Also, this provides a simple way
// to deal with the disparity between sets that have only one loop level (size 0-2)
// and those that have two or more (size > 2).
return prevResult;
}

void IDisposable.Dispose() => this.generatorIterator.Dispose();

/// <summary>
/// Transposes elements in the cached permutation array to produce the next permutation
/// </summary>
void NextPermutation()
{
// find the largest index j with m_Permutation[j] < m_Permutation[j+1]
var j = this.permutation.Length - 2;
while (this.permutation[j] > this.permutation[j + 1])
j--;

// find index k such that m_Permutation[k] is the smallest integer
// greater than m_Permutation[j] to the right of m_Permutation[j]
var k = this.permutation.Length - 1;
while (this.permutation[j] > this.permutation[k])
k--;

(this.permutation[j], this.permutation[k]) = (this.permutation[k], this.permutation[j]);

// move the tail of the permutation after the jth position in increasing order
for (int x = this.permutation.Length - 1, y = j + 1; x > y; x--, y++)
(this.permutation[x], this.permutation[y]) = (this.permutation[y], this.permutation[x]);
}

/// <summary>
/// Creates a new list containing the values from the original
/// set in their new permuted order.
/// </summary>
/// <remarks>
/// The reason we return a new permuted value set, rather than reuse
/// an existing collection, is that we have no control over what the
/// consumer will do with the results produced. They could very easily
/// generate and store a set of permutations and only then begin to
/// process them. If we reused the same collection, the caller would
/// be surprised to discover that all of the permutations looked the
/// same.
/// </remarks>
/// <returns>Array of permuted source sequence values</returns>
T[] PermuteValueSet()
{
var permutedSet = new T[this.permutation.Length];
for (var i = 0; i < this.permutation.Length; i++)
permutedSet[i] = this.valueSet[this.permutation[i]];
return permutedSet;
}
}

/// <summary>
/// Generates a sequence of lists that represent the permutations of the original sequence.
/// </summary>
Expand Down Expand Up @@ -205,10 +51,93 @@ public static IEnumerable<IList<T>> Permutations<T>(this IEnumerable<T> sequence

return _(); IEnumerable<IList<T>> _()
{
using var iter = new PermutationEnumerator<T>(sequence);
// The algorithm used to generate permutations uses the fact that any set can be put
// into 1-to-1 correspondence with the set of ordinals number (0..n). The
// implementation here is based on the algorithm described by Kenneth H. Rosen, in
// Discrete Mathematics and Its Applications, 2nd edition, pp. 282-284.

var valueSet = sequence.ToArray();

// There's always at least one permutation: a copy of original set.

yield return (IList<T>)valueSet.Clone();

// For empty sets and sets of cardinality 1, there exists only a single permutation.

if (valueSet.Length is 0 or 1)
yield break;

var permutation = new int[valueSet.Length];

// Initialize lexographic ordering of the permutation indexes.

for (var i = 0; i < permutation.Length; i++)
permutation[i] = i;

// For sets larger than 1 element, the number of nested loops needed is one less
// than the set length. Note that the factorial grows VERY rapidly such that 13!
// overflows the range of an Int32 and 28! overflows the range of a Decimal.

ulong factorial = valueSet.Length switch
{
0 => 1,
1 => 1,

Check warning on line 84 in MoreLinq/Permutations.cs

View check run for this annotation

Codecov / codecov/patch

MoreLinq/Permutations.cs#L83-L84

Added lines #L83 - L84 were not covered by tests
2 => 2,
3 => 6,
4 => 24,
5 => 120,
6 => 720,
7 => 5_040,
8 => 40_320,
9 => 362_880,
10 => 3_628_800,
11 => 39_916_800,
12 => 479_001_600,
13 => 6_227_020_800,
14 => 87_178_291_200,
15 => 1_307_674_368_000,
16 => 20_922_789_888_000,
17 => 355_687_428_096_000,
18 => 6_402_373_705_728_000,
19 => 121_645_100_408_832_000,
20 => 2_432_902_008_176_640_000,
_ => throw new OverflowException("Too many permutations."),

Check warning on line 104 in MoreLinq/Permutations.cs

View check run for this annotation

Codecov / codecov/patch

MoreLinq/Permutations.cs#L94-L104

Added lines #L94 - L104 were not covered by tests
};

for (var n = 1UL; n < factorial; n++)
{
// Transposes elements in the cached permutation array to produce the next
// permutation.

// Find the largest index j with permutation[j] < permutation[j+1]:

var j = permutation.Length - 2;
while (permutation[j] > permutation[j + 1])
j--;

// Find index k such that permutation[k] is the smallest integer greater than
// permutation[j] to the right of permutation[j]:

var k = permutation.Length - 1;
while (permutation[j] > permutation[k])
k--;

while (iter.MoveNext())
yield return iter.Current;
(permutation[j], permutation[k]) = (permutation[k], permutation[j]);

// Move the tail of the permutation after the j-th position in increasing order.

for (int x = permutation.Length - 1, y = j + 1; x > y; x--, y++)
(permutation[x], permutation[y]) = (permutation[y], permutation[x]);

// Yield a new array containing the values from the original set in their new
// permuted order.

var permutedSet = new T[permutation.Length];
for (var i = 0; i < permutation.Length; i++)
permutedSet[i] = valueSet[permutation[i]];

yield return permutedSet;
}
}
}
}
Expand Down

0 comments on commit 0e154ef

Please sign in to comment.