从一个单词到另一个单词的快速解决方案

时间:2015-11-02 03:13:49

标签: swift parallel-processing swift2 grand-central-dispatch

我有一个家庭作业,其目标是让一个单词成为另一个单词,一次只更改一个单词。我选择在Swift中使用标准BSD字典(/usr/share/dict/words)作为我的词源。

以下代码按预期工作。但是,对于某些组合,它运行得相当慢。例如,define -> system

我可以使用Grand Central Dispatch和并行处理来加快速度吗?非常感谢!

import Foundation

typealias CString = [CChar]

// Get list of words from the standard BSD dictionary
let wordList = try! String(contentsOfFile: "/usr/share/dict/words")
    .componentsSeparatedByString("\n")
    .map { $0.lowercaseString.cStringUsingEncoding(NSUTF8StringEncoding)! }


func distance(fromCString: CString, toCString: CString) -> Int {
    guard fromCString.count == toCString.count else {
        fatalError("The two strings must have the same number of characters")
    }

    var distance = 0
    for i in 0..<fromCString.count {
        if fromCString[i] != toCString[i] {
            distance++
        }
    }
    return distance
}

func find(start: String, _ end: String) -> [String]? {
    guard start.characters.count == end.characters.count else {
        fatalError("'\(start)' and '\(end)' must have the same number of characters")
    }

    // String is slow. Switch to bytes array for speed
    let startCString = start.cStringUsingEncoding(NSUTF8StringEncoding)!
    let endCString   = end.cStringUsingEncoding(NSUTF8StringEncoding)!

    // Get list of words with same length
    var candidates = wordList.filter { $0.count == startCString.count }

    // If either start or end is not in the dictionary, fail
    guard (candidates.contains { $0 == startCString }) else {
        fatalError("'\(start)' is not in the dictionary")
    }
    guard (candidates.contains { $0 == endCString }) else {
        fatalError("'\(end)' is not in the dictionary")
    }

    // Do the search
    var i = 0
    var iterations = 0
    var queue = [[CString]]()
    queue.append([startCString])

    while queue.count > 0, let lastWord = queue[0].last where lastWord != endCString {
        iterations++
        i = 0

        while i < candidates.count {
            if candidates[i] == lastWord {
                candidates.removeAtIndex(i)
            } else if distance(lastWord, toCString: candidates[i]) == 1 {
                queue.append(queue[0] + [candidates[i]])
                candidates.removeAtIndex(i)
            } else {
                i++
            }
        }

        queue.removeAtIndex(0)
    }

    if queue.isEmpty {
        print("Cannot go from '\(start)' to '\(end)'")
        return nil
    } else {
        return queue[0].map { String(CString: $0, encoding: NSUTF8StringEncoding)! }
    }
}

示例:

find("bat", "man")       // bat -> ban -> man. 190 iterations, fast.
find("many", "shop"))    // many -> mand -> main -> said -> saip -> ship -> shop. 4322 iterations, medium

find("define", "system") // impossible
find("defend", "oppose") // impossible

1 个答案:

答案 0 :(得分:3)

非常有趣的小问题。在字典中使用本机索引可以让您更快地完成此操作。此外,您可以通过同时从两端搜索单词链来显着减少迭代次数。

我做了一个设置时间较长的例子,但是一旦完成了单词索引,单词链结果几乎是瞬时的。

// --- wordPath.swift ---

