Возврат в решателе судоку не работает

Я пишу решатель судоку на Python, который принимает частично заполненную доску и использует обратное отслеживание и прямую проверку, чтобы заполнить остальное и решить головоломку. Прямая проверка - это когда каждый раз, когда вы присваиваете значение пустой ячейке, вы проверяете, есть ли у ее неназначенных «соседей» строки, столбца и поля непустые домены после назначения.

Чтобы представить мою доску (размеры: N x N доска с полями P x Q), я использую 2D-список, в котором каждая запись имеет форму [значение, [домен]], где значение - это присвоенный номер ячейки. (0, если не назначено) и домен - это возможные значения для ячейки, которые будут поддерживать согласованность головоломки судоку.

Я считаю, что делаю что-то не так с моей рекурсией, но не могу понять, что именно. Функция всегда терпит неудачу и возвращает False. Ниже приведена часть моей рекурсивной функции решателя с извлеченным кодом предварительной обработки. При необходимости я могу опубликовать всю функцию и ее вспомогательные функции.

def fc_recursive_solve(board, N, P, Q, row, col, outputFile, timeout):
    ###some preprocessing here to check if puzzle is solved and find next empty cell if not

    vals = board[row][col][1] #domain/possible values for the empty cell
    for num in vals:
        #if num doesn't violate the row, col, and box sudoku constraints
        if constraintCheck(row, col, num, P, N, Q, board):
            #make copy of cell's domain for backtracking
            tempDomain = copy.deepcopy(board[row][col][1])

            board[row][col][0] = num        #assign num to the cell

            #remove num from domains of neighbors,
            #if an empty domain results after removing num, 
            #return False and the original board,
            #else return True and the updated board
            noEmptyDomains, board = propagate_fc(board, N, P, Q, row, col, num)
            if noEmptyDomains:
                board[row][col][1] = [num]  #update domain to be num and then recurse
                if fc_recursive_solve(board, N, P, Q, row, col, outputFile, timeout):
                    return True
                #backtrack -- reset value and domain of assigned cell
                board[row][col][1] = tempDomain
                board[row][col][0] = 0
            else:
                board[row][col][0] = 0
    return False

РЕДАКТИРОВАТЬ: больше кода и опробовать решение Blckknght

def fc_recursive_solve(board, N, P, Q, row, col, outputFile, timeout):
    if time.clock() >= timeout:
        return "timeout"

    while row < N and board[row][col][0] != 0: #find next blank
        if col == N-1:
            row = row + 1
            col = 0
        else:
            col = col + 1

    if row == N: #solved
        return True

    for num in vals:
        if constraintCheck(row, col, num, P, N, Q, board):
            board[row][col][0] = num
            noEmptyDomains, new_board = propagate_fc(board, N, P, Q, row, col, num) # new var
            if noEmptyDomains:
                new_board[row][col][1] = [num]   # this doesn't modify board, only new_board
                if fc_recursive_solve(new_board, N, P, Q, row, col, outputFile, timeout):
                    return True
            board[row][col][0] = 0   # the only thing that's required to backtrack
    return False

def propagate_fc(board, N, P, Q, row, col, num):
    origBoard = copy.deepcopy(board)
    #row propagate
    for x in range(N):
        if board[x][col][0] == 0:
            if num in board[x][col][1]:
                board[x][col][1].remove(num)
        if len(board[x][col][1]) == 0:
            return False, origBoard #domain is empty; return original board

    #col propagate
    for y in range(N):
        if board[row][y][0] == 0:
            if num in board[row][y][1]:
                board[row][y][1].remove(num)
        if len(board[row][y][1]) == 0:
            return False, origBoard #domain is empty

    #box propagate
    rDiv = row/P
    cDiv = col/P
    for i in range((rDiv * P), ((rDiv + 1) * P)):
        for j in range((cDiv * Q), ((cDiv + 1) * Q)):
            if board[i][j][0] == 0:
                if num in board[i][j][1]:
                    board[i][j][1].remove(num)
            if len(board[i][j][1]) == 0:
                return False, origBoard #domain is empty

    return True, board #success; return board with updated domains

person Gerk    schedule 14.02.2016    source источник
comment
Не похоже, что ваш откат отменяет действия propagate_fc на доске где-либо.   -  person Blckknght    schedule 14.02.2016


Ответы (2)


Если вы выполняете откат, вам нужно иметь возможность вернуть состояние вашей доски к тому, как оно было раньше. Ваш текущий код этого не делает. Функция propagate_fc изменяет board таким образом, что отменить его нелегко.

Поскольку я не вижу простого способа изменить логику propagate_fc, я предлагаю изменить конструкцию решателя, чтобы он делал копии платы и только изменял копии, прежде чем передавать их на дальнейшие рекурсивные шаги. Если копия не приводит к решению, ее можно выбросить, вместо того, чтобы пытаться написать код обратного отслеживания, чтобы исправить ее.

(Я уверен, что можно отступить, просто не очевидно, какой хороший способ отслеживать изменения, и выяснить это - слишком много работы для этого ответа.)

В любом случае, вот что я предлагаю для модифицированной версии решателя:

