求解数独的回溯算法

Posted

技术标签:

【中文标题】求解数独的回溯算法【英文标题】:Backtracking algorithm for solving Sudoku 【发布时间】:2021-04-08 13:26:45 【问题描述】:

首先,感谢您抽出宝贵时间阅读本文。

我正在尝试创建一个回溯算法来解决特定的数独难题。但是,我遇到了一个我考虑了很长时间但无法掌握的问题。下面是我的回溯算法:

# The puzzle itself
board = [
    [0,0,6,8,4,0,0,0,0],
    [2,0,1,0,6,0,0,0,7],
    [0,3,9,0,0,0,0,1,0],
    [0,0,0,0,9,8,3,0,0],
    [0,6,0,0,0,0,0,9,0],
    [0,0,7,3,2,0,0,0,0],
    [0,4,0,0,0,0,1,3,0],
    [7,0,0,0,1,0,8,0,4],
    [0,0,0,0,3,5,7,0,0]
]

# A function which prints out the board in grids
def print_board():

    j = 1

    for rows in board:
        i = 1
        for elements in rows:
            if i % 3 == 0:
                print(elements, "|", end=" ")
            else:
                print(elements, end=" ")

            i += 1

        if j % 3 == 0:
            print("")
            print("-----------------------", end="")

        j += 1
        print("")

# A function which searches for the coordinate [x,y] of empty boxes (in this case if the value = 0 it is empty)
def search_empty():

    empty_list = []

    j = 0

    for rows in board:
        i = 0
        for elements in rows:
            if elements == 0:
                empty_list.append([i,j])

            i += 1

        j += 1

    return empty_list

# A function which takes the coordinate of an empty box and returns all the coordinates of boxes which fall in the same grid.
def set_grid(x, y):
    if x in range(3) and y in range(3):
        return [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]

    elif x in range(3, 6) and y in range(3):
        return [[3,0], [3,1], [3,2], [4,0], [4,1], [4,2], [5,0], [5,1], [5,2]]

    elif x in range(6, 9) and y in range(3):
        return [[6, 0], [6, 1], [6, 2], [7, 0], [7, 1], [7, 2], [8, 0], [8, 1], [8, 2]]

    elif x in range(3) and y in range(3, 6):
        return [[0,3], [0,4], [0,5], [1,3], [1,4], [1,5], [2,3], [2,4], [2,5]]

    elif x in range(3, 6) and y in range(3, 6):
        return [[3,3], [3,4], [3,5], [4,3], [4,4], [4,5], [5,3], [5,4], [5,5]]

    elif x in range(6, 9) and y in range(3, 6):
        return [[6, 3], [6, 4], [6, 5], [7, 3], [7, 4], [7, 5], [8, 3], [8, 4], [8, 5]]

    elif x in range(3) and y in range(6, 9):
        return [[0, 6], [0, 7], [0, 8], [1, 6], [1, 7], [1, 8], [2, 6], [2, 7], [2, 8]]

    elif x in range(3, 6) and y in range(6, 9):
        return [[3, 6], [3, 7], [3, 8], [4, 6], [4, 7], [4, 8], [5, 6], [5, 7], [5, 8]]

    elif x in range(6, 9) and y in range(6, 9):
        return [[6, 6], [6, 7], [6, 8], [7, 6], [7, 7], [7, 8], [8, 6], [8, 7], [8, 8]]

# A function which takes 3 inputs: lower, x and y.
# lower is the starting integer to test on, so first box defaults to 0. If say integer = 6 is the first answer to the first empty
# box, but 1 - 9 does not satisfy the rules of Sudoku for the second box, we backtrack to first box and start from integer + 1
# x and y are both coordinates of the empty box
def conditions(lower, x, y):

        grid_list = set_grid(x, y)
        for integers in range(lower, 10):
            print("Conditions running.")
            grid_test = True
            vertical_test = True
            horizontal_test = True
            
            # Grid test is to test if the current integer is present in the grid. If present, fails the grid test.
            for i in grid_list:
                [p, q] = i
                if integers == board[q][p]:
                    grid_test = False
                    break

            # Horizontal test is to test if the current integer is present in the same row. If present, fails the row test.
            for i in board[y]:
                if integers == i:
                    horizontal_test = False
                    break
            
            # Vertical test is to test if the current integer is present in the same column. If present, fails the vertical test.
            for i in range(9):
                if integers == board[i][x]:
                    vertical_test = False
                    break
            
            # If all three tests are satisfied and passed, the function returns True and the integer which satisfies all 3 rules.
            if grid_test and horizontal_test and vertical_test:
                print("Condition satisfied.")
                return True, integers

        print("Condition unsatisfied.")
        return False, 0