// create a list of link patterns for a word
// ------------------------------------------
// A link pattern is an ordered list of letters in a word where one letter is missing
//
// This corresponds to a combination of letters that can from the word
// by adding a single letter and, conversely to combinations of letters that
// are possible by removing a letter from the word
//
// Letters are sorted alphabetically in order to ignore permutations
//
// for example:   
//
//   "corner" has 6 link patterns (one of which is a duplicate)
//                
//   the sorted letters are : "cenorr"
//
//   the links are the 6 combinations of 5 out of 6 letters of "cenorr"
//   with letters kept in alphabetical order.
//
//   because letters are sorted, the combinations can be obtained by
//   removing one letter at each position.
//
//   removing position 1 : enorr
//   removing position 2 : cnorr
//   removing position 3 : ceorr
//   removing position 4 : cenrr 
//   removing position 5 : cenor 
//   removing position 6 : cenor (this is the duplicate)
//  
public func linksOfWord(_ word:String)->[String]
{
  var result:[String] = []

  // sort letters
  let sortedLetters = word.characters.sorted()

  // return one string for each combination of N-1 letters
  // exclude duplicates (which will always be consecutive)
  var wordLink = ""
  for skipChar in sortedLetters.indices
  { 
    let nextLink = String(sortedLetters[0..<skipChar]) 
                 + String(sortedLetters[skipChar+1..<sortedLetters.count]) 
    if nextLink == wordLink { continue }

    wordLink = nextLink
    result.append(wordLink)
  }

  return result
}


// Create an index of wordlinks to the words that can be fromed from them
// ----------------------------------------------------------------------
// - The wordLinks dictionary contains a list of all the words that can be formed
//   by adding one letter to a given link pattern
// - This is essentially the reverse of linksOfWord for all words in the dictionary
// - note: Swift will use lazy initialization for this global variable, only executing the closure once.
//
public var wordsOfLink:[String:[String]] = 
{
  var result:[String:[String]] = [:]

  // get all words
  let wordList = try! String(contentsOfFile: "/usr/share/dict/words")
                      .lowercased()
                      .components(separatedBy:"\n")

  // add each word to the index of its wordLinks                        
  for word in wordList
  {
     for wordLink in linksOfWord(word)
     {
        result[wordLink] = (result[wordLink] ?? []) + [word]
     }
  }

  return result   
}()

// Iteration counted, for comparison with OP
public var iterations = 0

// word path seeking algorithm
// ---------------------------
// - Go through word paths using linksOfWord and wordsOfLink as a virtual tree structure
//   linksOfWord("word") -> 1:N array of links -> wordsOfLink("Link") -> 1:N array of words
// - Performs a breadth-first tree search by exausting shorter path lengths before moving on to longer ones
// - Simultaneously search forward and backward to minimize the exponential nature of path expansions
//

