3

I have seen other questions about string permutations, but they don't completely cover my question.

Suppose I have an array of strings: ["A", "B", "C", "D", "E"] and I am looking for a way to get all possible combinations of for instance three elements:

AAA, AAB, AAC, AAD, AAE, ABA, ACA, ...

Other solutions for permutations (e.g. here, or here) don't allow the same element to be duplicated, and result in:

ABC, ABD, ABE, BAC, ...

I now use a brute-force approach, with many iterations, but of course that is super slow (because the number of individual strings can be more than 10)

Any ideas how to tackle this?

This is what I have now:

func getVariations() -> [String] {
var variations = [String]()

let elements = ["A", "B", "C", "D", "E"]

for e1 in elements {
    variations.append(e1)

    for e2 in elements {
        variations.append(e1 + e2)

        for e3 in elements {
            variations.append(e1 + e2 + e3)

            for e4 in elements {
                variations.append(e1 + e2 + e3 + e4)
            }
        }
    }

    return variations
}

One can imagine this gets out of hand when more elements are to be added.

koen
  • 5,383
  • 7
  • 50
  • 89
  • 1
    We are not here do your homework for you. Update your question with your actual code and clearly explain what issues you are having with your code. – rmaddy Feb 25 '18 at 17:32
  • Woah, this is not homework. I will add my current code which I already tried. – koen Feb 25 '18 at 17:34
  • 1
    This might help https://stackoverflow.com/a/25166979/1187415. I'll update it for Swift 3/4 later. – Martin R Feb 25 '18 at 17:54
  • 1
    More possible starting points here: https://stackoverflow.com/questions/33021064/how-to-generate-all-possible-combinations. – Martin R Feb 25 '18 at 18:23

2 Answers2

2

Treating you permutations as sequential numbers in a custom positional numeral system

"AAA, AAB, AAC, AAD, AAE, ABA, ACA, ..."

As per your example, you basically want to permute unique single-letter strings with replacement; with fixed sample size (above; 3). If so, you could consider your letters as unique digits in a custom numeral positional numeral system, for you example specifically a radix 5 system for which you'd like to compute all numbers expressible with at most 3 digits. Finally, you'd like to pad all numbers with leading "zeros" (A), in case you are using less digits than allowed (<3).

With this special case in mind, we can readily make use of the String(_:radix:) and Int(_:radix:) initializers to convert base 10 numbers to your specific numeral system, and implement a non-recursive approach as follows:

// Helper to pad the presented numbers to a given width.
extension String {
    func leftPadded(with padding: Character, toAtLeast width: Int) -> String {
        return count >= width ? self
            : String(repeating: padding, count: width - count) + self
    }
}

let digits = ["A", "B", "C", "D", "E"]
let base = digits.count
let width = 3

if let zero = digits.first.map(Character.init) {
    // Iterate and convert to your numeral system.
    for i in 0..<((0..<width).reduce(1) { (p, _) in p * base }) {
        let nonPaddedPermutation = String(i, radix: base)
            .flatMap { Int(String($0), radix: base) }
            .map { String(digits[$0]) }
            .joined()
        print(nonPaddedPermutation.leftPadded(with: zero, toAtLeast: width))
    } /* AAA
         AAB
         ...
         EED
         EEE */
}

Or, somewhat more general (treating allowed digits as characters rather than single-character strings):

extension String {
    func leftPadded(with padding: Character, toAtLeast width: Int) -> String {
        return count >= width ? self
            : String(repeating: padding, count: width - count) + self
    }
}

extension Array where Element == Character {
    // Limitation: all elements are unique (otherwise: `nil` return)
    func replacementPermute(sampleSize width: Int) -> [String]? {
        guard count == Set(self).count else { return nil }

        var permutations: [String] = []
        if let zero = first {
            let numPerms = ((0..<width).reduce(1) { (p, _) in p * count })
            permutations.reserveCapacity(numPerms)
            for i in 0..<numPerms {
                let nonPaddedPermutation = String(i, radix: count)
                    .flatMap { Int(String($0), radix: count) }
                    .map { String(self[$0]) }
                    .joined()
                permutations.append(nonPaddedPermutation
                    .leftPadded(with: zero, toAtLeast: width))
            }
        }
        return permutations
    }
}

// Example usage:
if let permutations = ["A", "", "C", "D", "E"]
    .flatMap(Character.init).replacementPermute(sampleSize: 3) {
    print(permutations)
    // ["AAA", "AA", "AAC", ... "EEA", "EE", "EEC", "EED", "EEE"]
}
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 1
    I think the number of permutations would be easier to read if calculated using power: `Int(pow(Double(base), Double(width)))`. If you want to use integer operations only, then `repeatElement(base, count: width).reduce(1, *)`. – Sulthan Feb 25 '18 at 19:53
  • This is definitely faster than my brute-force method. I am also going to try the links that @Martin R suggested and will post back here with my findings later. – koen Feb 25 '18 at 20:26
  • 1
    @Sulthan I intentionally avoided the `pow` and conversions, but the `repeatElement` is neat, thanks! Will update my answer at a later time. – dfrib Feb 25 '18 at 20:37
  • I prefer this approach, since it allows me to limit the sampleSize to one value, whereas other solutions give all possible combinations. In other words, I will just get `"AAA", "AA", "AAC", ...` but not `"AA", "A", ... – koen Feb 25 '18 at 20:42
  • Next step is how to filter out results with the same letter combinations (I don't need both "AAC" and "ACA"), but will post another question if I cannot figure that one out. – koen Feb 25 '18 at 20:45
2

In another question, you ask how to filter the results from dfri's answer (+1) to prune out duplicates resulting from a different order of elements (e.g. if you got a result set with "AAB", "ABA", and "BAA", prune out the latter two).

If that's what you want to do, I'd suggest writing a function that just returned that set of solutions directly:

extension Array where Element: StringProtocol {

    /// Return combinations of the elements of the array (ignoring the order of items in those combinations).
    ///
    /// - Parameters:
    ///   - size: The size of the combinations to be returned.
    ///   - allowDuplicates: Boolean indicating whether an item in the array can be repeated in the combinations (e.g. is the sampled item returned to the original set or not).
    ///
    /// - Returns: A collection of resulting combinations.

    func combinations(size: Int, allowDuplicates: Bool = false) -> [String] {
        let n = count

        if size > n && !allowDuplicates { return [] }

        var combinations: [String] = []

        var indices = [0]

        var i = 0

        while true {
            // build out array of indexes (if not complete)

            while indices.count < size {
                i = indices.last! + (allowDuplicates ? 0 : 1)
                if i < n {
                    indices.append(i)
                }
            }

            // add combination associated with this particular array of indices

            combinations.append(indices.map { self[$0] }.joined())

            // prepare next one (incrementing the last component and/or deleting as needed

            repeat {
                if indices.count == 0 { return combinations }
                i = indices.last! + 1
                indices.removeLast()
            } while i > n - (allowDuplicates ? 1 : (size - indices.count))
            indices.append(i)
        }
    }
}

Thus:

let array = ["A", "B", "C", "D"]
let result = array.combinations(size: 2, allowDuplicates: true)

would return:

["AA", "AB", "AC", "AD", "BB", "BC", "BD", "CC", "CD", "DD"]

And if you didn't want it to allow duplicates:

let result = array.combinations(size: 2)

would return:

["AB", "AC", "AD", "BC", "BD", "CD"]

This approach would avoid needing to later filter the results.

Note, I'm sure there are more elegant ways to achieve the above, but hopefully this illustrates the basic idea.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • This answer actually solves both this question and the second one (see link) together. And it is also much faster than any of the other solutions I've tried. – koen Feb 27 '18 at 13:32
  • A quick follow-up question: the input really is "ABCD" (so one string), and as a workaround I use `"ABCD".map { String($0) }` to be able to use the `combinations` function above. Does that sound like a more logical approach, or should in that case the extension be on a `String`. – koen Mar 01 '18 at 18:13
  • 1
    While above I tried to keep the above simple, I would actually change `combinations` to not require `StringProtocol` conformance, so it can be used with any array. And then create a `String` method to perform the `map` out to an array of characters/strings and then re-join them at the end. See https://gist.github.com/robertmryan/1ca0deab3e3e53d54dccf421a5c64144 – Rob Mar 01 '18 at 19:20
  • Nice improvement, thanks for sharing. Now, if the input is "ABCA", the result will contain both "AC" and "CA", and "AB" and "BA". So to filter these out, I could sort each string alphabetically, and then convert the array of strings to a `Set` and then back to an `Array` - does that sound like a way to go? – koen Mar 02 '18 at 15:25
  • the goal is to remove the duplicates from the combinations results array, so "ABCA" -> ["AB", "AC", "AA", "BC", "BA", "CA"] -> ["AB", "AC", "AA", "BC"] - sorry for the confusion. – koen Mar 02 '18 at 16:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/166123/discussion-between-rob-and-koen). – Rob Mar 02 '18 at 17:32