【问题标题】:Sudoku Solver in SML with Backtracking带回溯的 SML 中的数独求解器
【发布时间】:2016-02-16 02:45:45
【问题描述】:

我正在使用 SML/NJ 创建数独求解器。我已经准备好所有功能来实际操作输入数据(检查行的合法性,强制空格等),但我在回溯部分遇到了麻烦。

我遇到了this question,但我对如何在 SML 中实现它感到困惑。

请注意,棋盘是作为列表列表输入的,表示每行中的数字,0 表示未知点

[[0,0,0, 2,6,0, 7,0,1],
[6,8,0, 0,7,0, 0,9,0],
[1,9,0, 0,0,4, 5,0,0],

[8,2,0, 1,0,0, 0,4,0],
[0,0,4, 6,0,2, 9,0,0],
[0,5,0, 0,0,3, 0,2,8],

[0,0,9, 3,0,0, 0,7,4],
[0,4,0, 0,5,0, 0,3,6],
[7,0,3, 0,1,8, 0,0,0]]

这是我的(已编辑)solve 函数。

exception Sudoku;
fun solve board =
  let fun solve' board k =
    (* k is the row we are working on, so if it's 9, we SHOULD be done *)
    if k = 9 then board else
    let
      (* get row k from the board *)
      val row = (List.nth (board, k));

      fun trySpot number col =
        (* if number >= 10, raise an exception to backtrack *)
        if number > (length row) then raise Sudoku
        (* if col = 9, raise an exception to backtrack *)
        else if col = 9 then raise Sudoku
        (* if row[col] is not a zero, move to next col *)
        else if not (List.nth(row, col) = 0) then trySpot number (col + 1)
        (* row doesn't contain this num already *)
        else if length (List.filter (fn x => x = number) row) = 0 then
          let
            (* build the new row and board and check if legal (this works fine) *)
            val newRow = delete(col + 1, (insertAtPos row number col));
            val newBoard = delete(k + 1, (insertAtPos board newRow k));
            val isLegal = checkLegal newBoard;
          in
            (* if legal, keep solving with new board as base *)
            if isLegal then
              solve' (force newBoard) 0
              handle Sudoku => solve' (force board) (k + 1)
            (* not legal, try with next num *)
            else trySpot (number + 1) col
          end
        (* row already has this number, skipping *)
        else trySpot (number + 1) col
    in
      (* if board is complete and legal we're done *)
      if completedBoard board andalso checkLegal board then board
      (* if row has a zero then try a spot *)
      else if (zeroInList row) then trySpot 1 0
      (* otherwise move to next row *)
      else solve' (force board) (k + 1)
    end
  in
    (* initial solve *)
    solve' (force board) 0
  end; 

对上面的示例数据调用solve会返回以下列表

[[4,3,5,2,6,9,7,8,1],
[6,8,2,5,7,1,4,9,3],
[1,9,7,8,3,4,5,6,2],

[8,2,6,1,9,5,3,4,7],
[3,1,4,6,8,2,9,0,0],
[0,5,0,0,0,3,0,2,8],

[0,0,9,3,0,0,0,7,4],
[2,4,0,0,5,0,0,3,6],
[7,0,3,0,1,8,0,0,0]]

现在这是部分正确的。根据我曾经检查过的在线数独求解器,前四行看起来完全正确,但在第 5 行就搞砸了。我猜是因为它无法一路回溯。

它“备份”的唯一地方就是这一行

handle Sudoku => solve' (force board) (k + 1)

这告诉它只尝试解决旧板(没有新号码),但这会阻止它回溯不止一步(我认为)。这怎么可能实现?

如果有人好奇想看完整代码,可以找here.

提前致谢!

【问题讨论】:

  • Hansen 和 Rischel 所著的“SML 编程简介”一书包含一个回溯算法示例,用于解决使用异常实现的 8 个皇后问题。我能够毫不费力地修改他们的代码以获得骑士之旅。它可能会给你一些想法(尽管现在我更可能使用选项而不是例外)。
  • 谢谢推荐,我去看看
  • @JohnColeman,我看了一下他们的实现,并尝试修改我的求解函数以实现带异常的回溯。它在一定程度上有效,但我不确定如何让它进一步回到决策树中。有什么想法吗?
  • 目前没有想法,我从来没有做过多少数独游戏(即使是手工也没有——我更喜欢填字游戏和密码)。我只是认为这本书会给你一些想法。如果我这周晚些时候有时间,我会考虑一下,但接下来的几天对我来说很忙。

标签: sml sudoku backtracking smlnj


【解决方案1】:

我似乎已经让我的求解功能适用于我尝试过的所有案例。诀窍是跟踪我们正在处理的地点的非法号码列表。求解器现在将跳过这些数字,如果找不到前进的路径则回溯。

exception Sudoku;
(* solves a soduku board input that gives a list of lists representing the rows of a board (0 for unknown spaces) *)
fun solve board = 
  let fun solve' board row illegalNums = 
    (* if we are on row 9 and board is complete/legal, we are done *)
    if row = 9 andalso completedBoard board andalso checkLegal board then board else
    (* if we are on row 9 but the board is not complete/legal, throw an exception to backtrack *)
    if row = 9 then raise Sudoku else 
    let
      (* get the current row we are working on *)
      val cRow = (List.nth (board, row));

      (* trys a row[col] on the board with a certain number *)
      fun trySpot num cRow col =
        (* if number >= 10, raise an exception to backtrack *)
        if num > 9 then raise Sudoku
        (* if row[col] is not a 0, try next col *)
        else if not (List.nth (cRow, col) = 0) then trySpot num cRow (col + 1)
        (* if we know that the number we are on isn't allowed, skip to next number *)
        else if length (List.filter (fn x=> x = num) illegalNums) > 0 then trySpot (num + 1) cRow col
        (* if row already has this number, skip to next number *)
        else if length (List.filter (fn x=> x = num) cRow) > 0 then trySpot (num + 1) cRow col
        (* if col already has this number, skip to next number *)
        else if length (List.filter (fn x=> x = num) (List.nth((rowsToCols board), col))) > 0 then trySpot (num + 1) cRow col
        else 
          let
            (* make our new row and board *)
            val newRow = delete(col + 1, (insertAtPos cRow num col));
            val newBoard = delete(row + 1, (insertAtPos board newRow row));
          in
            (* if new board is legal, continue solving *)
            if checkLegal newBoard then
              solve' (force newBoard) 0 []
              (* handle any exceptions by adding the number to our list of illegal numbers, thereby backtracking *)
              handle Sudoku => solve' (force board) row (illegalNums@[num])
            (* if new board is not legal, try next number *)
            else trySpot (num + 1) cRow col
          end
    in
      (* if board is completed and legal, return board *)
      if completedBoard board andalso checkLegal board then board
      (* if current row has at least one 0, try a number in that row (beginning at col 1) *)
      else if (zeroInList cRow) then trySpot 1 cRow 0
      (* else, skip to next row and solve *)
      else solve' (force board) (row + 1) []
    end
  in
    (* initial solve *)
    solve' (force board) 0 []
  end;

较难的电路板可能需要相当长的时间才能完全解决,但我测试过的每一个最终都能达到。我确信我可以在我的代码中进行大量优化,因此我愿意接受建议。

【讨论】:

  • 我很高兴你让它工作。也许您可以将其发布在 Code Review 上以获得更多反馈。我认为一个改进是摆脱有利于选项的例外。返回NONE 会替换引发异常,而针对NONE 的模式匹配会替换handle。我在 O'CAML 中读到,至少使用选项比引发异常要快得多,因为它不涉及展开调用堆栈。看到这个问题:stackoverflow.com/q/7952625/4996248
  • 多读一点,我不确定使用选项是否会节省时间,尽管我仍然认为选项在概念上比例外更清晰。可能会产生影响的一件事是用vectors 替换列表 - 用于您的板表示 - 您的板具有固定大小,因此您不需要列表增长的能力,并且向量的O(1) 访问时间应该可能成为一种收获。
  • @JohnColeman 感谢您的建议。我将看看选项而不是例外。不幸的是,我被列表困住了。这是一个班级的作业,我们的教授希望我们使用普通的旧列表。不过,我可能仍然会四处乱逛,看看向量是否有所改进,所以谢谢你的想法!
  • 听起来你有个好教授。真正学习一门语言的唯一方法是将其用于解决既不平凡又有趣的问题。
猜你喜欢
  • 2023-03-27
  • 1970-01-01
  • 2014-02-07
  • 1970-01-01
  • 2016-05-25
  • 1970-01-01
  • 2017-12-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多