public func findWordPath(from fromWord:String, to toWord:String, shortestPath:Bool=false) -> [String]?
{
   iterations = 0

   // both words must be same length
   guard fromWord.characters.count == toWord.characters.count
   else  { return nil }

   // processing in lowercase only
   let startWord = fromWord.lowercased()
   let endWord   = toWord.lowercased()

   // keep track of links already processed (avoid infinite loops)
   var seenLinks = Set<String>() 
   var seenWords = Set<String>()

   // path expansion from starting word forward to ending word  
   var forwardLinks:[String:[String]] = [:]           // links that have a forward path connecting to the starting word
   var forwardPaths                   = [[startWord]] // partial forward paths to examine
   var forwardIndex                   = 0             // currently examined forwardPath (index) 

   // path expansion from ending word backward to starting word
   var backwardLinks:[String:[String]] = [:]            // links that have a backward path connecting to the ending word
   var backwardPaths                   = [[endWord]]    // partial backward paths to examine
   var backwardIndex                   = 0              // currently examined backwardPath (index)

   // establish initial links to start and end words 
   // - allows logic to only check path to path connections 
   //   (and not path to word in addition to it)
   linksOfWord(startWord).forEach{forwardLinks[$0] = [startWord]}
   linksOfWord(endWord).forEach{backwardLinks[$0]  = [endWord]}


   // Explore shorter paths in each direction before moving on to longer ones
   // This improves performance but does not guarantee the shortest word path 
   // will be selected when a connection is found 
   // e.g. forward(4)->backward(3) could be found before backward(4)->forward(2)
   //      resulting in a 7 word path when a 6 word path exists.
   // Finding the shortest possible word path requires that we explore forward only
   // (which is what the shortestPath parameter controls)
   var currentLength = 1

   // Examine partial word paths in multiple passes with an increasing path length (currentLength)
   // - For each path length, check if forward and backward paths can connect to one another.
   // - For each length, forwardIndex and backwardIndex advance through the paths
   //   of the current length thus exhausting both forward and backward paths of that length
   //   before moving on to the next length.
   // - As paths of the current length are examined, the partial path arrays receive new (longer) word paths
   //   to examine. 
   // - The added paths have 1 additional word which creates a new group of paths for the next pass
   //   at currentLength + 1
   // - When none of the partial word paths can be extended by adding a word that was not seen before
   //   the forward or backward path array will stop growing and the index will reach the end of the array
   //   indicating that no word path exists between start and end words
   while forwardIndex < forwardPaths.count 
         && ( backwardIndex < backwardPaths.count || shortestPath )
   {
       // Examine forward path links (of current length) for connection
       // to the end word or to a backward path that connects to it
       while forwardIndex < forwardPaths.count
          && forwardPaths[forwardIndex].count == currentLength
       {
          let forwardPath = forwardPaths[forwardIndex]

          forwardIndex   += 1
          iterations     += 1

          // expand links for last word of "forward" path
          for wordLink in linksOfWord(forwardPath.last!) 
          {
             // link connects to a backward path, return the combined forward and backward paths
             if let backwardPath = backwardLinks[wordLink]
             { return forwardPath + backwardPath }

             // don't explore links that have already been examined
             if !seenLinks.insert(wordLink).inserted
             { continue }

             // record forward path to allow linking from backward paths
             // i.e. this link is known to lead to the starting word (through forwardPath)
             if !shortestPath
             { forwardLinks[wordLink] = forwardPath }

             // for all words that can be created by adding one letter to the word link   
             // add new forward paths to examine on the next length pass
             for word in wordsOfLink[wordLink] ?? []
             { 
               if seenWords.insert(word).inserted
               {
                 forwardPaths.append(forwardPath + [word])
               }
             }
          } 
       }

       // Examine backward path links (of current length) for connection
       // to the start word or to a forward path that connects to it
       // allowing one length backward path to support unknown end words
       while !shortestPath
          && backwardIndex < backwardPaths.count
          && backwardPaths[backwardIndex].count == currentLength
       {
          let backwardPath = backwardPaths[backwardIndex]

          backwardIndex   += 1
          iterations      += 1

          // expand links for first word of "backward" path
          for wordLink in linksOfWord(backwardPath.first!)
          {
             // link connects to starting path, combine and return result
             if let forwardPath = forwardLinks[wordLink]
             { return forwardPath + backwardPath }

             // don't explore links that have already been examined
             if !seenLinks.insert(wordLink).inserted
             { continue }

             // record backward path to allow linking from forward paths
             // i.e. this link is known to lead to the ending word (through backwardPath)
             backwardLinks[wordLink] =  backwardPath

             // for all words that can be created by adding one letter to the word link   
             // add new backward paths to examine on the next length pass
             for word in wordsOfLink[wordLink] ?? []
             { 
                if seenWords.insert(word).inserted
                {
                  backwardPaths.append( [word] + backwardPath ) 
                }
             }
          } 
       }

       // all forward and backward paths of currentLength have been examined
       // move on to next length
       currentLength += 1
   }

   // when either path list is exausted, there are no possible paths.
   return nil
}

...

// --- Playground ---

// compute word path and print result
func printWordPath(_ firstWord:String, _ lastWord:String)
{
  print("    printWordPath(\"\(firstWord)\",\"\(lastWord)\")")

  let startTime = ProcessInfo().systemUptime
  if let foundPath = findWordPath(from:firstWord, to:lastWord, shortestPath:false)
  { 
    let time = String(format:"%.5f",ProcessInfo().systemUptime-startTime)
    print("    //\(foundPath.count) words :  \(foundPath)\n    //        \(iterations) iterations, \(time) sec ")
  }
  else
  { print("    // No Path Found between \(firstWord) and \(lastWord)") }
  print("")
}