# This is where the backtracking begins.
def trials():

    n = 0

    a = 1

    # A list which records all the "correct at that time" integers from previous empty boxes. New "correct" integers are appended.

    history = []

    # Creates a static list of coordinates of empty boxes.
    empty_list = search_empty()

    while True:
        ## This line is to debug
        print(history)

        # p has value of True or False, and q has value of the correct integer if True, 0 if False
        [p, q] = conditions(a, empty_list[n][0], empty_list[n][1])

        if not p:
            # If p is false, we backtrack by shifting to the last empty box.
            n -= 1

            # a is the 'lower' input for conditions() function.
            a = history[n] + 1

            [x, y] = empty_list[n]

            # Since the 'correct' answer of previous box is not correct anymore, we are replacing it back with 0 (erasing it).
            board[y][x] = 0

            ## This line is to debug.
            print("a:", a, "old a:", history[n])

            # Removing the old 'correct' answer from the history list.
            history.remove(history[n])

        else:
            # If p is True, the 'correct' integer gets appended to the list.
            history.append(q)

            [x, y] = empty_list[n]

            # The correct answer is replacing the 0 (writing it on the empty box).
            board[y][x] = q

            # n increments by 1 to proceed to the next empty box.
            n += 1
        
        # When we run through the entire list of empty boxes, we break this loop.
        if n == len(empty_list) - 1:
            print("Done!")
            break


print_board()
trials()
print_board()

但是,在运行时,我收到以下信息:

0 0 6 | 8 4 0 | 0 0 0 | 
2 0 1 | 0 6 0 | 0 0 7 | 
0 3 9 | 0 0 0 | 0 1 0 | 
-----------------------
0 0 0 | 0 9 8 | 3 0 0 | 
0 6 0 | 0 0 0 | 0 9 0 | 
0 0 7 | 3 2 0 | 0 0 0 | 
-----------------------
0 4 0 | 0 0 0 | 1 3 0 | 
7 0 0 | 0 1 0 | 8 0 4 | 
0 0 0 | 0 3 5 | 7 0 0 | 
-----------------------
[]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5, 7]
Conditions running.
Condition satisfied.
[5, 7, 1]
Conditions running.
Conditions running.
Condition satisfied.
[5, 7, 1, 2]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed
a: 3 old a: 2
[5, 7, 1]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5, 7, 1, 9]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed
a: 10 old a: 9
[5, 7, 1]
Condition unsatisifed
a: 2 old a: 1
[5, 7]
Conditions running.
Condition satisfied.
[5, 7, 2]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5, 7, 2, 9]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed
a: 10 old a: 9
[5, 7, 2]
Condition unsatisifed
a: 3 old a: 2
[5, 7]
Conditions running.
Condition satisfied.
[5, 7, 3]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5, 7, 3, 9]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed
a: 10 old a: 9
[5, 7, 3]
Condition unsatisifed
a: 4 old a: 3
[5, 7]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition satisfied.
[5, 7, 9]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed
a: 10 old a: 9
[5, 7]
Condition unsatisifed
a: 8 old a: 7
[5]
Conditions running.
Conditions running.
Condition unsatisifed
a: 6 old a: 5
[]
Conditions running.
Conditions running.
Conditions running.
Conditions running.
Condition unsatisifed