def fc_recursive_solve(board, N, P, Q, row, col, outputFile, timeout):
    if time.clock() >= timeout:
        return "timeout"

    while row < N and board[row][col][0] != 0: #find next blank
        if col == N-1:
            row = row + 1
            col = 0
        else:
            col = col + 1

    if row == N: #solved
        return board

    for num in vals:
        if constraintCheck(row, col, num, P, N, Q, board):
            new_board = copy.deepcopy(board)
            new_board[row][col][0] = num
            if propagate_fc(new_board, N, P, Q, row, col, num):
                new_board[row][col][1] = [num] 
                result = fc_recursive_solve(new_board, N, P, Q, row, col, outputFile, timeout)
                if result is not None and result != "timeout":
                    return result
        return None

Я изменил его, чтобы вернуть решенную доску, если она удалась. В противном случае вы получите True ответ, но не сможете увидеть результат (поскольку board кода верхнего уровня не будет изменен).

Вот измененная версия propagate_fc:

def propagate_fc(board, N, P, Q, row, col, num):
    # no copying any more here

    #row propagate
    for x in range(N):
        if board[x][col][0] == 0:
            if num in board[x][col][1]:
                board[x][col][1].remove(num)
        if len(board[x][col][1]) == 0:
           return False

    #col propagate
    for y in range(N):
        if board[row][y][0] == 0:
            if num in board[row][y][1]:
                board[row][y][1].remove(num)
        if len(board[row][y][1]) == 0:
            return False

    #box propagate
    rDiv = row/P
    cDiv = col/P
    for i in range((rDiv * P), ((rDiv + 1) * P)):
        for j in range((cDiv * Q), ((cDiv + 1) * Q)):
            if board[i][j][0] == 0:
                if num in board[i][j][1]:
                    board[i][j][1].remove(num)
            if len(board[i][j][1]) == 0:
                return False

    return board #success; return new board

Единственное реальное изменение здесь состоит в том, что я не беспокоюсь о возврате board, поскольку мы всегда модифицируем его на месте. Требуется только результат типа bool (чтобы сказать, действительна плата или нет).

(Сначала я подумал, что есть еще одна проблема с if len(...) == 0 проверками в каждом блоке, но оказалось, что это не проблема. Вы можете получить немного лучшую производительность, выполняя эти проверки, только если вы просто removed значение из текущий список board[x][y][1] (с отступом этих блоков на два уровня), но это вряд ли даст большой прирост производительности.)

person Blckknght    schedule 14.02.2016
comment
Здравствуйте, спасибо за вашу помощь. Я понимаю проблему и попробовал предложенное вами изменение, но решатель все еще не находит решения. Я буду продолжать попытки отладить его. Я также обновил свой основной пост, добавив для вас полную функцию, а также командуropate_fc, как вы сказали, это будет полезно. - person Gerk; 14.02.2016
comment
Пример кода, который я предоставил для fc_recursive_solve, не будет работать, потому что propagate_fc действительно изменяет board, который он передается на месте. Попробуйте заменить строку deepcopy на повторную привязку board в функции, а не просто сохранять резервную копию. О, и я думаю, у вас также есть еще одна проблема, когда вы можете удалить одиночное значение [num] из board[col][row][1], а затем решить, что проблема невозможна, даже если это именно то место, где вы помещаете это значение. Я бы сделал отступ для проверки if len(board[x][row][1]) == 0 под блоком if board[x][row][0] == 0 (и, что эквивалентно, в блоке col). - person Blckknght; 14.02.2016
comment
Не могли бы вы немного пояснить, что вы имеете в виду, заменяя строку deepcopy для повторной привязки платы в функции? Я пробовал делать board = copy.deepcopy (board), но не уверен, что вы имели в виду. Также с некоторыми настройками и вашими предложениями я наконец смог заставить эту штуку работать с головоломками разных размеров. Как ни странно, решение больших головоломок с помощью этого решателя с прямой проверкой занимает приличное количество времени, чем мой простой слепой рекурсивный решатель, что наводит меня на мысль, что я делаю что-то очень неоптимально - все глубокие копии замедляют это, я полагаю? - person Gerk; 14.02.2016
comment
Ваша функция propagate_fc работает путем изменения board на месте. Это не работает с дизайном с возвратом, если вы не можете придумать способ отменить эти изменения позже, на шаге с возвратом. Мое исправление решателя предполагало, что propagate_fc вернул только измененную копию, но на самом деле он всегда изменял оригинал (и только иногда возвращал копию оригинала, когда обнаруживалось противоречие для текущего num размещения). Я довольно существенно переписал код в своем ответе, чтобы показать, как вы можете (надеюсь) заставить его работать без возврата. - person Blckknght; 14.02.2016

Основываясь на быстром взгляде, я думаю, вы перепутали распространение строки / столбца:

#row propagate
for x in range(row):                <== should this be range(col) ?
    if board[row][x][0] == 0:       <== row is fixed, but cols (x) changes
        if num in board[row][x][1]:
            board[row][x][1].remove(num)
    if len(board[row][x][1]) == 0:
        return False, origBoard #domain is empty; return original board
person RootTwo    schedule 14.02.2016
comment
Вы правы, спасибо. На самом деле он должен быть в диапазоне (N) для обоих. К сожалению, проблема с кодом все еще есть, но я обновлю это исправление в своем основном сообщении. - person Gerk; 14.02.2016