Оптимизация алгоритма обратного отслеживания решения судоку - PullRequest
13 голосов
/ 05 октября 2009

Я надеюсь оптимизировать алгоритм возврата для моего Судоку Солвер.


Что он делает сейчас:

Функция рекурсивного решателя берет загадку судоку с различными заданными значениями.

Я прочесываю все пустые слоты в головоломке, ищу слот с наименьшими возможностями и получаю список значений.

Из списка значений я переберу его, поместив одно из значений из списка в слот, и рекурсивно решу его, пока не заполнится вся сетка.


Эта реализация все еще занимает невероятно много времени для некоторых головоломок, и я надеюсь еще больше оптимизировать это. У кого-нибудь есть идеи, как я мог бы еще больше оптимизировать это?


Вот мой код на 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 ]

6 голосов
/ 10 августа 2010

У меня было задание сделать это: создать самый быстрый решатель судоку на Java. Я выиграл конкурс со временем 0,3 миллисекунды.

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

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

Я написал об этом в блоге и разместил код здесь: http://www.byteauthor.com/2010/08/sudoku-solver/

4 голосов
/ 02 февраля 2010

долгое время я писал решатель судоку (несколько лет назад, но я держу весь код, который я пишу). Он не был обобщен для определения «большего» размера, чем обычный судоку, но он довольно быстрый.

Это решает следующее за 103 мс (на Core 2 Duo 1,86 ГГц) и действительно не было оптимизировано:

        {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 голосов
/ 07 октября 2009

Некоторое время назад я реализовал «Танцующие связи» Дональда Кнута и его Алгоритм X для судоку в Ruby (язык, который, как известно, не слишком эффективен). Для нескольких примеров, которые я проверил, на моем ноутбуке с частотой 1,5 ГГц потребовалось несколько миллисекунд.

Вы можете посмотреть в википедии, как работают Танцующие ссылки, и самостоятельно адаптировать его к Судоку. Или взгляните на «Решатель судоку в Java, реализующий алгоритм танцевальных связей Кнута» .

PS: Алгоритм X - это алгоритм возврата.

2 голосов
/ 05 октября 2009

Выполнить некоторое распространение ограничений перед каждым недетерминированным шагом.

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

Большинство головоломок Судоку для людей разработаны так, что они вообще не нуждаются в возврате.

1 голос
/ 21 января 2010

Поиск слота с наименьшими возможными решениями невероятно дорог, и для традиционной головоломки Судоку, вероятно, не стоит накладных расходов.

Более простая оптимизация состоит в том, чтобы отслеживать, сколько каждой цифры было использовано, и когда вы «пытаетесь» разместить цифру в слоте, начните с той, которая использовалась меньше всего (правка включите те, с которыми была затравлена ​​загадка). Это повысит вероятность того, что ваш алгоритм начнет по успешному пути, а не по неудачному.

Кроме того, проверьте Искусственный интеллект: современный подход , как предложено Имсасу. Это фантастическая книга, подробно рассказывающая о рекурсивном возврате.

P.S. Мне любопытно, что прирост производительности (если таковой имеется) дает ваша оптимизация "первого шага". У тебя есть фигура?

1 голос
/ 07 октября 2009

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

Также большая потеря скорости должна быть в создании нового массива для каждого рекурсивного вызова. Вместо этого сделайте изменение в массиве перед рекурсивным вызовом, а затем отмените его после рекурсивного вызова. В основном добавьте инвариант, что Solve будет менять слоты во время работы, но когда он вернется, он оставит все как было при вызове функции.

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

Помогает ли размещение числа в квадрате с наименьшим количеством вариантов? Без этого код будет намного проще (вам не нужно сохранять вещи в связанных списках и т. Д.)

Вот мой код псевдо:

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;
    }       
}
0 голосов
/ 13 января 2018

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

Здесь, однако, я бы хотел больше сосредоточиться на процессе оптимизации. Чтобы быть более точным, я исследовал различные подходы, чтобы минимизировать время решения и количество итераций. И это больше о алгоритмических улучшениях, которые могут быть сделаны, а не о программировании.

Итак, подумав об этом, в алгоритме обратного отслеживания грубой силы не так много вещей, которые можно было бы оптимизировать (рад, что здесь вы ошиблись). Два реальных улучшения, которые могут быть сделаны: во-первых, метод, с помощью которого выбирается следующая пустая ячейка, и, во-вторых, метод, с помощью которого выбирается следующая возможная цифра. Эти два варианта могут иметь значение для перехода по тупиковому пути поиска или к поисковому пути, который заканчивается решением.

Затем я сел и попытался придумать разные методы для вышеупомянутых двух вариантов. Вот что я придумал.

Следующая пустая ячейка может быть выбрана следующими способами:

  • 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 - цифра, которая является наиболее распространенным из доступных вариантов доска

Итак, я запрограммировал вышеуказанные методы в программу. Предыдущие цифры и буквы могут быть переданы в качестве параметров в программу, и она будет использовать соответствующий метод оптимизации. Более того, поскольку иногда две и более ячейки могут иметь одинаковую оценку, существует возможность указать второй параметр сортировки. Например, параметр «EC» будет означать выбор случайной ячейки из всех ячеек с наименьшим количеством доступных вариантов.

