From 599717827a742080d5893e0fa59907c452de9843 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 18:55:50 -0500 Subject: [PATCH 01/21] =?UTF-8?q?Add=20tests=20for=20`Combination`?= =?UTF-8?q?=E2=80=99s=20`count`=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CombinationsTests.swift | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index 4079d6d7..d0819242 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -13,18 +13,37 @@ import XCTest import Algorithms final class CombinationsTests: XCTestCase { + func testCount() { + let c = "ABCD" + + let c0 = c.combinations(ofCount: 0).count + XCTAssertEqual(c0, 1) + + let c1 = c.combinations(ofCount: 1).count + XCTAssertEqual(c1, 4) + + let c2 = c.combinations(ofCount: 2).count + XCTAssertEqual(c2, 6) + + let c3 = c.combinations(ofCount: 3).count + XCTAssertEqual(c3, 4) + + let c4 = c.combinations(ofCount: 4).count + XCTAssertEqual(c4, 1) + } + func testCombinations() { let c = "ABCD" - + let c1 = c.combinations(ofCount: 1) XCTAssertEqual(["A", "B", "C", "D"], c1.map { String($0) }) - + let c2 = c.combinations(ofCount: 2) XCTAssertEqual(["AB", "AC", "AD", "BC", "BD", "CD"], c2.map { String($0) }) - + let c3 = c.combinations(ofCount: 3) XCTAssertEqual(["ABC", "ABD", "ACD", "BCD"], c3.map { String($0) }) - + let c4 = c.combinations(ofCount: 4) XCTAssertEqual(["ABCD"], c4.map { String($0) }) } @@ -33,7 +52,7 @@ final class CombinationsTests: XCTestCase { // `k == 0` results in one zero-length combination XCTAssertEqualSequences([[]], "".combinations(ofCount: 0)) XCTAssertEqualSequences([[]], "ABCD".combinations(ofCount: 0)) - + // `k` greater than element count results in zero combinations XCTAssertEqualSequences([], "".combinations(ofCount: 5)) XCTAssertEqualSequences([], "ABCD".combinations(ofCount: 5)) From a4c5e8c17eb153a68c73b571b0798309b5ec8ec9 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 18:56:09 -0500 Subject: [PATCH 02/21] =?UTF-8?q?Document=20`Combinations`=E2=80=99s=20`co?= =?UTF-8?q?unt`=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Algorithms/Combinations.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 42dde9cb..e12e1774 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -22,7 +22,8 @@ public struct Combinations { self.base = base self.k = base.count < k ? -1 : k } - + + /// The total number of combinations. @inlinable public var count: Int { func binomial(n: Int, k: Int) -> Int { From cd9d372287e4b38b58508791ffdf120d7079ef53 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 18:56:53 -0500 Subject: [PATCH 03/21] =?UTF-8?q?Make=20`Combinations`=E2=80=99s=20=20`k`?= =?UTF-8?q?=20a=20`let`=20instead=20of=20a=20`var`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `k` is `internal` only and unmodified --- Sources/Algorithms/Combinations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index e12e1774..cbe802ec 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -15,7 +15,7 @@ public struct Combinations { public let base: Base @usableFromInline - internal var k: Int + internal let k: Int @usableFromInline internal init(_ base: Base, k: Int) { From 88bd1db5e8e5cfbf4630fcb8cc559f2b46e23b18 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 19:55:53 -0500 Subject: [PATCH 04/21] Correct function signature in comment --- Sources/Algorithms/Combinations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index cbe802ec..8f4a5aa1 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -130,7 +130,7 @@ extension Combinations: Equatable where Base: Equatable {} extension Combinations: Hashable where Base: Hashable {} //===----------------------------------------------------------------------===// -// combinations(count:) +// combinations(ofCount:) //===----------------------------------------------------------------------===// extension Collection { From cdc3d25c1b4a58bbe16b35dcee92b19856c0999c Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 20:19:48 -0500 Subject: [PATCH 05/21] Add ability to iterate through `Combinations` of all sizes --- Sources/Algorithms/Combinations.swift | 155 +++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 14 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 8f4a5aa1..f6dcc813 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -14,18 +14,49 @@ public struct Combinations { /// The collection to iterate over for combinations. public let base: Base + /// The range of accepted sizes of combinations. + /// - Note: This may be `nil` if the attempted range entirely exceeds the + /// upper bounds of the size of the `base` collection. @usableFromInline - internal let k: Int + internal let k: ClosedRange? + /// Initializes a `Combinations` for all combinations of `base` of all sizes. + /// - Parameter base: The collection to iterate over for combinations. + @usableFromInline + internal init(_ base: Base) { + self.init(base, k: 0...base.count) + } + + /// Initializes a `Combinations` for all combinations of `base` of size `k`. + /// - Parameters: + /// - base: The collection to iterate over for combinations. + /// - k: The expected size of each combination. @usableFromInline internal init(_ base: Base, k: Int) { + self.init(base, k: k...k) + } + + /// Initializes a `Combinations` for all combinations of `base` of sizes + /// within a given range. + /// - Parameters: + /// - base: The collection to iterate over for combinations. + /// - k: The range of accepted sizes of combinations. + @usableFromInline + internal init(_ base: Base, k: ClosedRange) { + assert(k.lowerBound >= 0, "Can't have combinations with a negative number of elements.") self.base = base - self.k = base.count < k ? -1 : k + self.k = (k.lowerBound <= base.count) ? k.clamped(to: 0...base.count) : nil } /// The total number of combinations. @inlinable public var count: Int { + guard let k = self.k else { return 0 } + let n = base.count + if k == 0...n { + return 1 << n + } + func binomial(n: Int, k: Int) -> Int { switch k { case n, 0: return 1 @@ -35,9 +66,9 @@ public struct Combinations { } } - return k >= 0 - ? binomial(n: base.count, k: k) - : 0 + return k.map { + binomial(n: n, k: $0) + }.reduce(0, +) } } @@ -47,6 +78,10 @@ extension Combinations: Sequence { @usableFromInline internal let base: Base + /// The current range of accepted sizes of combinations. + @usableFromInline + internal var k: ClosedRange + @usableFromInline internal var indexes: [Base.Index] @@ -55,10 +90,9 @@ extension Combinations: Sequence { internal init(_ combinations: Combinations) { self.base = combinations.base - self.finished = combinations.k < 0 - self.indexes = combinations.k < 0 - ? [] - : Array(combinations.base.indices.prefix(combinations.k)) + self.k = combinations.k ?? 0...0 + self.indexes = Array(combinations.base.indices.prefix(k.upperBound)) + self.finished = (combinations.k == nil) } /// Advances the current indices to the next set of combinations. If @@ -88,17 +122,22 @@ extension Combinations: Sequence { finished = true return } - + let i = indexes.count - 1 base.formIndex(after: &indexes[i]) if indexes[i] != base.endIndex { return } - + var j = i while indexes[i] == base.endIndex { j -= 1 guard j >= 0 else { - // Finished iterating over combinations - finished = true + // Finished iterating over combinations of this size. + if k.lowerBound < k.upperBound { + k = k.lowerBound...k.upperBound.advanced(by: -1) + self.indexes = Array(base.indices.prefix(k.upperBound)) + } else { + finished = true + } return } @@ -129,6 +168,95 @@ extension Combinations: LazySequenceProtocol where Base: LazySequenceProtocol {} extension Combinations: Equatable where Base: Equatable {} extension Combinations: Hashable where Base: Hashable {} +//===----------------------------------------------------------------------===// +// combinations() +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns a collection of all the combinations of this collection's + /// elements, including the full collection and an empty collection + /// + /// This example prints the all the combinations from an array of letters: + /// + /// let letters = ["A", "B", "C", "D"] + /// for combo in letters.combinations() { + /// print(combo.joined(separator: ", ")) + /// } + /// // A, B, C, D + /// // A, B, C + /// // A, B, D + /// // A, C, D + /// // B, C, D + /// // A, B + /// // A, C + /// // A, D + /// // B, C + /// // B, D + /// // C, D + /// // A + /// // B + /// // C + /// // D + /// + /// The returned collection presents combinations in a consistent order, where + /// the indices in each combination are in ascending lexicographical order. + /// That is, in the example above, the combinations in order are the elements + /// at `[0, 1, 2, 3]`, `[0, 1, 2]`, `[0, 1, 3]`, `[0, 2, 3]`, `[1, 2, 3]`, … + /// `[0]`, `[1]`, `[2]`, `[3]`, `[]`. + /// + /// - Complexity: O(1) + @inlinable + public func combinations() -> Combinations { + return Combinations(self) + } +} + +//===----------------------------------------------------------------------===// +// combinations(ofCounts:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns a collection of combinations of this collection's elements, with + /// each combination having the specified number of elements. + /// + /// This example prints the different combinations of 1 and 2 from an array of + /// four colors: + /// + /// let colors = ["fuchsia", "cyan", "mauve", "magenta"] + /// for combo in colors.combinations(ofCounts: 1...2) { + /// print(combo.joined(separator: ", ")) + /// } + /// // fuchsia, cyan + /// // fuchsia, mauve + /// // fuchsia, magenta + /// // cyan, mauve + /// // cyan, magenta + /// // mauve, magenta + /// // fuchsia + /// // cyan + /// // mauve + /// // magenta + /// + /// The returned collection presents combinations in a consistent order, where + /// the indices in each combination are in ascending lexicographical order. + /// That is, in the example above, the combinations in order are the elements + /// at `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, `[1, 3]`, `[2, 3]`, `[0]`, + /// `[1]`, `[2]`, and finally `[3]`. + /// + /// If `k` is `0...0`, the resulting sequence has exactly one element, an + /// empty array. If `k.upperBound` is greater than the number of elements in + /// this sequence, the resulting sequence has no elements. + /// + /// - Parameter k: The range of numbers of elements to include in each + /// combination. + /// + /// - Complexity: O(1) + @inlinable + public func combinations(ofCounts k: ClosedRange) -> Combinations { + return Combinations(self, k: k) + } +} + //===----------------------------------------------------------------------===// // combinations(ofCount:) //===----------------------------------------------------------------------===// @@ -163,7 +291,6 @@ extension Collection { /// - Complexity: O(1) @inlinable public func combinations(ofCount k: Int) -> Combinations { - assert(k >= 0, "Can't have combinations with a negative number of elements.") return Combinations(self, k: k) } } From 0465522c2489f7e6411042335d09b2bd39531743 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 20:20:19 -0500 Subject: [PATCH 06/21] Add tests for `Combinations` with a range of accepted sizes --- .../CombinationsTests.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index d0819242..4498342c 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -30,6 +30,36 @@ final class CombinationsTests: XCTestCase { let c4 = c.combinations(ofCount: 4).count XCTAssertEqual(c4, 1) + + let c5 = c.combinations(ofCounts: 0...0).count + XCTAssertEqual(c5, 1) + + let c6 = c.combinations(ofCounts: 1...1).count + XCTAssertEqual(c6, 4) + + let c7 = c.combinations(ofCounts: 1...2).count + XCTAssertEqual(c7, 10) + + let c8 = c.combinations(ofCounts: 1...3).count + XCTAssertEqual(c8, 14) + + let c9 = c.combinations(ofCounts: 2...4).count + XCTAssertEqual(c9, 11) + + // `k` greater than element count results in same number of combinations + let c10 = c.combinations(ofCounts: 3...10).count + XCTAssertEqual(c10, 5) + + // `k` greater than element count results in same number of combinations + let c11 = c.combinations(ofCounts: 4...10).count + XCTAssertEqual(c11, 1) + + // `k` entirely greater than element count results in no combinations + let c12 = c.combinations(ofCounts: 5...10).count + XCTAssertEqual(c12, 0) + + let c13 = c.combinations().count + XCTAssertEqual(c13, 16) } func testCombinations() { @@ -46,19 +76,31 @@ final class CombinationsTests: XCTestCase { let c4 = c.combinations(ofCount: 4) XCTAssertEqual(["ABCD"], c4.map { String($0) }) + + let c5 = c.combinations(ofCounts: 2...4) + XCTAssertEqual(["ABCD", "ABC", "ABD", "ACD", "BCD", "AB", "AC", "AD", "BC", "BD", "CD"], c5.map { String($0) }) + + let c6 = c.combinations() + XCTAssertEqual(["ABCD", "ABC", "ABD", "ACD", "BCD", "AB", "AC", "AD", "BC", "BD", "CD", "A", "B", "C", "D", ""], c6.map { String($0) }) } func testEmpty() { // `k == 0` results in one zero-length combination XCTAssertEqualSequences([[]], "".combinations(ofCount: 0)) + XCTAssertEqualSequences([[]], "".combinations(ofCounts: 0...0)) XCTAssertEqualSequences([[]], "ABCD".combinations(ofCount: 0)) + XCTAssertEqualSequences([[]], "ABCD".combinations(ofCounts: 0...0)) // `k` greater than element count results in zero combinations XCTAssertEqualSequences([], "".combinations(ofCount: 5)) + XCTAssertEqualSequences([], "".combinations(ofCounts: 5...10)) XCTAssertEqualSequences([], "ABCD".combinations(ofCount: 5)) + XCTAssertEqualSequences([], "ABCD".combinations(ofCounts: 5...10)) } func testCombinationsLazy() { XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 1)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...3)) + XCTAssertLazySequence("ABC".lazy.combinations()) } } From 8ed61a68643c68d9319c8f71384da3087d98f123 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 20:46:52 -0500 Subject: [PATCH 07/21] Make `Combinations` iterate in increasing order of size This mirrors the order of the range --- Sources/Algorithms/Combinations.swift | 59 +++++++++++-------- .../CombinationsTests.swift | 6 +- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index f6dcc813..315cb541 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -91,7 +91,7 @@ extension Combinations: Sequence { internal init(_ combinations: Combinations) { self.base = combinations.base self.k = combinations.k ?? 0...0 - self.indexes = Array(combinations.base.indices.prefix(k.upperBound)) + self.indexes = Array(combinations.base.indices.prefix(k.lowerBound)) self.finished = (combinations.k == nil) } @@ -115,11 +115,21 @@ extension Combinations: Sequence { /// // so the iteration is finished. @usableFromInline internal mutating func advance() { + /// Advances `k` by increasing its `lowerBound` or finishes the iteration. + func advanceK() { + if k.lowerBound < k.upperBound { + k = k.lowerBound.advanced(by: 1)...k.upperBound + self.indexes = Array(base.indices.prefix(k.lowerBound)) + } else { + finished = true + } + } + guard !indexes.isEmpty else { // Initial state for combinations of 0 elements is an empty array with // `finished == false`. Even though no indexes are involved, advancing // from that state means we are finished with iterating. - finished = true + advanceK() return } @@ -132,12 +142,7 @@ extension Combinations: Sequence { j -= 1 guard j >= 0 else { // Finished iterating over combinations of this size. - if k.lowerBound < k.upperBound { - k = k.lowerBound...k.upperBound.advanced(by: -1) - self.indexes = Array(base.indices.prefix(k.upperBound)) - } else { - finished = true - } + advanceK() return } @@ -182,27 +187,29 @@ extension Collection { /// for combo in letters.combinations() { /// print(combo.joined(separator: ", ")) /// } - /// // A, B, C, D - /// // A, B, C - /// // A, B, D - /// // A, C, D - /// // B, C, D + /// // + /// // A + /// // B + /// // C + /// // D /// // A, B /// // A, C /// // A, D /// // B, C /// // B, D /// // C, D - /// // A - /// // B - /// // C - /// // D + /// // A, B, C + /// // A, B, D + /// // A, C, D + /// // B, C, D + /// // A, B, C, D /// /// The returned collection presents combinations in a consistent order, where - /// the indices in each combination are in ascending lexicographical order. + /// the indices in each combination are in ascending lexicographical order, + /// and the size of the combinations are in increasing order. /// That is, in the example above, the combinations in order are the elements - /// at `[0, 1, 2, 3]`, `[0, 1, 2]`, `[0, 1, 3]`, `[0, 2, 3]`, `[1, 2, 3]`, … - /// `[0]`, `[1]`, `[2]`, `[3]`, `[]`. + /// at `[]`, `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, + /// `[1, 2]`, `[1, 3]`, … `[0, 1, 2, 3]`. /// /// - Complexity: O(1) @inlinable @@ -226,22 +233,22 @@ extension Collection { /// for combo in colors.combinations(ofCounts: 1...2) { /// print(combo.joined(separator: ", ")) /// } + /// // fuchsia + /// // cyan + /// // mauve + /// // magenta /// // fuchsia, cyan /// // fuchsia, mauve /// // fuchsia, magenta /// // cyan, mauve /// // cyan, magenta /// // mauve, magenta - /// // fuchsia - /// // cyan - /// // mauve - /// // magenta /// /// The returned collection presents combinations in a consistent order, where /// the indices in each combination are in ascending lexicographical order. /// That is, in the example above, the combinations in order are the elements - /// at `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, `[1, 3]`, `[2, 3]`, `[0]`, - /// `[1]`, `[2]`, and finally `[3]`. + /// at `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, + /// `[1, 3]`, and finally `[2, 3]`. /// /// If `k` is `0...0`, the resulting sequence has exactly one element, an /// empty array. If `k.upperBound` is greater than the number of elements in diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index 4498342c..eb03c072 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -78,10 +78,10 @@ final class CombinationsTests: XCTestCase { XCTAssertEqual(["ABCD"], c4.map { String($0) }) let c5 = c.combinations(ofCounts: 2...4) - XCTAssertEqual(["ABCD", "ABC", "ABD", "ACD", "BCD", "AB", "AC", "AD", "BC", "BD", "CD"], c5.map { String($0) }) + XCTAssertEqual(["AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c5.map { String($0) }) - let c6 = c.combinations() - XCTAssertEqual(["ABCD", "ABC", "ABD", "ACD", "BCD", "AB", "AC", "AD", "BC", "BD", "CD", "A", "B", "C", "D", ""], c6.map { String($0) }) + let c6 = c.combinations(ofCounts: 0...4) + XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD","ABCD"], c6.map { String($0) }) } func testEmpty() { From 0618c7025e345a7d50f82bde2933e3b07f0d53cf Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Fri, 4 Dec 2020 23:29:51 -0500 Subject: [PATCH 08/21] Document additions to `Combinations`: `combinations(ofCounts:)` and `combinations()` --- Guides/Combinations.md | 46 ++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Guides/Combinations.md b/Guides/Combinations.md index 6f0e186a..57f41bf8 100644 --- a/Guides/Combinations.md +++ b/Guides/Combinations.md @@ -36,6 +36,52 @@ for combo in numbers2.combinations(ofCount: 2) { // [10, 10] ``` +The `combinations(ofCounts:)` method returns a sequence of all the different +combinations of the given sizes of a collection’s elements in increasing order of size. + +```swift +let numbers = [10, 20, 30, 40] +for combo in numbers(ofCounts: 2...3) { + print(combo) +} +// [10, 20] +// [10, 30] +// [10, 40] +// [20, 30] +// [20, 40] +// [30, 40] +// [10, 20, 30] +// [10, 20, 40] +// [10, 30, 40] +// [20, 30, 40] +``` + +The `combinations()` method returns a sequence of *all* the different +combinations of a collection’s elements in increasing order of size. + +```swift +let numbers = [10, 20, 30, 40] +for combo in numbers.combinations() { + print(combo) +} +// [] +// [10] +// [20] +// [30] +// [40] +// [10, 20] +// [10, 30] +// [10, 40] +// [20, 30] +// [20, 40] +// [30, 40] +// [10, 20, 30] +// [10, 20, 40] +// [10, 30, 40] +// [20, 30, 40] +// [10, 20, 30, 40] +``` + ## Detailed Design The `combinations(ofCount:)` method is declared as a `Collection` extension, diff --git a/README.md b/README.md index 9f07d3d3..bd785384 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Read more about the package, and the intent behind it, in the [announcement on s #### Combinations / permutations -- [`combinations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of a particular size of the elements in a collection. +- [`combinations(ofCount:)`, `combinations(ofCounts:)`, `combinations()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. - [`permutations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Permutations.md): Permutations of a particular size of the elements in a collection, or of the full collection. #### Mutating algorithms From 1abd5dd224eb30d7ee66ce495b69147604fd3411 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Mon, 14 Dec 2020 16:49:15 -0500 Subject: [PATCH 09/21] Add ability to use partial ranges, instead of just `ClosedRange`, for specifying combination sizes --- Sources/Algorithms/Combinations.swift | 31 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 315cb541..4532d669 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -18,13 +18,13 @@ public struct Combinations { /// - Note: This may be `nil` if the attempted range entirely exceeds the /// upper bounds of the size of the `base` collection. @usableFromInline - internal let k: ClosedRange? + internal let k: Range? /// Initializes a `Combinations` for all combinations of `base` of all sizes. /// - Parameter base: The collection to iterate over for combinations. @usableFromInline internal init(_ base: Base) { - self.init(base, k: 0...base.count) + self.init(base, k: 0...) } /// Initializes a `Combinations` for all combinations of `base` of size `k`. @@ -33,6 +33,7 @@ public struct Combinations { /// - k: The expected size of each combination. @usableFromInline internal init(_ base: Base, k: Int) { + assert(k >= 0, "Can't have combinations with a negative number of elements.") self.init(base, k: k...k) } @@ -42,10 +43,15 @@ public struct Combinations { /// - base: The collection to iterate over for combinations. /// - k: The range of accepted sizes of combinations. @usableFromInline - internal init(_ base: Base, k: ClosedRange) { - assert(k.lowerBound >= 0, "Can't have combinations with a negative number of elements.") + internal init( + _ base: Base, k: R + ) where R.Bound == Int { + let range = k.relative(to: R.Bound.zero.. { public var count: Int { guard let k = self.k else { return 0 } let n = base.count - if k == 0...n { + if k == 0..<(n + 1) { return 1 << n } @@ -80,7 +86,7 @@ extension Combinations: Sequence { /// The current range of accepted sizes of combinations. @usableFromInline - internal var k: ClosedRange + internal var k: Range @usableFromInline internal var indexes: [Base.Index] @@ -90,7 +96,7 @@ extension Combinations: Sequence { internal init(_ combinations: Combinations) { self.base = combinations.base - self.k = combinations.k ?? 0...0 + self.k = combinations.k ?? 0..<1 self.indexes = Array(combinations.base.indices.prefix(k.lowerBound)) self.finished = (combinations.k == nil) } @@ -117,8 +123,9 @@ extension Combinations: Sequence { internal mutating func advance() { /// Advances `k` by increasing its `lowerBound` or finishes the iteration. func advanceK() { - if k.lowerBound < k.upperBound { - k = k.lowerBound.advanced(by: 1)...k.upperBound + let advancedLowerBound = k.lowerBound.advanced(by: 1) + if advancedLowerBound < k.upperBound { + k = advancedLowerBound..) -> Combinations { + public func combinations( + ofCounts k: R + ) -> Combinations where R.Bound == Int { return Combinations(self, k: k) } } From dd2a1fa1f5400b741e7b5dbdb5f79f4e01ee6e94 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Mon, 14 Dec 2020 16:49:43 -0500 Subject: [PATCH 10/21] Add tests for `Combinations` using partial ranges --- Sources/Algorithms/Combinations.swift | 2 +- .../CombinationsTests.swift | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 4532d669..d38b8c52 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -33,7 +33,6 @@ public struct Combinations { /// - k: The expected size of each combination. @usableFromInline internal init(_ base: Base, k: Int) { - assert(k >= 0, "Can't have combinations with a negative number of elements.") self.init(base, k: k...k) } @@ -307,6 +306,7 @@ extension Collection { /// - Complexity: O(1) @inlinable public func combinations(ofCount k: Int) -> Combinations { + assert(k >= 0, "Can't have combinations with a negative number of elements.") return Combinations(self, k: k) } } diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index eb03c072..ad73aa10 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -58,8 +58,14 @@ final class CombinationsTests: XCTestCase { let c12 = c.combinations(ofCounts: 5...10).count XCTAssertEqual(c12, 0) - let c13 = c.combinations().count + let c13 = c.combinations(ofCounts: 0...).count XCTAssertEqual(c13, 16) + + let c14 = c.combinations(ofCounts: ...3).count + XCTAssertEqual(c14, 15) + + let c15 = c.combinations().count + XCTAssertEqual(c15, 16) } func testCombinations() { @@ -81,7 +87,19 @@ final class CombinationsTests: XCTestCase { XCTAssertEqual(["AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c5.map { String($0) }) let c6 = c.combinations(ofCounts: 0...4) - XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD","ABCD"], c6.map { String($0) }) + XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c6.map { String($0) }) + + let c7 = c.combinations() + XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c7.map { String($0) }) + + let c8 = c.combinations(ofCounts: ...4) + XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c8.map { String($0) }) + + let c9 = c.combinations(ofCounts: ...3) + XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD"], c9.map { String($0) }) + + let c10 = c.combinations(ofCounts: 1...) + XCTAssertEqual(["A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c10.map { String($0) }) } func testEmpty() { @@ -101,6 +119,8 @@ final class CombinationsTests: XCTestCase { func testCombinationsLazy() { XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 1)) XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...3)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: ...3)) XCTAssertLazySequence("ABC".lazy.combinations()) } } From cbf6f3794c1dcee55a777f814d2ea2e5c87175e1 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Tue, 5 Jan 2021 12:32:00 -0800 Subject: [PATCH 11/21] =?UTF-8?q?Simplify=20range=20expression=20by=20not?= =?UTF-8?q?=20using=20`R.Bound`=20since=20it=E2=80=99s=20always=20`Int`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Algorithms/Combinations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index d38b8c52..a9441315 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -45,7 +45,7 @@ public struct Combinations { internal init( _ base: Base, k: R ) where R.Bound == Int { - let range = k.relative(to: R.Bound.zero.. Date: Tue, 5 Jan 2021 12:36:59 -0800 Subject: [PATCH 12/21] =?UTF-8?q?Increment=20range=E2=80=99s=20bound=20wit?= =?UTF-8?q?h=20`+=201`=20instead=20of=20`advanced(by:=201)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Algorithms/Combinations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index a9441315..309409d4 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -122,7 +122,7 @@ extension Combinations: Sequence { internal mutating func advance() { /// Advances `k` by increasing its `lowerBound` or finishes the iteration. func advanceK() { - let advancedLowerBound = k.lowerBound.advanced(by: 1) + let advancedLowerBound = k.lowerBound + 1 if advancedLowerBound < k.upperBound { k = advancedLowerBound.. Date: Wed, 6 Jan 2021 09:44:48 -0800 Subject: [PATCH 13/21] Rename `k` to `kRange` to indicate that it now represents a range of `k` Functions that accept a single integer value still name the parameter as `k`. --- Sources/Algorithms/Combinations.swift | 62 ++++++++++++++------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 309409d4..41710820 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -18,13 +18,13 @@ public struct Combinations { /// - Note: This may be `nil` if the attempted range entirely exceeds the /// upper bounds of the size of the `base` collection. @usableFromInline - internal let k: Range? + internal let kRange: Range? /// Initializes a `Combinations` for all combinations of `base` of all sizes. /// - Parameter base: The collection to iterate over for combinations. @usableFromInline internal init(_ base: Base) { - self.init(base, k: 0...) + self.init(base, kRange: 0...) } /// Initializes a `Combinations` for all combinations of `base` of size `k`. @@ -33,22 +33,22 @@ public struct Combinations { /// - k: The expected size of each combination. @usableFromInline internal init(_ base: Base, k: Int) { - self.init(base, k: k...k) + self.init(base, kRange: k...k) } /// Initializes a `Combinations` for all combinations of `base` of sizes /// within a given range. /// - Parameters: /// - base: The collection to iterate over for combinations. - /// - k: The range of accepted sizes of combinations. + /// - kRange: The range of accepted sizes of combinations. @usableFromInline internal init( - _ base: Base, k: R + _ base: Base, kRange: R ) where R.Bound == Int { - let range = k.relative(to: 0 ..< .max) + let range = kRange.relative(to: 0 ..< .max) self.base = base let upperBound = base.count + 1 - self.k = range.lowerBound < upperBound + self.kRange = range.lowerBound < upperBound ? range.clamped(to: 0.. { /// The total number of combinations. @inlinable public var count: Int { - guard let k = self.k else { return 0 } + guard let k = self.kRange else { return 0 } let n = base.count if k == 0..<(n + 1) { return 1 << n @@ -84,20 +84,25 @@ extension Combinations: Sequence { internal let base: Base /// The current range of accepted sizes of combinations. + /// - Note: The range is contracted until empty while iterating over + /// combinations of different sizes. When the range is empty, iteration is + /// finished. @usableFromInline - internal var k: Range + internal var kRange: Range + /// Whether or not iteration is finished (`kRange` is empty) @usableFromInline - internal var indexes: [Base.Index] + internal var isFinished: Bool { + return kRange.isEmpty + } @usableFromInline - internal var finished: Bool + internal var indexes: [Base.Index] internal init(_ combinations: Combinations) { self.base = combinations.base - self.k = combinations.k ?? 0..<1 - self.indexes = Array(combinations.base.indices.prefix(k.lowerBound)) - self.finished = (combinations.k == nil) + self.kRange = combinations.kRange ?? 0..<0 + self.indexes = Array(combinations.base.indices.prefix(kRange.lowerBound)) } /// Advances the current indices to the next set of combinations. If @@ -120,14 +125,13 @@ extension Combinations: Sequence { /// // so the iteration is finished. @usableFromInline internal mutating func advance() { - /// Advances `k` by increasing its `lowerBound` or finishes the iteration. - func advanceK() { - let advancedLowerBound = k.lowerBound + 1 - if advancedLowerBound < k.upperBound { - k = advancedLowerBound..= 0 else { // Finished iterating over combinations of this size. - advanceK() + advanceKRange() return } @@ -164,7 +168,7 @@ extension Combinations: Sequence { @inlinable public mutating func next() -> [Base.Element]? { - if finished { return nil } + guard !isFinished else { return nil } defer { advance() } return indexes.map { i in base[i] } } @@ -256,19 +260,19 @@ extension Collection { /// at `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, /// `[1, 3]`, and finally `[2, 3]`. /// - /// If `k` is `0...0`, the resulting sequence has exactly one element, an + /// If `kRange` is `0...0`, the resulting sequence has exactly one element, an /// empty array. If `k.upperBound` is greater than the number of elements in /// this sequence, the resulting sequence has no elements. /// - /// - Parameter k: The range of numbers of elements to include in each + /// - Parameter kRange: The range of numbers of elements to include in each /// combination. /// /// - Complexity: O(1) @inlinable public func combinations( - ofCounts k: R + ofCounts kRange: R ) -> Combinations where R.Bound == Int { - return Combinations(self, k: k) + return Combinations(self, kRange: kRange) } } From f3dae046f7a141df69b2e6032a937597c781838d Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Wed, 6 Jan 2021 20:07:15 -0800 Subject: [PATCH 14/21] Remove `combinations()` in favor of using `0...` --- Guides/Combinations.md | 26 ------- README.md | 2 +- Sources/Algorithms/Combinations.swift | 76 ++++++------------- .../CombinationsTests.swift | 6 +- 4 files changed, 28 insertions(+), 82 deletions(-) diff --git a/Guides/Combinations.md b/Guides/Combinations.md index 57f41bf8..c52ef530 100644 --- a/Guides/Combinations.md +++ b/Guides/Combinations.md @@ -56,32 +56,6 @@ for combo in numbers(ofCounts: 2...3) { // [20, 30, 40] ``` -The `combinations()` method returns a sequence of *all* the different -combinations of a collection’s elements in increasing order of size. - -```swift -let numbers = [10, 20, 30, 40] -for combo in numbers.combinations() { - print(combo) -} -// [] -// [10] -// [20] -// [30] -// [40] -// [10, 20] -// [10, 30] -// [10, 40] -// [20, 30] -// [20, 40] -// [30, 40] -// [10, 20, 30] -// [10, 20, 40] -// [10, 30, 40] -// [20, 30, 40] -// [10, 20, 30, 40] -``` - ## Detailed Design The `combinations(ofCount:)` method is declared as a `Collection` extension, diff --git a/README.md b/README.md index bd785384..920a8392 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Read more about the package, and the intent behind it, in the [announcement on s #### Combinations / permutations -- [`combinations(ofCount:)`, `combinations(ofCounts:)`, `combinations()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. +- [`combinations(ofCount:)`, `combinations(ofCounts:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. - [`permutations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Permutations.md): Permutations of a particular size of the elements in a collection, or of the full collection. #### Mutating algorithms diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 41710820..212ed057 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -20,13 +20,6 @@ public struct Combinations { @usableFromInline internal let kRange: Range? - /// Initializes a `Combinations` for all combinations of `base` of all sizes. - /// - Parameter base: The collection to iterate over for combinations. - @usableFromInline - internal init(_ base: Base) { - self.init(base, kRange: 0...) - } - /// Initializes a `Combinations` for all combinations of `base` of size `k`. /// - Parameters: /// - base: The collection to iterate over for combinations. @@ -183,51 +176,6 @@ extension Combinations: LazySequenceProtocol where Base: LazySequenceProtocol {} extension Combinations: Equatable where Base: Equatable {} extension Combinations: Hashable where Base: Hashable {} -//===----------------------------------------------------------------------===// -// combinations() -//===----------------------------------------------------------------------===// - -extension Collection { - /// Returns a collection of all the combinations of this collection's - /// elements, including the full collection and an empty collection - /// - /// This example prints the all the combinations from an array of letters: - /// - /// let letters = ["A", "B", "C", "D"] - /// for combo in letters.combinations() { - /// print(combo.joined(separator: ", ")) - /// } - /// // - /// // A - /// // B - /// // C - /// // D - /// // A, B - /// // A, C - /// // A, D - /// // B, C - /// // B, D - /// // C, D - /// // A, B, C - /// // A, B, D - /// // A, C, D - /// // B, C, D - /// // A, B, C, D - /// - /// The returned collection presents combinations in a consistent order, where - /// the indices in each combination are in ascending lexicographical order, - /// and the size of the combinations are in increasing order. - /// That is, in the example above, the combinations in order are the elements - /// at `[]`, `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, - /// `[1, 2]`, `[1, 3]`, … `[0, 1, 2, 3]`. - /// - /// - Complexity: O(1) - @inlinable - public func combinations() -> Combinations { - return Combinations(self) - } -} - //===----------------------------------------------------------------------===// // combinations(ofCounts:) //===----------------------------------------------------------------------===// @@ -260,6 +208,30 @@ extension Collection { /// at `[0]`, `[1]`, `[2]`, `[3]`, `[0, 1]`, `[0, 2]`, `[0, 3]`, `[1, 2]`, /// `[1, 3]`, and finally `[2, 3]`. /// + /// This example prints _all_ the combinations (including an empty array and + /// the original collection) from an array of numbers: + /// + /// let numbers = [10, 20, 30, 40] + /// for combo in numbers.combinations(ofCount: 0...) { + /// print(combo) + /// } + /// // [] + /// // [10] + /// // [20] + /// // [30] + /// // [40] + /// // [10, 20] + /// // [10, 30] + /// // [10, 40] + /// // [20, 30] + /// // [20, 40] + /// // [30, 40] + /// // [10, 20, 30] + /// // [10, 20, 40] + /// // [10, 30, 40] + /// // [20, 30, 40] + /// // [10, 20, 30, 40] + /// /// If `kRange` is `0...0`, the resulting sequence has exactly one element, an /// empty array. If `k.upperBound` is greater than the number of elements in /// this sequence, the resulting sequence has no elements. diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index ad73aa10..a2a4f512 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -64,7 +64,7 @@ final class CombinationsTests: XCTestCase { let c14 = c.combinations(ofCounts: ...3).count XCTAssertEqual(c14, 15) - let c15 = c.combinations().count + let c15 = c.combinations(ofCounts: 0...).count XCTAssertEqual(c15, 16) } @@ -89,7 +89,7 @@ final class CombinationsTests: XCTestCase { let c6 = c.combinations(ofCounts: 0...4) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c6.map { String($0) }) - let c7 = c.combinations() + let c7 = c.combinations(ofCounts: 0...) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c7.map { String($0) }) let c8 = c.combinations(ofCounts: ...4) @@ -121,6 +121,6 @@ final class CombinationsTests: XCTestCase { XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...3)) XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...)) XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: ...3)) - XCTAssertLazySequence("ABC".lazy.combinations()) + XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 0...)) } } From a1f6b402551180da3a29fe095cccdae75654c16a Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Wed, 6 Jan 2021 20:11:30 -0800 Subject: [PATCH 15/21] Re-do line wrap in Combinations.md --- Guides/Combinations.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Guides/Combinations.md b/Guides/Combinations.md index c52ef530..d1c1e2f7 100644 --- a/Guides/Combinations.md +++ b/Guides/Combinations.md @@ -37,7 +37,8 @@ for combo in numbers2.combinations(ofCount: 2) { ``` The `combinations(ofCounts:)` method returns a sequence of all the different -combinations of the given sizes of a collection’s elements in increasing order of size. +combinations of the given sizes of a collection’s elements in increasing order +of size. ```swift let numbers = [10, 20, 30, 40] @@ -76,9 +77,9 @@ array at every index advancement. `Combinations` does conform to ### Complexity -Calling `combinations(ofCount:)` accesses the count of the collection, so it’s an -O(1) operation for random-access collections, or an O(_n_) operation otherwise. -Creating the iterator for a `Combinations` instance and each call to +Calling `combinations(ofCount:)` accesses the count of the collection, so it’s +an O(1) operation for random-access collections, or an O(_n_) operation +otherwise. Creating the iterator for a `Combinations` instance and each call to `Combinations.Iterator.next()` is an O(_n_) operation. ### Naming From 5ec5f6ac1f84d2c330c1eb7ea342784bd7bb4af1 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Wed, 6 Jan 2021 20:18:58 -0800 Subject: [PATCH 16/21] Update copyright year for files modified in 2021 --- Sources/Algorithms/Combinations.swift | 2 +- Tests/SwiftAlgorithmsTests/CombinationsTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 212ed057..b0237f18 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Algorithms open source project // -// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index a2a4f512..0c8c8afc 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Algorithms open source project // -// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information From 70ad269d13688b9b5b5258ec796574b6550b375e Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Mon, 11 Jan 2021 10:03:16 -0800 Subject: [PATCH 17/21] Rename `combinations(ofCounts:)` to `combinations(ofCount:)` --- Guides/Combinations.md | 8 +-- README.md | 2 +- Sources/Algorithms/Combinations.swift | 14 ++---- .../CombinationsTests.swift | 50 +++++++++---------- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/Guides/Combinations.md b/Guides/Combinations.md index d1c1e2f7..b52400f4 100644 --- a/Guides/Combinations.md +++ b/Guides/Combinations.md @@ -36,13 +36,13 @@ for combo in numbers2.combinations(ofCount: 2) { // [10, 10] ``` -The `combinations(ofCounts:)` method returns a sequence of all the different -combinations of the given sizes of a collection’s elements in increasing order -of size. +Given a range, the `combinations(ofCount:)` method returns a sequence of all +the different combinations of the given sizes of a collection’s elements in +increasing order of size. ```swift let numbers = [10, 20, 30, 40] -for combo in numbers(ofCounts: 2...3) { +for combo in numbers.combinations(ofCount: 2...3) { print(combo) } // [10, 20] diff --git a/README.md b/README.md index 920a8392..40ced02f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Read more about the package, and the intent behind it, in the [announcement on s #### Combinations / permutations -- [`combinations(ofCount:)`, `combinations(ofCounts:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. +- [`combinations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Combinations.md): Combinations of particular sizes of the elements in a collection. - [`permutations(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Permutations.md): Permutations of a particular size of the elements in a collection, or of the full collection. #### Mutating algorithms diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index b0237f18..3b4dc5ca 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -177,7 +177,7 @@ extension Combinations: Equatable where Base: Equatable {} extension Combinations: Hashable where Base: Hashable {} //===----------------------------------------------------------------------===// -// combinations(ofCounts:) +// combinations(ofCount:) //===----------------------------------------------------------------------===// extension Collection { @@ -188,7 +188,7 @@ extension Collection { /// four colors: /// /// let colors = ["fuchsia", "cyan", "mauve", "magenta"] - /// for combo in colors.combinations(ofCounts: 1...2) { + /// for combo in colors.combinations(ofCount: 1...2) { /// print(combo.joined(separator: ", ")) /// } /// // fuchsia @@ -242,17 +242,11 @@ extension Collection { /// - Complexity: O(1) @inlinable public func combinations( - ofCounts kRange: R + ofCount kRange: R ) -> Combinations where R.Bound == Int { return Combinations(self, kRange: kRange) } -} - -//===----------------------------------------------------------------------===// -// combinations(ofCount:) -//===----------------------------------------------------------------------===// - -extension Collection { + /// Returns a collection of combinations of this collection's elements, with /// each combination having the specified number of elements. /// diff --git a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift index 0c8c8afc..4239b7e0 100644 --- a/Tests/SwiftAlgorithmsTests/CombinationsTests.swift +++ b/Tests/SwiftAlgorithmsTests/CombinationsTests.swift @@ -31,40 +31,40 @@ final class CombinationsTests: XCTestCase { let c4 = c.combinations(ofCount: 4).count XCTAssertEqual(c4, 1) - let c5 = c.combinations(ofCounts: 0...0).count + let c5 = c.combinations(ofCount: 0...0).count XCTAssertEqual(c5, 1) - let c6 = c.combinations(ofCounts: 1...1).count + let c6 = c.combinations(ofCount: 1...1).count XCTAssertEqual(c6, 4) - let c7 = c.combinations(ofCounts: 1...2).count + let c7 = c.combinations(ofCount: 1...2).count XCTAssertEqual(c7, 10) - let c8 = c.combinations(ofCounts: 1...3).count + let c8 = c.combinations(ofCount: 1...3).count XCTAssertEqual(c8, 14) - let c9 = c.combinations(ofCounts: 2...4).count + let c9 = c.combinations(ofCount: 2...4).count XCTAssertEqual(c9, 11) // `k` greater than element count results in same number of combinations - let c10 = c.combinations(ofCounts: 3...10).count + let c10 = c.combinations(ofCount: 3...10).count XCTAssertEqual(c10, 5) // `k` greater than element count results in same number of combinations - let c11 = c.combinations(ofCounts: 4...10).count + let c11 = c.combinations(ofCount: 4...10).count XCTAssertEqual(c11, 1) // `k` entirely greater than element count results in no combinations - let c12 = c.combinations(ofCounts: 5...10).count + let c12 = c.combinations(ofCount: 5...10).count XCTAssertEqual(c12, 0) - let c13 = c.combinations(ofCounts: 0...).count + let c13 = c.combinations(ofCount: 0...).count XCTAssertEqual(c13, 16) - let c14 = c.combinations(ofCounts: ...3).count + let c14 = c.combinations(ofCount: ...3).count XCTAssertEqual(c14, 15) - let c15 = c.combinations(ofCounts: 0...).count + let c15 = c.combinations(ofCount: 0...).count XCTAssertEqual(c15, 16) } @@ -83,44 +83,44 @@ final class CombinationsTests: XCTestCase { let c4 = c.combinations(ofCount: 4) XCTAssertEqual(["ABCD"], c4.map { String($0) }) - let c5 = c.combinations(ofCounts: 2...4) + let c5 = c.combinations(ofCount: 2...4) XCTAssertEqual(["AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c5.map { String($0) }) - let c6 = c.combinations(ofCounts: 0...4) + let c6 = c.combinations(ofCount: 0...4) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c6.map { String($0) }) - let c7 = c.combinations(ofCounts: 0...) + let c7 = c.combinations(ofCount: 0...) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c7.map { String($0) }) - let c8 = c.combinations(ofCounts: ...4) + let c8 = c.combinations(ofCount: ...4) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c8.map { String($0) }) - let c9 = c.combinations(ofCounts: ...3) + let c9 = c.combinations(ofCount: ...3) XCTAssertEqual(["", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD"], c9.map { String($0) }) - let c10 = c.combinations(ofCounts: 1...) + let c10 = c.combinations(ofCount: 1...) XCTAssertEqual(["A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], c10.map { String($0) }) } func testEmpty() { // `k == 0` results in one zero-length combination XCTAssertEqualSequences([[]], "".combinations(ofCount: 0)) - XCTAssertEqualSequences([[]], "".combinations(ofCounts: 0...0)) + XCTAssertEqualSequences([[]], "".combinations(ofCount: 0...0)) XCTAssertEqualSequences([[]], "ABCD".combinations(ofCount: 0)) - XCTAssertEqualSequences([[]], "ABCD".combinations(ofCounts: 0...0)) + XCTAssertEqualSequences([[]], "ABCD".combinations(ofCount: 0...0)) // `k` greater than element count results in zero combinations XCTAssertEqualSequences([], "".combinations(ofCount: 5)) - XCTAssertEqualSequences([], "".combinations(ofCounts: 5...10)) + XCTAssertEqualSequences([], "".combinations(ofCount: 5...10)) XCTAssertEqualSequences([], "ABCD".combinations(ofCount: 5)) - XCTAssertEqualSequences([], "ABCD".combinations(ofCounts: 5...10)) + XCTAssertEqualSequences([], "ABCD".combinations(ofCount: 5...10)) } func testCombinationsLazy() { XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 1)) - XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...3)) - XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 1...)) - XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: ...3)) - XCTAssertLazySequence("ABC".lazy.combinations(ofCounts: 0...)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 1...3)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 1...)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCount: ...3)) + XCTAssertLazySequence("ABC".lazy.combinations(ofCount: 0...)) } } From 4c4100f83d4d2f5e5a2b2e2b656041e859e89f56 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Mon, 11 Jan 2021 11:32:00 -0800 Subject: [PATCH 18/21] Avoid any intermediate heap allocations when advancing `indexes` Co-authored-by: Kyle Macomber --- Sources/Algorithms/Combinations.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 3b4dc5ca..6d31033f 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -124,7 +124,8 @@ extension Combinations: Sequence { if kRange.lowerBound < kRange.upperBound { let advancedLowerBound = kRange.lowerBound + 1 kRange = advancedLowerBound.. Date: Mon, 11 Jan 2021 12:41:49 -0800 Subject: [PATCH 19/21] Avoid counting `base` more than once This is similar to how `Permutations` works --- Sources/Algorithms/Combinations.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index 6d31033f..b716b47a 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -14,6 +14,9 @@ public struct Combinations { /// The collection to iterate over for combinations. public let base: Base + @usableFromInline + internal let baseCount: Int + /// The range of accepted sizes of combinations. /// - Note: This may be `nil` if the attempted range entirely exceeds the /// upper bounds of the size of the `base` collection. @@ -40,9 +43,11 @@ public struct Combinations { ) where R.Bound == Int { let range = kRange.relative(to: 0 ..< .max) self.base = base - let upperBound = base.count + 1 + let baseCount = base.count + self.baseCount = baseCount + let upperBound = baseCount + 1 self.kRange = range.lowerBound < upperBound - ? range.clamped(to: 0.. { @inlinable public var count: Int { guard let k = self.kRange else { return 0 } - let n = base.count - if k == 0..<(n + 1) { + let n = baseCount + if k == 0 ..< (n + 1) { return 1 << n } @@ -123,7 +128,7 @@ extension Combinations: Sequence { func advanceKRange() { if kRange.lowerBound < kRange.upperBound { let advancedLowerBound = kRange.lowerBound + 1 - kRange = advancedLowerBound.. Date: Mon, 11 Jan 2021 13:55:52 -0800 Subject: [PATCH 20/21] Clarifying comment about behavior of limited range --- Sources/Algorithms/Combinations.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index b716b47a..d7e89e28 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -239,8 +239,8 @@ extension Collection { /// // [10, 20, 30, 40] /// /// If `kRange` is `0...0`, the resulting sequence has exactly one element, an - /// empty array. If `k.upperBound` is greater than the number of elements in - /// this sequence, the resulting sequence has no elements. + /// empty array. The given range is limited to `0...base.count`. If the + /// limited range is empty, the resulting sequence has no elements. /// /// - Parameter kRange: The range of numbers of elements to include in each /// combination. From 7a6c1353f3c579cf709d0ea9602f598813ffa2c5 Mon Sep 17 00:00:00 2001 From: Matt Zanchelli Date: Mon, 11 Jan 2021 14:32:47 -0800 Subject: [PATCH 21/21] Update documentation on complexity of `combinations(ofCount:)` --- Sources/Algorithms/Combinations.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Algorithms/Combinations.swift b/Sources/Algorithms/Combinations.swift index d7e89e28..643feada 100644 --- a/Sources/Algorithms/Combinations.swift +++ b/Sources/Algorithms/Combinations.swift @@ -245,7 +245,9 @@ extension Collection { /// - Parameter kRange: The range of numbers of elements to include in each /// combination. /// - /// - 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 `Combinations` + /// accesses the `count` of the base collection. @inlinable public func combinations( ofCount kRange: R @@ -279,7 +281,9 @@ extension Collection { /// /// - Parameter k: The number of elements to include in each combination. /// - /// - 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 `Combinations` + /// accesses the `count` of the base collection. @inlinable public func combinations(ofCount k: Int) -> Combinations { assert(k >= 0, "Can't have combinations with a negative number of elements.")