How to generate all possible combinations?

前端 未结 4 986
南旧
南旧 2020-12-09 12:12

I\'m currently trying to make a Set of all possible combinations from an Array of Strings, were each element contains only one letter.

相关标签:
4条回答
  • 2020-12-09 12:52

    Here's a Swift 3 function that is a bit faster. It is based on an extension to the Array type that can be used on arrays with any element type.

    public func allCombinations(_ array:[String], minLength:Int=2) -> [String]
    {
       var result:[String] = []
       for n in minLength...array.count
       {
          result = result + array.combinations(of:n).map{ $0.joined(separator:"") }
       }
       return result
    }
    
    extension Array
    {
        public func combinations(of group:Int) -> [[Element]]
        {
           if group > count  { return [] }
    
           if group == count { return [self] }
    
           var result:[[Element]] = []
    
           var comboIndexes = (0..<group).map{$0}
    
           let fullCombo   = group - 1
           let indexLimit  = count - fullCombo
    
           var carry = fullCombo
    
           while carry >= 0
           {
              if carry == fullCombo
              { result.append(comboIndexes.map{self[$0]}) }
    
              comboIndexes[carry] += 1
    
              if comboIndexes[carry] == carry + indexLimit 
              { carry -= 1 ; continue }
    
              while carry < fullCombo
              {
                 carry += 1
                 comboIndexes[carry] = comboIndexes[carry-1] + 1 
              }       
           }
    
           return result
       }
    }
    

    In my tests, it ran about 40x faster than vacawama's second version on 7 letters.

    [EDIT] I realized later that this function produces combinations (as requested in the OP) where vacawama's function produces permutations. I tested an equivalent algorithm for permutations and it was merely 55% faster than vacawama's.

    extension Array
    {
       public func permutations(of group:Int? = nil) -> [[Element]]
       {
          let group       = group ?? count
          var result      : [[Element]] = []
          var permutation : [Element]   = []
    
          func permute(from baseIndex:Int)
          {
             if baseIndex == permutation.count - 1
             { 
               result.append(permutation)
               return 
             }
    
             permute(from:baseIndex+1)
    
             for index in baseIndex+1..<permutation.count
             {
                swap(&permutation[baseIndex],&permutation[index]) 
                permute(from:baseIndex+1)
             }
             let baseElement = permutation[baseIndex]
             permutation.remove(at:baseIndex)
             permutation.append(baseElement)
          }
    
          var comboIndexes = (0..<group).map{$0}
    
          let fullCombo   = group - 1
          let indexLimit  = count - fullCombo
    
          var carry = fullCombo
    
          while carry >= 0
          {
             if carry == fullCombo
             { 
               permutation = comboIndexes.map{self[$0]}
               permute(from:0)
             }
    
             comboIndexes[carry] += 1
    
             if comboIndexes[carry] == carry + indexLimit 
             { carry -= 1 ; continue }
    
             while carry < fullCombo
             {
                carry += 1
                comboIndexes[carry] = comboIndexes[carry-1] + 1 
             }       
          }
    
          return result
       }
    }
    
    0 讨论(0)
  • 2020-12-09 13:07

    This is quite similar to @vacawama's answer, but hopefully different enough that it deserves a separate answer :)

    Here, an array with all combinations is built (explaining comments inline):

    func combinations(array : [String]) -> [String] {
    
        // Recursion terminates here:
        if array.count == 0 { return [] }
    
        // Concatenate all combinations that can be built with element #i at the
        // first place, where i runs through all array indices:
        return array.indices.flatMap { i -> [String] in
    
            // Pick element #i and remove it from the array:
            var arrayMinusOne = array
            let elem = arrayMinusOne.removeAtIndex(i)
    
            // Prepend element to all combinations of the smaller array:
            return [elem] + combinations(arrayMinusOne).map { elem + $0 }
        }
    }
    

    Then you can filter the strings with at least two letters, and convert it to a Set:

    let c = Set(combinations(["A", "B", "C"]).filter { $0.characters.count >= 2 })
    print(c)
    // ["BA", "AC", "ABC", "AB", "BCA", "CB", "BC", "CAB", "ACB", "CA", "CBA", "BAC"]
    

    I made a simple performance comparison (compiled in Release mode on a Macbook Pro):

    let array = ["A", "B", "C", "D", "E", "F", "G"]
    
    let t1 = NSDate()
    let c1 = Set(combinations(array).filter { $0.characters.count >= 2 })
    let t2 = NSDate()
    let c2 = permute(array)
    let t3 = NSDate()
    
    print(c1 == c2) // true
    print(t2.timeIntervalSinceDate(t1))
    print(t3.timeIntervalSinceDate(t2))
    

    The result depends on the size of the input array, but @vacawama's updated method is the fastest:

    # of array   This      vacawama's   vacawama's
    elements:    method:   1st method:  2nd method:
    
      2          0.00016   0.00005      0.00001
      3          0.00043   0.00013      0.00004
      4          0.00093   0.00062      0.00014
      5          0.00335   0.00838      0.00071
      6          0.01756   0.24399      0.00437
      7          0.13625   11.90969     0.03692
    
    0 讨论(0)
  • 2020-12-09 13:13

    Try this.

    The general algorithm is to have a fromList containing the letters you haven't used yet and a toList that is the string you've built up so far. This uses recursion to build up all possible strings and adds them to the set when the length is 2 or greater:

    func permute(fromList: [String], toList: [String] = [String](), var set: Set<String> = Set<String>()) -> Set<String> {
        if toList.count >= 2 {
            set.insert(toList.joinWithSeparator(""))
        }
        if !fromList.isEmpty {
            for (index, item) in fromList.enumerate() {
                var newFrom = fromList
                newFrom.removeAtIndex(index)
                set = permute(newFrom, toList: toList + [item], set: set)
            }
        }
        return set
    }
    
    permute(["A", "B", "C"])
    // {"BA", "AC", "ABC", "AB", "BCA", "CB", "BC", "CAB", "ACB", "CA", "CBA", "BAC"}
    
    permute(["A", "A", "B"])
    // {"BA", "BAA", "AAB", "AB", "ABA", "AA"}
    

    Faster Answer:

    As @MartinR pointed out in his post, the solution above is a little slow because of all of the creation and copying of sets. I had originally written this using an inout variable for set, but changed it to the more functional interface to make it nice to call.

    Here is my original (faster) implementation, plus I embedded it in a permute that takes just an [String] and returns a Set<String>. It does the work of creating the set and the toList array and then calls the inner version of permute to do the real work:

    func permute(list: [String], minStringLen: Int = 2) -> Set<String> {
        func permute(fromList: [String], toList: [String], minStringLen: Int, inout set: Set<String>) {
            if toList.count >= minStringLen {
                set.insert(toList.joinWithSeparator(""))
            }
            if !fromList.isEmpty {
                for (index, item) in fromList.enumerate() {
                    var newFrom = fromList
                    newFrom.removeAtIndex(index)
                    permute(newFrom, toList: toList + [item], minStringLen: minStringLen, set: &set)
                }
            }
        }
    
        var set = Set<String>()
        permute(list, toList:[], minStringLen: minStringLen, set: &set)
        return set
    }
    
    permute(["A", "B", "C"])
    // {"BA", "AC", "ABC", "AB", "BCA", "CB", "BC", "CAB", "ACB", "CA", "CBA", "BAC"}
    
    permute(["A", "A", "B"])
    // {"BA", "BAA", "AAB", "AB", "ABA", "AA"}
    
    permute(["A", "A", "B"], minStringLen: 1)
    // {"BA", "A", "BAA", "AB", "AA", "B", "AAB", "ABA"}
    
    permute(["A", "A", "B"], minStringLen: 3)
    // {"ABA", "BAA", "AAB"}
    

    Edit: I added a minStringLen parameter (with default value of 2) instead of hard coding that value.

    See @MartinR's answer for performance comparisons.


    Swift 3 and Swift 4:

    func permute(list: [String], minStringLen: Int = 2) -> Set<String> {
        func permute(fromList: [String], toList: [String], minStringLen: Int, set: inout Set<String>) {
            if toList.count >= minStringLen {
                set.insert(toList.joined(separator: ""))
            }
            if !fromList.isEmpty {
                for (index, item) in fromList.enumerated() {
                    var newFrom = fromList
                    newFrom.remove(at: index)
                    permute(fromList: newFrom, toList: toList + [item], minStringLen: minStringLen, set: &set)
                }
            }
        }
    
        var set = Set<String>()
        permute(fromList: list, toList:[], minStringLen: minStringLen, set: &set)
        return set
    }
    
    print(permute(list: ["A", "B", "C"]))
    // ["ABC", "CA", "BAC", "ACB", "BA", "CAB", "BC", "CB", "BCA", "CBA", "AB", "AC"]
    
    print(permute(list: ["A", "A", "B"]))
    // ["AA", "AAB", "ABA", "AB", "BA", "BAA"]
    
    print(permute(list: ["A", "A", "B"], minStringLen: 1))
    // ["AAB", "ABA", "B", "BA", "A", "BAA", "AA", "AB"]
    
    print(permute(list: ["A", "A", "B"], minStringLen: 3))
    // ["AAB", "ABA", "BAA"]
    
    0 讨论(0)
  • 2020-12-09 13:13

    In your output example it wasn't clear what you really want, either:

    1. all combinations and permutations of them:

      ["AB", "BA", "AC", "CA", "AD", "DA", ..., "ABCD", "ABDC", "ACBD", "ACDB", ...]
      
    2. just all combinations:

      ["AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", ...]
      

    I can recommend @oisdk's great Swift library: SwiftSequence for both of them, it has lots of useful functions. In the Combinations section he even shows an example of its usage with the Power Set, which is almost exactly what you're looking for in case 1. Upon importing the files of his library, you can create the powerSet function on CollectionTypes like this:

    extension CollectionType {
        func powerSet() -> LazySequence<FlattenSequence<LazyMapSequence<Self, ComboSeq<Self.Generator.Element>>>>{
            var i = 0
            return lazy.flatMap{ _ in self.lazyCombos(++i) }
        }
    }
    

    This method evaluates lazily, which means that it's only evaluated when it really needs to. Now you mentioned that you only want to have combinations of at least 2 elements. This is easily done with the filter method:

    let combinations = ["A", "B", "C", "D"].powerSet().filter{ $0.count >= 2 }
        // As an array: [["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"]]
    

    For case 2. where you need permutations of those as well, you can do this:

    let combPerms = combinations.flatMap{ $0.permutations() }
        // As an array: [["A", "B"], ["B", "A"], ["A", "C"], ["C", "A"], ["A", "D"], ["D", "A"], ["B", "C"], ["C", "B"], ["B", "D"], ["D", "B"], ["C", "D"], ["D", "C"], ["A", "B", "C"], ["A", "C", "B"], ["B", "A", "C"], ["B", "C", "A"], ["C", "A", "B"], ["C", "B", "A"], ["A", "B", "D"], ["A", "D", "B"], ["B", "A", "D"], ["B", "D", "A"], ["D", "A", "B"], ["D", "B", "A"], ["A", "C", "D"], ["A", "D", "C"], ["C", "A", "D"], ["C", "D", "A"], ["D", "A", "C"], ["D", "C", "A"], ["B", "C", "D"], ["B", "D", "C"], ["C", "B", "D"], ["C", "D", "B"], ["D", "B", "C"], ["D", "C", "B"], ["A", "B", "C", "D"], ["A", "B", "D", "C"], ["A", "C", "B", "D"], ["A", "C", "D", "B"], ["A", "D", "B", "C"], ["A", "D", "C", "B"], ["B", "A", "C", "D"], ["B", "A", "D", "C"], ["B", "C", "A", "D"], ["B", "C", "D", "A"], ["B", "D", "A", "C"], ["B", "D", "C", "A"], ["C", "A", "B", "D"], ["C", "A", "D", "B"], ["C", "B", "A", "D"], ["C", "B", "D", "A"], ["C", "D", "A", "B"], ["C", "D", "B", "A"], ["D", "A", "B", "C"], ["D", "A", "C", "B"], ["D", "B", "A", "C"], ["D", "B", "C", "A"], ["D", "C", "A", "B"], ["D", "C", "B", "A"]]
    

    You can convert these to a Set of Strings or an Array:

    let array = Array(combPerms)
    let set = Set(combPerms)
    

    But I strongly recommend to use the lazy version ;) And yes, to remove duplicates you can just use Set(["A", "B", "C", "D"]) instead of just ["A", "B", "C", "D"]

    You can also do case 2. in one go like this:

    let x = ["A", "B", "C", "D"]
    
    let result = Set(
        x.indices
            .flatMap{ x.lazyCombos($0 + 1) }
            .filter{ $0.count >= 2 }
            .flatMap{ $0.permutations() }
            .map{ $0.joinWithSeparator("") })
    
    0 讨论(0)
提交回复
热议问题