Первая функция назначит веса, умноженные на 1000, а вторая функция добавит новые веса, умноженные на 1. Таким образом, если, например, из первой функции три ячейки имеют одинаковый вес, например, 3000, 3000 3000, тогда вторая функция добавит свои веса. например 3111, 3256, 3025. При сортировке всегда выбирают наименьший вес. А если нужно обратное, то весовые функции вызываются с -1000 драм -1, но сортировка по-прежнему выбирает наименьший вес.

Прежде чем продолжить, стоит отметить, что программа всегда выбирает пустую ячейку (не заполненную) и всегда выбирает цифру, которая соответствует текущим ограничениям Судоку для ячейки (в противном случае это просто неразумно).

Имея вышеизложенное, я тогда решил запустить программу с каждой возможной комбинацией параметров и посмотреть, что произойдет, какие из них работают лучше всего - в основном для грубой силы грубой силы :) Есть 12 методов для выбора ячейки и 11 методов для выбора цифр в теории существует 17244 комбинаций, которые нужно попробовать, но я удалил некоторые ненужные (такие как «AA», «BB» и т. д., а также исключил случайные методы, так как все они ужасно неэффективны), поэтому число комбинаций в итоге было 12,100. Каждый прогон был сделан на одной и той же головоломке Судоку, которая очень проста:

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 прогонов каждого, потому что я получал 0 раз из функции time.time () для одиночных прогонов. А также, если для выполнения какой-либо комбинации потребуется более 10 секунд, сценарий остановится и перейдет к следующей.

Сценарий, завершенный в 13:04:31 часов на ноутбуке с Intel Core i7-4712MQ 2,30 ГГц, использовалось не более 2 из 8 ядер, а средняя загрузка ЦП составляла около 12%. 8 652 из 12 100 комбинаций, выполненных менее чем за 10 секунд.

И победителями являются: (* числа, скорректированные для единичного времени выполнения / итераций)

1) Самое быстрое время 1,55 мс: «A0» и «A1» с 84 итерациями и 46 итерациями возврата и "B0", "B01", "B1", "B10", "BA01", "BA1", "BD01", "BD1" и "BD10" с 65 итерациями и 27 итерациями возврата. Самые быстрые методы - самые простые, такие как A, B и D. Другой метод не появляется до ранжирования 308, а это «E0».

2) Наименьшее количество итераций из 38 и 0 итераций возврата: Удивительно, что многим методам удалось добиться этого, самыми быстрыми из которых были "B17", "B6", "B7", "BA16", "BA60", "BA7", "BD17" и "BD70" со временем 2,3 мс и самые медленные из них: «IK91», «JK91», «KI91», «KJ91», «KJ9a», «IK9a», «JK9a» и «KI9a» со временем около 107 мс. Также удивительно, что метод F имеет несколько хороших позиций, таких как «FB6» с 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.

Победителем становится «A0», заканчивающийся за 1092 мс с 49 559 итерациями и 49 498 итерациями возврата. Большинство других не очень хорошо. «A0», «A1», «B0», «B01», «B1», «B10», «BA01», «BA1», «BD01», «BD1» и «BD10» закончили примерно за 2500 мс и 91k итерации, а остальные 30+ секунд, 400 тыс. + итераций.

Но этого недостаточно, поэтому я выполнил полный тест всех наборов параметров и для самого сложного судоку. На этот раз делать один раз не 20, а также время отключения 2,5 секунды. Сценарий завершен в 8:23:30 часов. 149 из 12 100 комбинаций, выполненных менее чем за 2,5 секунды. Победителями в обеих категориях являются «E36», «E37», «EA36» и «EA37» со временем 109 мс, 362 итерациями и 301 итерациями возврата. Кроме того, на первых 38 позициях доминировала начальная буква «Е».

В целом E возглавляет чарты, без сомнения, просто взглянув на сводную таблицу. A, B, I и J имеют несколько рейтингов, но ничего особенного, а остальные даже не успели раз в 2,5 секунды.

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

Надеюсь, это поможет:)

0 голосов
/ 02 июля 2014

Результаты моих оптимизаций алгоритма возврата для судоку приведены ниже. Вы можете скачать код с http://yikes.com/~bear/suds.c. Он основан исключительно на принципе "дырки в голубях", и я обнаружил, что он обычно быстрее, чем решение на основе правил.

Используя значения из другого поста в этой теме, я получаю результат 7 мс на core2 duo @ 2,2 ГГц или 3 мс на ядре i5. Это сопоставимо с результатом плаката в 100 мс, хотя это могло быть измерено другим способом. Время добавлено в 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 
0 голосов
/ 05 октября 2009

Вам, вероятно, следует использовать профилировщик, чтобы увидеть, какое утверждение занимает больше всего времени, а затем подумать о том, как его оптимизировать.

Без использования профилировщика я предлагаю вам каждый раз создавать новый PuzzleGenerator с нуля и передавать слоты в качестве аргумента методу возможногоValuesInGrid. Я думаю, это означает, что PuzzleGenerator каждый раз пересчитывает все с нуля, для каждой позиции и для каждой конфигурации слотов; тогда как вместо этого он мог бы быть [намного] более эффективным, если бы он запомнил предыдущие результаты и изменялся постепенно.

...