我很难理解 [5, 7] 是如何在不执行任何条件检查的情况下跳到 [5] 的(正如 [5] 和 [5, 7] 之间没有“条件正在运行。”这一点很明显。这让我相信这是算法失败的主要原因。

非常感谢任何帮助。这是我正在从事的第一个实际项目,所以如果有任何初学者的错误,我会全神贯注:)。

【问题讨论】:

【参考方案1】:

顺便说一句,数独问题的纯回溯通常会导致大量的执行时间。

您的问题是您在找到好的解决方案时从未重置a。一个解决方案可能是在 while 循环的 else 末尾添加 a=1。此外,您使用history.remove(history[n]) 删除了history 中等于history[n] 的第一项,从而导致一些错误。您应该将其替换为 del。这是更正后的循环:

   while True:
        ## This line is to debug

        # p has value of True or False, and q has value of the correct integer if True, 0 if False
        [p, q] = conditions(a, empty_list[n][0], empty_list[n][1])

        if not p:
            # Removing the old 'correct' answer from the history list.
            del(history[n])
            # If p is false, we backtrack by shifting to the last empty box.
            n -= 1

            # a is the 'lower' input for conditions() function.
            a = history[n] + 1
            history[n]+=1

            board[y][x] = 0
            [x, y] = empty_list[n]

            # Since the 'correct' answer of previous box is not correct anymore, we are replacing it back with 0 (erasing it).

            ## This line is to debug.


        else:
            # If p is True, the 'correct' integer gets appended to the list.
            history[n]=q
            history.append(0)
            [x, y] = empty_list[n]

            # The correct answer is replacing the 0 (writing it on the empty box).
            board[y][x] = q
            n += 1

            # n increments by 1 to proceed to the next empty box.
            a=1 
        # When we run through the entire list of empty boxes, we break this loop.
        if n == len(empty_list):
            print("Done!")
            break

这导致输出:

0 0 6 | 8 4 0 | 0 0 0 | 
2 0 1 | 0 6 0 | 0 0 7 | 
0 3 9 | 0 0 0 | 0 1 0 | 
-----------------------
0 0 0 | 0 9 8 | 3 0 0 | 
0 6 0 | 0 0 0 | 0 9 0 | 
0 0 7 | 3 2 0 | 0 0 0 | 
-----------------------
0 4 0 | 0 0 0 | 1 3 0 | 
7 0 0 | 0 1 0 | 8 0 4 | 
0 0 0 | 0 3 5 | 7 0 0 | 
-----------------------
Done!
5 7 6 | 8 4 1 | 9 2 3 | 
2 8 1 | 9 6 3 | 5 4 7 | 
4 3 9 | 2 5 7 | 6 1 8 | 
-----------------------
1 2 4 | 5 9 8 | 3 7 6 | 
3 6 8 | 1 7 4 | 2 9 5 | 
9 5 7 | 3 2 6 | 4 8 1 | 
-----------------------
6 4 5 | 7 8 9 | 1 3 2 | 
7 9 3 | 6 1 2 | 8 5 4 | 
8 1 2 | 4 3 5 | 7 6 9 | 
-----------------------

哪个是正确的答案。

【讨论】:

啊,我忽略了重置“a”。 另外,您使用 history.remove(history[n]) 删除历史的第一项等于 history[n] 我不太明白这个说法,你能澄清一下它是怎么回事与使用 del(history[n]) 不同? @NeoZenith sel over remove 对于使其正常工作同样重要 @NeoZenith removelist 类的一个方法,将一个值作为参数,并删除该值在列表中的第一次出现。例如,如果您有l=[1,2,3,4,5,1],则l.remove(l[5]) 会将l 转换为[2,3,4,5,1]。相反,del(l[5]) 会将l 转换为[1,2,3,4,5] @NeoZenith 如果代码有效,请验证答案 啊,我现在明白了。非常感谢您的洞察力!

以上是关于求解数独的回溯算法的主要内容,如果未能解决你的问题,请参考以下文章

python中数独的回溯算法

优化回溯算法求解数独

数独求解器回溯算法不起作用

为啥我使用回溯解决数独的 JAVA 代码没有给出任何解决方案? [关闭]

Gui 可视化递归回溯数独

求解数独的所有解法,java编程实现