diff --git a/Guides/Permutations.md b/Guides/Permutations.md index ccfbd271..018135a7 100644 --- a/Guides/Permutations.md +++ b/Guides/Permutations.md @@ -60,6 +60,31 @@ for perm in numbers2.permutations() { // [10, 10, 20] ``` +Given a range, the `permutations(ofCount:)` method returns a sequence of all the different permutations of the given sizes of a collection’s elements in increasing order of size. + +```swift +let numbers = [10, 20, 30] +for perm in numbers.permutations(ofCount: 0...) { + print(perm) +} +// [] +// [10] +// [20] +// [30] +// [10, 20] +// [10, 30] +// [20, 10] +// [20, 30] +// [30, 10] +// [30, 20] +// [10, 20, 30] +// [10, 30, 20] +// [20, 10, 30] +// [20, 30, 10] +// [30, 10, 20] +// [30, 20, 10] +``` + ## Detailed Design The `permutations(ofCount:)` method is declared as a `Collection` extension, diff --git a/Sources/Algorithms/Permutations.swift b/Sources/Algorithms/Permutations.swift index 937f63cb..94ad2890 100644 --- a/Sources/Algorithms/Permutations.swift +++ b/Sources/Algorithms/Permutations.swift @@ -11,62 +11,93 @@ /// A sequence of all the permutations of a collection's elements. public struct Permutations { - /// The base collection. + /// The base collection to iterate over for permutations. public let base: Base + @usableFromInline internal let baseCount: Int - internal let countToChoose: Int + /// The range of accepted sizes of permutations. + /// - Note: This may be empty if the attempted range entirely exceeds the + /// bounds of the size of the `base` collection. + @usableFromInline + internal let kRange: Range + + /// Initializes a `Permutations` for all permutations of `base` of size `k`. + /// - Parameters: + /// - base: The collection to iterate over for permutations + /// - k: The expected size of each permutation, or `nil` (default) to + /// iterate over all permutations of the same size as the base collection. + @usableFromInline internal init(_ base: Base, k: Int? = nil) { + let kRange: ClosedRange? + if let countToChoose = k { + kRange = countToChoose ... countToChoose + } else { + kRange = nil + } + self.init(base, kRange: kRange) + } + + /// Initializes a `Permutations` for all combinations of `base` of sizes + /// within a given range. + /// - Parameters: + /// - base: The collection to iterate over for permutations. + /// - kRange: The range of accepted sizes of permutations, or `nil` to + /// iterate over all permutations of the same size as the base collection. + @usableFromInline + internal init( + _ base: Base, kRange: R? + ) where R.Bound == Int { self.base = base let baseCount = base.count self.baseCount = baseCount - self.countToChoose = k ?? baseCount + let upperBound = baseCount + 1 + self.kRange = kRange?.relative(to: 0 ..< .max) + .clamped(to: 0 ..< upperBound) ?? + baseCount ..< upperBound } + /// The total number of permutations. + @inlinable public var count: Int { - return baseCount >= countToChoose - ? stride(from: baseCount, to: baseCount - countToChoose, by: -1).reduce(1, *) - : 0 + return kRange.map { + stride(from: baseCount, to: baseCount - $0, by: -1).reduce(1, *) + }.reduce(0, +) } } - + extension Permutations: Sequence { /// The iterator for a `Permutations` instance. public struct Iterator: IteratorProtocol { @usableFromInline internal var base: Base + @usableFromInline - internal var indexes: [Base.Index] - @usableFromInline - internal var hasMorePermutations: Bool + internal let baseCount: Int + + /// The current range of accepted sizes of permutations. + /// - Note: The range is contracted until empty while iterating over + /// permutations of different sizes. When the range is empty, iteration is + /// finished. @usableFromInline - internal var countToChoose: Int = 0 - - /// `true` if we're generating permutations of the full collection. + internal var kRange: Range + + /// Whether or not iteration is finished (`kRange` is empty) @usableFromInline - internal var permutesFullCollection: Bool { - countToChoose == indexes.count + internal var isFinished: Bool { + return kRange.isEmpty } @usableFromInline - internal init(_ base: Base) { - self.base = base - self.indexes = Array(base.indices) - self.countToChoose = self.indexes.count - self.hasMorePermutations = true - } + internal var indexes: [Base.Index] @usableFromInline - internal init(_ base: Base, count: Int) { - self.base = base - self.countToChoose = count - - // Produce exactly one empty permutation when `count == 0`. - self.indexes = count == 0 ? [] : Array(base.indices) - - // Can't produce any permutations when `count > base.count`. - self.hasMorePermutations = count <= indexes.count + internal init(_ permutations: Permutations) { + self.base = permutations.base + self.baseCount = permutations.baseCount + self.kRange = permutations.kRange + self.indexes = Array(permutations.base.indices) } /// Advances the `indexes` array such that the first `countToChoose` @@ -82,29 +113,30 @@ extension Permutations: Sequence { /// - Complexity: O(*n*), where *n* is the length of the collection. @usableFromInline internal mutating func nextState() -> Bool { + let countToChoose = self.kRange.lowerBound let edge = countToChoose - 1 - + // Find first index greater than the one at `edge`. if let i = indexes[countToChoose...].firstIndex(where: { indexes[edge] < $0 }) { indexes.swapAt(edge, i) } else { - indexes.reverse(subrange: countToChoose..= 0 && indexes[lastAscent] >= indexes[lastAscent + 1]) { lastAscent -= 1 } - if (lastAscent < 0) { + if lastAscent < 0 { return false } - + // Find rightmost index less than that at `lastAscent`. if let i = indexes[lastAscent...].lastIndex(where: { indexes[lastAscent] < $0 }) { indexes.swapAt(lastAscent, i) } - indexes.reverse(subrange: (lastAscent + 1).. [Base.Element]? { - if !hasMorePermutations { return nil } + guard !isFinished else { return nil } + + /// Advances `kRange` by incrementing its `lowerBound` until the range is + /// empty, when iteration is finished. + func advanceKRange() { + kRange.removeFirst() + indexes = Array(base.indices) + } + + let countToChoose = self.kRange.lowerBound + if countToChoose == 0 { + defer { + advanceKRange() + } + return [] + } + let permutesFullCollection = (countToChoose == baseCount) if permutesFullCollection { // If we're permuting the full collection, each iteration is just a // call to `nextPermutation` on `indexes`. - defer { hasMorePermutations = indexes.nextPermutation() } + defer { + let hasMorePermutations = indexes.nextPermutation() + if !hasMorePermutations { + advanceKRange() + } + } return indexes.map { base[$0] } } else { // Otherwise, return the items at the first `countToChoose` indices and // advance the state. - defer { hasMorePermutations = nextState() } + defer { + let hasMorePermutations = nextState() + if !hasMorePermutations { + advanceKRange() + } + } return indexes.prefix(countToChoose).map { base[$0] } } } } - @usableFromInline - internal var permutesFullCollection: Bool { - baseCount == countToChoose - } - public func makeIterator() -> Iterator { - permutesFullCollection - ? Iterator(base) - : Iterator(base, count: countToChoose) + Iterator(self) } } extension Permutations: LazySequenceProtocol where Base: LazySequenceProtocol {} //===----------------------------------------------------------------------===// -// nextPermutation(by:) +// nextPermutation() //===----------------------------------------------------------------------===// extension MutableCollection - where Self: BidirectionalCollection, Element: Comparable +where Self: BidirectionalCollection, Element: Comparable { /// Permutes this collection's elements through all the lexical orderings. /// @@ -163,8 +214,8 @@ extension MutableCollection /// - Complexity: O(*n*), where *n* is the length of the collection. @usableFromInline internal mutating func nextPermutation() -> Bool { - // ensure we have > 1 element in the collection - if isEmpty { return false } + // Ensure we have > 1 element in the collection. + guard !isEmpty else { return false } var i = index(before: endIndex) if i == startIndex { return false } @@ -178,7 +229,7 @@ extension MutableCollection formIndex(before: &j) } swapAt(i, j) - self.reverse(subrange: ip1..( + ofCount kRange: R + ) -> Permutations where R.Bound == Int { + return Permutations(self, kRange: kRange) + } + /// Returns a collection of the permutations of this collection of the /// specified length. /// @@ -237,10 +344,13 @@ extension Collection { /// sequence, the resulting sequence has no elements. /// /// - Parameter k: The number of elements to include in each permutation. - /// If `count` is `nil`, the resulting sequence represents permutations - /// of this entire collection. + /// If `k` is `nil`, the resulting sequence represents permutations of this + /// entire collection. /// - /// - Complexity: O(1) + /// - Complexity: O(1) for random-access base collections. O(*n*) where *n* + /// is the number of elements in the base collection, since `Permutations` + /// accesses the `count` of the base collection. + @inlinable public func permutations(ofCount k: Int? = nil) -> Permutations { precondition( k ?? 0 >= 0, diff --git a/Tests/SwiftAlgorithmsTests/PermutationsTests.swift b/Tests/SwiftAlgorithmsTests/PermutationsTests.swift index 416a3e4b..939378bc 100644 --- a/Tests/SwiftAlgorithmsTests/PermutationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/PermutationsTests.swift @@ -26,27 +26,58 @@ final class PermutationsTests: XCTestCase { XCTAssertTrue(p.allSatisfy { $0.count == count }) XCTAssertTrue(p.isSorted(by: { $0.lexicographicallyPrecedes($1) })) } - + + func ts(count: R) where R.Bound == Int { + let p = count.relative(to: 0 ..< .max) + .clamped(to: 0 ..< c.count + 1) + .flatMap { + c.permutations(ofCount: $0) + } + + let p2 = c.permutations(ofCount: count) + + XCTAssertEqual(p.count, p2.count) + XCTAssertEqualSequences(p, Array(p2)) + } + + t(count: 0) t(count: 1) t(count: 2) t(count: 3) t(count: 4) t(count: 5) t(count: nil) + + ts(count: 0...0) + ts(count: 1...1) + ts(count: 0...1) + ts(count: 0...2) + ts(count: 0...3) + ts(count: 1...3) + ts(count: 2...3) + ts(count: 0...) + ts(count: ...5) + ts(count: ...4) + ts(count: ...6) } func testEmpty() { // `k == 0` results in one zero-length permutation XCTAssertEqual(1, "".permutations().count) XCTAssertEqual(1, "ABCD".permutations(ofCount: 0).count) + XCTAssertEqual(1, "ABCD".permutations(ofCount: 0...0).count) XCTAssertEqual(Array("".permutations()), [[]]) XCTAssertEqual(Array("".permutations(ofCount: 0)), [[]]) XCTAssertEqual(Array("ABCD".permutations(ofCount: 0)), [[]]) + XCTAssertEqual(Array("ABCD".permutations(ofCount: 0...0)), [[]]) // `k` greater than element count results in zero permutations XCTAssertEqual(0, "".permutations(ofCount: 5).count) XCTAssertEqual(Array("".permutations(ofCount: 5)), []) XCTAssertEqual(Array("ABCD".permutations(ofCount: 5)), []) + XCTAssertEqual(Array("ABCD".permutations(ofCount: 5..<6)), []) + XCTAssertEqual(Array("ABCD".permutations(ofCount: 5..<7)), []) + XCTAssertEqual(Array("ABCD".permutations(ofCount: 5...)), []) } func testNextPermutation() { @@ -64,7 +95,7 @@ final class PermutationsTests: XCTestCase { XCTAssertEqual([1, 2, 3, 4, 7, 6, 5], numbers) _ = numbers.nextPermutation() XCTAssertEqual([1, 2, 3, 5, 4, 6, 7], numbers) - + // Fast-forward to end of permutations. while numbers.nextPermutation() {} XCTAssertEqual([1, 2, 3, 4, 5, 6, 7], numbers) @@ -73,4 +104,101 @@ final class PermutationsTests: XCTestCase { func testPermutationsLazy() { XCTAssertLazySequence("ABCD".lazy.permutations(ofCount: 2)) } + + func testDocumentationExample1() { + // From Guides/Permutations.md + let numbers = [10, 20, 30] + let permutations = numbers.permutations() + XCTAssertEqualSequences(permutations, [ + [10, 20, 30], + [10, 30, 20], + [20, 10, 30], + [20, 30, 10], + [30, 10, 20], + [30, 20, 10], + ]) + } + + func testDocumentationExample2() { + // From Guides/Permutations.md + let numbers = [10, 20, 30] + let permutations = numbers.permutations(ofCount: 2) + XCTAssertEqualSequences(permutations, [ + [10, 20], + [10, 30], + [20, 10], + [20, 30], + [30, 10], + [30, 20], + ]) + } + + func testDocumentationExample3() { + // From Guides/Permutations.md + let numbers2 = [20, 10, 10] + let permutations = numbers2.permutations() + XCTAssertEqualSequences(permutations, [ + [20, 10, 10], + [20, 10, 10], + [10, 20, 10], + [10, 10, 20], + [10, 20, 10], + [10, 10, 20], + ]) + } + + func testDocumentationExample4() { + // From Guides/Permutations.md + let numbers = [10, 20, 30] + let permutations = numbers.permutations(ofCount: 0...) + XCTAssertEqualSequences(permutations, [ + [], + [10], + [20], + [30], + [10, 20], + [10, 30], + [20, 10], + [20, 30], + [30, 10], + [30, 20], + [10, 20, 30], + [10, 30, 20], + [20, 10, 30], + [20, 30, 10], + [30, 10, 20], + [30, 20, 10], + ]) + } + + func testDocumentationExample5() { + // From Permutations.swift + let names = ["Alex", "Celeste", "Davide"] + let permutations = names.permutations(ofCount: 2) + XCTAssertEqualSequences(permutations, [ + ["Alex", "Celeste"], + ["Alex", "Davide"], + ["Celeste", "Alex"], + ["Celeste", "Davide"], + ["Davide", "Alex"], + ["Davide", "Celeste"], + ]) + } + + func testDocumentationExample6() { + // From Permutations.swift + let names = ["Alex", "Celeste", "Davide"] + let permutations = names.permutations(ofCount: 1...2) + XCTAssertEqualSequences(permutations, [ + ["Alex"], + ["Celeste"], + ["Davide"], + ["Alex", "Celeste"], + ["Alex", "Davide"], + ["Celeste", "Alex"], + ["Celeste", "Davide"], + ["Davide", "Alex"], + ["Davide", "Celeste"], + ]) + } }