数独求解器由于某种原因不断卡住

时间:2010-09-17 06:44:30

标签: c++ algorithm logic sudoku

所以我不得不为高中的计算机项目编写一个程序,我想做了一个sudoko求解器。 'solve'算法的实现如下: -

  1. 对于任何只有一个元素“适合”查看行,列,3x3集的任何点,请将该数字放入。重复执行此操作直至无法再进行此操作。这可以在'singleLeft'函数中看到。
  2. 如果一个数字“适合”某个点,但在相关的行,列或3x3集合中没有其他地方,请将该数字放入。这可以在'checkOnlyAllowed'函数中看到。
  3. 如果我们尚未完成,请做一个'猜测' - 取一些“适合”的数字,把它放在那里然后再用这个算法解决(递归) - 如果它有效,我们就是完成。
  4. 到目前为止,我有这段代码:

    #include <iostream>
    #include <fstream>
    #include <cstdlib>
    
    using namespace std;
    
    //Prints a message and exits the application.
    void error(const char msg[])
    {
        cout << "An error occurred!" << endl;
        cout << "Description: " << msg << endl;
    
        exit(0);
    }
    
    //A representation of a sudoku board. Can be read from a file or from memory.
    class Sudoku
    {
        protected:
            //For a point x, y and a number n in the board, mAllowed[x][y][n]
            //is 1 if n is allowed in that point, 0 if not.
            int mAllowed[9][9][10];
            int filledIn;
    
        public:
            /*
             * For mBoard[i][j], the location is (i,j) in the below map:
             *
             * (0,0) (0,1) (0,2)  (0,3) (0,4) (0,5)  (0,6) (0,7) (0,8) 
             * (1,0) (1,1) (1,2)  (1,3) (1,4) (1,5)  (1,6) (1,7) (1,8) 
             * (2,0) (2,1) (2,2)  (2,3) (2,4) (2,5)  (2,6) (2,7) (2,8) 
             *   
             * (3,0) (3,1) (3,2)  (3,3) (3,4) (3,5)  (3,6) (3,7) (3,8) 
             * (4,0) (4,1) (4,2)  (4,3) (4,4) (4,5)  (4,6) (4,7) (4,8) 
             * (5,0) (5,1) (5,2)  (5,3) (5,4) (5,5)  (5,6) (5,7) (5,8) 
             *    
             * (6,0) (6,1) (6,2)  (6,3) (6,4) (6,5)  (6,6) (6,7) (6,8) 
             * (7,0) (7,1) (7,2)  (7,3) (7,4) (7,5)  (7,6) (7,7) (7,8) 
             * (8,0) (8,1) (8,2)  (8,3) (8,4) (8,5)  (8,6) (8,7) (8,8) 
             *
             */
    
            int mBoard[9][9];
    
            //Read in from file with given name.
            Sudoku(char filename[])
            {
                filledIn = 0;
    
                int i, j, k;
    
                //Fill the board with 0s.
                for (i = 0; i < 9; ++i)
                    for (j = 0; j < 9; ++j)
                        mBoard[i][j] = 0;
    
                //Set every number to 'allowed' initially.
                for (i = 0; i < 9; ++i)
                    for (j = 0; j < 9; ++j)
                        for (k = 1; k <= 9; ++k)
                            mAllowed[i][j][k] = 1;
    
                //Read in from the file.
                ifstream file(filename);
                if (!file)
                    error("File doesn't exist!");
                for (i = 0; i < 9; ++i)
                    for (j = 0; j < 9; ++j)
                        if (file)
                        {
                            int m;
                            file >> m;
                            if (m)
                                set(i, j, m);
                        }
                        else
                            error("Not enough entries in file!");
            }
    
            //Solve the board!
            int solve()
            {
                int prevFilledIn;
    
                do
                {
                    prevFilledIn = filledIn;
                    singleLeft();
                    checkOnlyAllowed();
                } while (filledIn - prevFilledIn > 3);
    
                if (filledIn < 81)
                    guess();
                return filledIn == 81;
            }
    
            //Given a point i, j, this looks for places where this point
            //disallows a number and sets the 'mAllowed' table accordingly.
            void fixAllowed(int i, int j)
            {
                int n = mBoard[i][j], k;
    
                for (k = 0; k < 9; ++k)
                    mAllowed[i][k][n] = 0;
                for (k = 0; k < 9; ++k)
                    mAllowed[k][j][n] = 0;
    
                //Look in 3x3 sets too. First, set each coordinate to the
                //highest multiple of 3 below itself. This takes us to the
                //top-left corner of the 3x3 set this point was in. Then,
                //add vectorially all points (x,y) where x and y each are
                //one of 0, 1 or 2 to visit each point in this set.
                int x = (i / 3) * 3;
                int y = (j / 3) * 3;
    
                for (k = 0; k < 3; ++k)
                    for (int l = 0; l < 3; ++l)
                        mAllowed[x + k][y + l][n] = 0;
    
                mAllowed[i][j][n] = 1;
            }
    
            //Sets a point i, j to n.
            void set(int i, int j, int n)
            {
                mBoard[i][j] = n;
                fixAllowed(i, j);
                ++filledIn;
            }
    
            //Try using 'single' on a point, ie, only one number can fit in this
            //point, so put it in and return 1. If more than one number can fit,
            //return 0.
            int trySinglePoint(int i, int j)
            {
                int c = 0, m;
                for (m = 1; m <= 9; ++m)
                    c += mAllowed[i][j][m];
    
                if (c == 1)
                {
                    for (m = 1; m <= 9; ++m)
                        if (mAllowed[i][j][m])
                            set(i, j, m);
                    //printBoard();
                    return 1;
                }
                return 0;
            }
    
            //Try to solve by checking for spots that have only one number remaining.
            void singleLeft()
            {
                for (;;)
                {
                    for (int i = 0; i < 9; ++i)
                        for (int j = 0; j < 9; ++j)
                            if (!mBoard[i][j])
                                if (trySinglePoint(i, j))
                                    goto logic_worked;
    
                    //If we reached here, board is either full or unsolvable by this logic, so
                    //our job is done.
                    return;
    logic_worked:
                    continue;
                }
            }
    
            //Within rows, columns or sets, whether this number is 'allowed' in spots
            //other than i, j.
            int onlyInRow(int n, int i, int j)
            {
                for (int k = 0; k < 9; ++k)
                    if (k != j && mAllowed[i][k][n])
                        return 0;
                return 1;
            }
            int onlyInColumn(int n, int i, int j)
            {
                for (int k = 0; k < 9; ++k)
                    if (k != i && mAllowed[k][j][n])
                        return 0;
                return 1;
            }
            int onlyInSet(int n, int i, int j)
            {
                int x = (i / 3) * 3;
                int y = (j / 3) * 3;
    
                for (int k = 0; k < 3; ++k)
                    for (int l = 0; l < 3; ++l)
                        if (!(x + k == i && y + l == j) && mAllowed[x + k][y + l][n])
                            return 0;
                return 1;
            }
            //If a number is 'allowed' in only one spot within a row, column or set, it's
            //guaranteed to have to be there.
            void checkOnlyAllowed()
            {
                for (int i = 0; i < 9; ++i)
                    for (int j = 0; j < 9; ++j)
                        if (!mBoard[i][j])
                            for (int m = 1; m <= 9; ++m)
                                if (mAllowed[i][j][m])
                                    if (onlyInRow(m, i, j) || onlyInColumn(m, i, j) || onlyInSet(m, i, j))
                                        set(i, j, m);
            }
    
            //Copy from a given board.
            void copyBoard(int board[9][9])
            {
                filledIn = 0;
                for (int i = 0; i < 9; ++i)
                    for (int j = 0; j < 9; ++j)
                    {
                        if (board[i][j] > 0)
                            ++filledIn;
                        mBoard[i][j] = board[i][j];
                    }
            }
    
            //Try to solve by 'guessing'.
            void guess()
            {
                for (int i = 0; i < 9; ++i)
                    for (int j = 0; j < 9; ++j)
                        for (int n = 1; n <= 9; ++n)
                            if (!mBoard[i][j])
                                if (mAllowed[i][j][n] == 1)
                                {
                                    //Do a direct copy so that it gets the 'mAllowed'
                                    //table too.
                                    Sudoku s = *this;
    
                                    //Try solving with this number at this spot.
                                    s.set(i, j, n);
                                    if (s.solve())
                                    {
                                        //It was able to do it! Copy and report success!
                                        copyBoard(s.mBoard);
                                        return;
                                    }
                                }
            }
    
            //Print the board (for debug purposes)
            void printBoard()
            {
                for (int i = 0; i < 9; ++i)
                {
                    for (int j = 0; j < 9; ++j)
                        cout << mBoard[i][j] << " ";
                    cout << endl;
                }
                cout << endl;
                char s[5];
                cin >> s;
            }
    
    };
    
    int main(int argc, char **argv)
    {
        //char filename[42];
        //cout << "Enter filename: ";
        //cin >> filename;
    
        char *filename = argv[1];
    
        Sudoku s(filename);
        if (!s.solve())
            error("Couldn't solve!");
    
        cout << "Solved! Here's the solution:" << endl << endl;
    
        for (int i = 0; i < 9; ++i)
        {
            for (int j = 0; j < 9; ++j)
                cout << s.mBoard[i][j] << " ";
            cout << endl;
        }
    
        return 0;
    }
    

    (代码包括行号:http://sprunge.us/AiUc?cpp

    现在我明白它不是很好的风格,但是它来自深夜编码会话,我们在学校实验室使用较旧的编译器,所以我不得不做一些不同的事情(在那个编译器中,标准头文件的扩展名为“.h”,for循环中声明的变量位于outside-for范围内,...)。

    该文件应包含棋盘格中每个点的空白分隔数字,从左上角开始,从左到右,从上到下,空白点由'0'表示。

    对于以下文件,它的效果相当不错:

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

    然而,这个给它带来了麻烦:

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

    如果我注释掉打印语句并跟踪进度,我可以看到它是从点向错误的方向开始的。最终它会卡在最后,而回溯永远不会回到足够远的地方。我认为'checkOnlyAllowed'部分有问题......

    您认为这可能是什么问题?

    另外 - 我知道我可以在'mAllowed'表中使用一个位域,但我们还没有在学校正式了解按位操作。 :P

3 个答案:

答案 0 :(得分:0)

在第170行,你有一个跳出for循环的goto,然后继续。这可能会给你一些奇怪的行为,继续错误的循环,这可能取决于特定的编译器。

尝试用以下代码替换第164-177行:

164             for (;;)
165             {
166                 bool successfullyContributedToTheBoard = false;
167                 for (int i = 0; i < 9; ++i)
168                     for (int j = 0; j < 9; ++j)
169                         if (!mBoard[i][j])
170                             if (trySinglePoint(i, j))
171                                 successfullyContributedToTheBoard = true;
172                 if (!successfullyContributedToTheBoard)
173                     return;
174             }

答案 1 :(得分:0)

我没有查看您的代码,但您的策略与我用于编码Sudoku解算器的策略完全相同。但我不记得它很慢。我马上得到了解决方案。在我的测试中,程序所做的“猜测”的最大数量为3。这是为了数独问题而应该是非常困难的。对于后向跟踪而言,三个并不是一个很大的数字,你可以选择一个只剩下几个可能性的单元格(两个或三个),它将搜索空间限制在仅约20-30个状态(对于硬数独问题)。

我所说的是,可以使用这种策略并快速解决数独问题。您只需要弄清楚如何优化代码。尽量避免多余的工作。尽量记住事情,这样你就不需要一次又一次地重新计算它们。

答案 2 :(得分:0)

好吧,我搞定了!似乎'猜测'中的i,j循环是不必要的 - 即,它应该只对一个空位进行猜测,因为它的'子进程'将处理其余的。修复这个实际上使代码更简单。现在它的效果非常好,实际上非常快!

感谢大家的帮助。 ; - )