优化回溯算法解决数独

时间:2009-10-05 05:09:01

标签: algorithm sudoku

我希望为我的Sudoku Solver优化我的回溯算法。


它现在做了什么:

递归求解器函数采用具有各种给定值的数独谜题。

我将浏览拼图中的所有空插槽,寻找可能性最小的插槽,并获取值列表。

从值列表中,我将通过将列表中的一个值放入插槽中来循环遍历它,并递归求解它,直到整个网格被填满。


这个实现对于一些难题仍然需要非常长的时间,我希望进一步优化这一点。有没有人有任何想法我怎么能够进一步优化这个?


如果你感兴趣的话,这是我的Java代码。

public int[][] Solve(int[][] slots) {
    // recursive solve v2 : optimization revision

    int[] least = new int[3];
    least[2] = Integer.MAX_VALUE;
    PuzzleGenerator value_generator = new PuzzleGenerator();
    LinkedList<Integer> least_values = null;

    // 1: find a slot with the least possible solutions
    // 2: recursively solve.

    // 1 - scour through all slots.
    int i = 0;
    int j = 0;
    while (i < 9) {
        j = 0;
        while (j < 9) {
            if (slots[i][j] == 0) {
                int[] grid_posi = { i, j };
                LinkedList<Integer> possible_values = value_generator
                        .possibleValuesInGrid(grid_posi, slots);
                if ((possible_values.size() < least[2])
                        && (possible_values.size() != 0)) {
                    least[0] = i;
                    least[1] = j;
                    least[2] = possible_values.size();
                    least_values = possible_values;
                }
            }
            j++;
        }
        i++;
    }

    // 2 - work on the slot
    if (least_values != null) {
        for (int x : least_values) {
            int[][] tempslot = new int[9][9];
            ArrayDeepCopy(slots, tempslot);
            tempslot[least[0]][least[1]] = x;

            /*ConsoleInterface printer = new gameplay.ConsoleInterface();
            printer.printGrid(tempslot);*/

            int[][] possible_sltn = Solve(tempslot);
            if (noEmptySlots(possible_sltn)) {
                System.out.println("Solved");
                return possible_sltn;
            }
        }
    }
    if (this.noEmptySlots(slots)) {
        System.out.println("Solved");
        return slots;
    }
    slots[0][0] = 0;
    return slots;
}

9 个答案:

答案 0 :(得分:6)

我有一项任务就是:用Java构建最快的数独求解器。我最终以0.3毫秒的时间赢得了比赛。

我没有使用跳舞链接算法并且没有与它进行比较,但是一些参赛者必须尝试过,但我最接近的竞争对手花了大约15毫秒。

我只是使用递归回溯算法,用4个“规则”对其进行扩充(这使得几乎每个谜题都不需要回溯)并且将一个字段保存为每个位置的合法值列表。

我写了一篇关于它的博客文章并在此处发布了代码:http://www.byteauthor.com/2010/08/sudoku-solver/

答案 1 :(得分:4)

很长一段时间我写了一个数独求解器(几年前,但我保留了我写的所有代码)。它没有被普遍解决比通常的数独“更大”的尺寸,但它非常快。

它在103毫秒内解决了以下问题(在Core 2 Duo 1.86 Ghz上)并且还没有进行优化:

        {0,0,0,0,7,0,9,4,0},
        {0,7,0,0,9,0,0,0,5},
        {3,0,0,0,0,5,0,7,0},
        {0,8,7,4,0,0,1,0,0},
        {4,6,3,0,0,0,0,0,0},
        {0,0,0,0,0,7,0,8,0},
        {8,0,0,7,0,0,0,0,0},
        {7,0,0,0,0,0,0,2,8},
        {0,5,0,2,6,8,0,0,0},            

你的速度有多快,哪个板慢?你确定你不经常重访不应该重访的路径吗?

以下是算法的内容:

private static void solveRec( final IPlatform p ) {
    if (p.fullBoardSolved()) {
        solved = p;
        return;
    }
    boolean newWayTaken = false;
    for (int i = 0; i < 9 && !newWayTaken; i++) {
        for (int j = 0; j < 9 && !newWayTaken; j++) {
            if (p.getByteAt(i, j) == 0) {
                newWayTaken = true;
                final Set<Byte> s = p.avail(i / 3, j /3);
                for (Iterator<Byte> it = s.iterator(); it.hasNext();) {
                    final Byte b = it.next();
                    if (!p.columnContains(j, b) && !p.lineContains(i, b)) {
                        final IPlatform ptemp = duplicateChangeOne(p, b, i, j);
                        solveRec(ptemp);
                        if (solved != null) {
                            return;
                        }
                    }
                }
            }
        }
    }
}

