koen
koen

Reputation: 5729

swift string permutations allowing the same strings

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.

Upvotes: 3

Views: 1505

Answers (2)

Rob
Rob

Reputation: 437682

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.

Upvotes: 2

dfrib
dfrib

Reputation: 73186

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"]
}

Upvotes: 2

Related Questions