高效的置换树算法

时间:2015-11-30 13:35:31

标签: ruby algorithm recursion sudoku

我试图编写一个算法来解决任何数独游戏。然而,我原来的方法是有缺陷的,因为我没有意识到一个结构良好的数独应该只有一个解决方案。

因此,结果算法没有针对数独求解进行优化,而且更为通用:它递归地生成当前数独布局的每个可能允许的结果/布局。因此,该算法在理论上可以找到空白数独的每个解决方案(但我/我们/人类可能不会看到输出)。

我的第一个问题是:对于这种类型的搜索算法,什么是最佳方法 - 我的算法如何改进?总体战略是:

  • 在分支点尝试所有可能的解决方案
  • 如果无法解决单元格,则终止分支
  • 如果达成解决方案,则终止分支

结构和方法改编自为解决SICP(https://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html)中的八皇后问题而提出的时间解耦解决方案,似乎这是一种反向跟踪算法(状态为“时间” “推入机器”,这是真的吗?

我的第二个问题是,在处理递归算法时,如何使用Ruby> = 2.x Lazy功能生成可枚举?

期望的结果是做一些像solve(sudoku).first(x)这样的东西来检索第一个x解决方案。即解决方案存储为流/序列。我遇到的困难是递归生成嵌套的枚举,我正在努力寻找一种方法来传递一个强制方法通过整个树。

我在下面列出了密钥代码,但可以在此处找到完整的方法套件:https://gist.github.com/djtango/fe9322748cf8a055fc0e

def solve(sudoku)
  guess(simplify_sudoku(sudoku))
end

def guess(sudoku, row = 0, column = 0)
  return sudoku.flatten                 if end_of_grid?(row, column)
  return guess(sudoku, row + 1, 0)      if end_of_column?(column)
  return guess(sudoku, row, column + 1) if filled?(sudoku, row, column)
  return nil                            if insoluble?(sudoku, row, column)
  permissible_values(sudoku, row, column).map do |value|
    guess(update_sudoku(sudoku, value, row, column), row, column + 1)
  end
end

def simplify_sudoku(sudoku)
  return sudoku if no_easy_solutions?(sudoku)
  simplify_sudoku(fill_in(sudoku))
end

def fill_in(sudoku)
  new_sudoku = copy_sudoku(sudoku)
  new_sudoku.map.with_index do |row, row_index|
    row.map.with_index do |column, column_index|
      solution = permissible_values(new_sudoku, row_index, column_index)
      easy_and_not_filled?(solution, new_sudoku, row_index, column_index) ? [solution.first] : column
    end
  end
end

在尝试实现延迟评估时,引入惰性方法的最自然点是猜测方法,如下所示:

  permissible_values(sudoku, row, column).map do |value|
    guess(update_sudoku(sudoku, value, row, column), row, column + 1)
  end

但在实施时,结果是:

solve(blank_sdk)
 => #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8, 9]>:map> 

solve(blank_sdk).first(10)
 => [#<Enumerator::Lazy: #<Enumerator::Lazy: [2, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8]>:map>] 

solve(blank_sdk).first(1000000000000)
 => [#<Enumerator::Lazy: #<Enumerator::Lazy: [2, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8]>:map>] 

1 个答案:

答案 0 :(得分:1)

您不需要在每个分支点尝试所有可能的解决方案。您可以通过找到剩余选项数最少的单元格并尝试其中的变体来做得更好。这样,您可以快速设置仅剩下一个选项的所有单元格。当你需要尝试不同的解决方案时,你可能只会尝试一些变种(2-3),而不是所有的可能性,所以它应该更快。如果它们都不适用于那个单元格,那么你可以停止,因为没有解决方案。