和IPlatform抽象(请很好,它是在很多年前编写的,之前我知道在Java中添加'I'之前接口名称并不是风靡一时):

public interface IPlatform {

    byte getByteAt(int i, int j);

    boolean lineContains(int line, int value);

    boolean columnContains(int column, int value);

    Set<Byte> avail(int i, int j);

    boolean fullBoardSolved();

}

答案 2 :(得分:2)

在每个非确定性步骤之前进行一些约束传播。

在实践中,这意味着您有一些规则可以检测强制值并插入它们,并且只有当这不再取得进展时,您才需要回溯搜索可能的值。

大多数人类数独游戏的设计都是为了让它们根本不需要回溯。

答案 3 :(得分:2)

不久前,我在Ruby中实现了Donald Knuth的Dancing Links和他的算法X for Sudoku(一种不太知名的语言)。对于我检查过的几个例子,我的1.5 GHz笔记本电脑花了几毫秒。

你可以看一下wikpedia的Dancing Links如何运作,并自己适应Sudoku。或者你看看"A Sudoku Solver in Java implementing Knuth’s Dancing Links Algorithm"

PS:算法X是一种回溯算法。

答案 4 :(得分:1)

我认为一个重要的优化不仅是保持电路板的状态,而且如果它包含每个数字1-9,则保持每行/列/平方。现在要检查一个位置是否可以有一个数字,你只需要检查位置所在的行/列/方是否包含该数字(这只是3个数组查找)。

同样大的速度损失必须为每个递归调用创建一个新数组。而不是这样做在递归调用之前在数组中进行更改,然后在递归调用之后撤消它。基本上添加Solve在运行时会更改插槽的不变量,但是当它返回时它将保留它,就像调用函数时一样。

每次解决退货时,您都必须检查电路板是否已解决。如果求解没有找到解决方案,它应该只返回null,如果找到解决方案它应该返回。通过这种方式,您可以快速测试是否递归调用求解或是否找到了解决方案。

将数字放在最少选项的方块中真的有帮助吗?没有它,代码就会简单得多(你不必在链表中保存等等。)

这是我的伪代码:

for(square on the board)
      for(possible value)
           if(this square can hold this value){
                place value on the board
                update that this row/col/square now contains this value

                recursive call
                if recursive call succeeded return the value from that call

                update that this row/col/square does not contain this value
                undo placing value on board
           }
if (no empty squares)
    return solved

这是我的代码(我还没有测试过):

public int[][] solve(int[][] board, boolean[][] row, boolean[][] col, boolean[][] square){
    boolean noEmpty = true;
    for(int i = 0; i < 9;i++){
        for(int j = 0; j < 9;j++){
            if(board[i][j] == 0){
                noEmpty = false;
                for(int v = 1; v <= 9; v++){
                    int sq = (i/3)*3+(j/3);
                    if(row[i][v-1] == false && col[j][v-1] == false && square[sq][v-1] == false){
                        board[i][j] = v;
                        row[i][v-1] = true;
                        col[j][v-1] = true;
                        square[sq][v-1] = true;
                        int[][] ans = solve(board,row,col,square);
                        if(ans != null)
                            return ans;
                        square[sq][v-1] = false;
                        col[j][v-1] = false;
                        row[i][v-1] = false;
                        board[i][j] = 9;
                    }
                }
            }
        }
    }
    if(noEmpty){
        int[][] ans = new int[9][9];
        for(int i = 0; i < 9;i++)
            for(int j = 0; j < 9;j++)
                ans[i][j] = board[i][j];
        return ans;
    }else{
        return null;
    }       
}

答案 5 :(得分:1)

使用尽可能少的解决方案查找插槽非常昂贵,对于传统的Sudoku拼图可能不值得花费。

更简单的优化是跟踪每个数字的使用次数,当您“尝试”将数字放入插槽时,从最少使用的数字开始(编辑:确保包括拼图播种的那些)。这将使您的算法更有可能开始成功的路径,而不是失败的路径。

另外,请按照Imsasu的建议查看Artificial Intelligence: A Modern Approach。这是一本很棒的书,涵盖了非常详细的递归回溯。

P.S。我很好奇你的“第1步”优化给出的性能提升(如果有的话)。你有一个人物吗?

答案 6 :(得分:0)

您应该使用分析器来查看哪个语句占用的时间最多,然后考虑如何优化它。

不使用分析器,我的建议是你每次都从头开始创建一个新的PuzzleGenerator,并将slot作为参数传递给possibleValuesInGrid方法。我认为这意味着PuzzleGenerator每次都会重新计算每个位置和每个插槽配置的所有内容;相反,如果它记住以前的结果并逐渐改变,它可能会更有效率。