printWordPath("bat","man")
// 3 words :  ["bat", "amt", "man"]
//        16 iterations, 6.34718 sec <-- longer time here because of initializations

printWordPath("many","shop")
// 5 words :  ["many", "hymn", "homy", "hypo", "shop"]
//        154 iterations, 0.00752 sec 

printWordPath("many","star")
// 4 words :  ["many", "maty", "mast", "star"]
//        101 iterations, 0.00622 sec 

printWordPath("define","system")
// 6 words :  ["define", "fenite", "entify", "feisty", "stymie", "system"]
//        574 iterations, 0.02374 sec 

printWordPath("defend","oppose")
// 6 words :  ["defend", "depend", "depone", "podeon", "pooped", "oppose"]
//        336 iterations, 0.01273 sec 

printWordPath("alphabet","integers")
// 7 words :  ["alphabet", "lapithae", "apterial", "epistlar", "splinter", "sterling", "integers"]
//        1057 iterations, 0.04454 sec 

printWordPath("Elephant","Microbes")
// 8 words :  ["elephant", "antelope", "lapstone", "parsonet", "somepart", "imposter", "comprise", "microbes"]
//        2536 iterations, 0.10133 sec 

printWordPath("Microbes","Elephant")
// 8 words :  ["microbes", "comprise", "persicot", "petrolic", "copalite", "pectinal", "planchet", "elephant"]
//        2232 iterations, 0.09649 sec 

printWordPath("Work","Home")
// 4 words :  ["work", "worm", "mohr", "home"]
//        52 iterations, 0.00278 sec 

printWordPath("Head","Toes")
// 4 words :  ["head", "ahet", "ates", "toes"]
//        146 iterations, 0.00684 sec 

printWordPath("North","South")
// 3 words :  ["north", "horst", "south"]
//        22 iterations, 0.00189 sec 

printWordPath("Employee","Pesident")
// 7 words :  ["employee", "employed", "redeploy", "leporide", "pedelion", "disponee", "pesident"]
//        390 iterations, 0.01810 sec 

printWordPath("Yellow","Orange")
// 5 words :  ["yellow", "lowery", "royale", "royena", "orange"]
//        225 iterations, 0.01025 sec 

printWordPath("Whale","shark")
// 4 words :  ["whale", "hawse", "asher", "shark"]
//        94 iterations, 0.00473 sec 

printWordPath("Police","Fellon")
// 4 words :  ["police", "pinole", "lionel", "fellon"]
//        56 iterations, 0.00336 sec 

printWordPath("religion","insanity")
// 6 words :  ["religion", "triolein", "reinstil", "senility", "salinity", "insanity"]
//        483 iterations, 0.02411 sec 

printWordPath("ebony","ivory")
// 4 words :  ["ebony", "eryon", "irony", "ivory"]
//        53 iterations, 0.00260 sec 

printWordPath("electronic","mechanical")
// 7 words :  ["electronic", "citronelle", "collineate", "tellinacea", "chatelaine", "atechnical", "mechanical"]
//        316 iterations, 0.01618 sec 

printWordPath("detrimental","appropriate")
// 7 words :  ["detrimental", "pentremital", "interpolate", "interportal", "prerational", "preparation", "appropriate"]
//        262 iterations, 0.01319 sec 

请注意,这可以找到前两个示例的解决方案,我也注意到很多 - &gt; mand - &gt;主要 - &gt;说 - &gt; ......正在取代来自&#34; main&#34;到&#34;说&#34;,所以我最终得到了一条不同的道路。

顺便说一下,我把这些函数放在操场上的一个单独的文件中(在Sources下),因为行执行的计数使执行速度变慢,直到爬行。

[编辑]改变迭代次数使其与OP代码的计数方式更加一致。添加了更多示例(对此有太多乐趣)。

[EDIT]改编为Swift 3,增加了对逻辑的更多解释,进一步简化/优化了代码。