答案 7 :(得分:0)

我对Sudoku的回溯算法的优化结果如下。您可以从http://yikes.com/~bear/suds.c下载代码。这完全基于鸽子洞原理,我发现它通常比基于规则的解决更快。

使用此线程上另一个帖子的值,我在core2 duo @ 2.2 ghz上获得7ms的结果,或者在核心i5上获得3ms的结果。这与海报的100ms结果相比,尽管可能是以不同的方式测量的。时间已添加到http://yikes.com/~bear/suds2.c

我10年前写过这篇文章,如果我重新解决这个问题,肯定会以不同的方式进行优化。

$ ./a.out 000070940070090005300005070087400100463000000000007080800700000700000028050268000
[----------------------- Input  Data ------------------------]

*,*,*   *,7,*   9,4,*   
*,7,*   *,9,*   *,*,5   
3,*,*   *,*,5   *,7,*   

*,8,7   4,*,*   1,*,*   
4,6,3   *,*,*   *,*,*   
*,*,*   *,*,7   *,8,*   

8,*,*   7,*,*   *,*,*   
7,*,*   *,*,*   *,2,8   
*,5,*   2,6,8   *,*,*   

[------------------ Solution 01 -------------------]

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

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

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

Time: 0.003s Cyles: 8619081 

答案 8 :(得分:0)

我最近用Python编写了一个可以解决数独谜题的程序。它基本上是一种强制搜索空间的回溯算法。我已经发布了有关实际算法in this thread的更多详细信息。

然而,我想更多地关注优化过程。更确切地说,我已经探索了不同的方法来最小化求解时间和迭代次数。这更多是关于可以进行的算法改进,而不是编程改进。

所以考虑到这一点,回溯蛮力算法中没有很多东西可以优化(很高兴在这里被证明是错误的)。可以做出的两个真正的改进是:首先,选择下一个空白单元的方法,第二,选择下一个可能数字的方法。这两个选择可以区分下一个死胡同搜索路径或沿着一个以解决方案结束的搜索路径。

接下来,我坐下来试图为上述两种选择提出不同的方法。这就是我想出来的。

可以通过以下方式选择下一个空白单元格:

  • A - 从左到右,从上到下的第一个单元格
  • B - 从右到左,从下到上的第一个单元格
  • C - 随机选择的单元格
  • D - 距离网格中心最近的单元格
  • E - 目前拥有最少选择的细胞(选择 这里表示从1到9的数字
  • F - 当前拥有最多选择的单元格
  • G - 具有最少空白相关细胞的细胞(相关细胞 是来自同一行,来自同一列或来自相同3x3的一个 象限)
  • H - 具有最多空白相关细胞的细胞
  • I - 最接近所有已填充细胞的细胞(从中测量) 细胞中心指向细胞中心点)
  • J - 离所有已填充细胞最远的细胞
  • K - 相关空白单元格最少的单元格 选择
  • L - 相关空白单元格最多的单元格 选择

可以通过以下方式选择下一个数字:

  • 0 - 最低位数
  • 1 - 最高位
  • 2 - 随机选择的数字
  • 3 - 启发式,全面使用最少的数字
  • 4 - 启发式地,全面使用最多的数字
  • 5 - 将导致相关空白单元格最少的数字 可供选择的数量
  • 6 - 将导致相关空白单元格最多的数字 可供选择的数量
  • 7 - 相关中最不常见的数字 空白细胞
  • 8 - 相关数字中最常用的数字 空白细胞
  • 9 - 数字是最不常见的数字 板
  • a - 数字是最常见的数字选择 板

所以我已经将上述方法编程到程序中。前面的数字和字母可以作为参数传递给程序,它将使用相应的优化方法。更重要的是,因为有时两个或更多的单元格可以具有相同的分数,所以可以选择提供第二个分类参数。例如参数&#34; EC&#34;意味着从所有可用选择最少的细胞中选择一个随机细胞。

第一个函数将分配权重乘以1000,第二个函数将添加新的权重乘以1.因此,如果例如从第一个函数中,三个单元格具有相同的权重,例如, 3000,3000 3000,然后第二个功能将添加自己的权重。例如3111,3256,3025。排序总是选择最低的重量。如果需要相反的情况,则使用-1000 amd -1调用权重函数,但排序仍然选择最低权重。

在继续之前,值得一提的是程序将始终选择一个空白单元格(而不是填充单元格)并始终选择一个位于单元格当前数独约束范围内的数字(否则这样做是不合理的)。

有了上述内容,我接着决定运行程序,每个可能的参数组合,看看会发生什么,哪些表现最好 - 基本上是强力暴力:)有12种方法选择细胞和11种方法对于数字选择所以理论上有17424种组合要尝试,但我删除了一些不必要的组合(例如&#34; AA&#34;,&#34; BB&#34;等等,并且也将随机方法排除在外它们都非常低效),所以最终的组合数量是12,100。每次运行都是在同一个Sudoku难题上完成的,这很简单:

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

...搜索空间为36,691,771,392。这只是给定拼图的每个空白单元格的选择数量的简单乘积。这是一种夸大其词,因为一旦一个细胞被填满,这就减少了其他细胞的选择数量,但这是我能想到的最快最简单的分数。

我编写了一个简短的脚本(当然是Python),它自动完成了整个测试过程 - 它为每组参数运行求解器,记录完成时间并将所有内容转储到文件中。另外,我决定每次运行20次,因为我从time.time()函数获得了0次单次运行。而且,如果任何组合花费超过10秒钟完成,脚本将停止并移动到下一个。

脚本在13:04:31小时在英特尔酷睿i7-4712MQ 2.30GHz的笔记本电脑上完成,8个核心中不超过2个,平均CPU负载约为12%。 12,100种组合中有8,652种在10秒内完成。

获奖者是:( *数字调整回单次运行时间/迭代次数)

1)最快时间为1.55毫秒: &#34; A0&#34;和&#34; A1&#34; 84次迭代和46次回溯迭代 和&#34; B0&#34;,&#34; B01&#34;,&#34; B1&#34;,&#34; B10&#34;,&#34; BA01&#34;,&#34; BA1&#34;,&#34; BD01&#34;,&#34; BD1&#34;和&#34; BD10&#34; 65次迭代和27次回溯迭代 最快的方法是最简单的方法,如A,B和D.另一种方法直到排名位置308才出现,那就是&#34; E0&#34;。

2)38和0回溯迭代的最少次迭代: 令人惊讶的是,许多方法设法实现了这一目标,最快的方法是&#34; B17&#34;,&#34; B6&#34;,&#34; B7&#34;,&#34; BA16&#34;,&# 34; BA60&#34;,&#34; BA7&#34;,&#34; BD17&#34;和&#34; BD70&#34;时间为2.3毫秒,最慢的是&#34; IK91&#34;,&#34; JK91&#34;,&#34; KI91&#34;,&#34; KJ91&#34;,&#34; KJ9a&#34;,&#34; IK9a&#34;,&#34; JK9a&#34;和&#34; KI9a&#34;时间约为107毫秒。 同样令人惊讶的是,方法F在这里有一些好的位置,例如&#34; FB6&#34; 7毫秒(???)

总体而言,A,B,D,E,G和K的表现似乎明显优于C,F,H和L,I和J之间有点差异。而且,数字的选择似乎并不重要。

最后,让我们看看这些获胜方法如何处理世界上最难的数独谜题,正如本文所述http://www.telegraph.co.uk/news/science/science-news/9359579/Worlds-hardest-sudoku-can-you-crack-it.html *考虑到算法并不是普遍快速的,也许某些算法在一些数独游戏上做得更好,但在其他算法上则没有... 难题是:

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

...且搜索空间为95,865,912,019,648,512 x 10 ^ 20。

获胜者是&#34; A0&#34;在1092毫秒内完成49,559次迭代和49,498次回溯迭代。大多数其他人并没有做得很好。 &#34; A0&#34;,&#34; A1&#34;,&#34; B0&#34;,&#34; B01&#34;,&#34; B1&#34;,&#34; B10& #34;,&#34; BA01&#34;,&#34; BA1&#34;,&#34; BD01&#39;,&#34; BD1&#34;和&#34; BD10&#34;完成大约2500毫秒和91k迭代,其余30多秒,400k +迭代。

但这还不够,所以我对最难的数独游戏的所有参数进行了全面测试。这次单次运行不是20次,也是2.5秒的截止时间。该剧本于8:23:30完成。在不到2.5秒的时间内完成了12,100种组合中的149种。 这两个类别的获奖者是&#34; E36&#34;,&#34; E37&#34;,&#34; EA36&#34;和&#34; EA37&#34;时间为109毫秒,362次迭代和301次回溯迭代。此外,前38个职位由一个开头E&#34;

主导

整体E在图表中排名第一,毫无疑问只需查看摘要电子表格即可。 A,B,I和J有一些排名,但没有什么,其余的甚至没有在2.5秒内完成一次。

总而言之,我认为可以肯定地说,如果Sudoku难题是一个简单的难题,那么用最简单的算法来强制它,但如果Sudoku难题是一个难题,那么它值得花费选择方法的开销。

希望这会有所帮助:)