Python vs C++: Алгоритм решения Судоку

Python vs C++

На самом деле, сначала я просто для эксперимента реализовал алгоритм решения Судоку на языке Python, чтобы оценить его лаконичность и эффективность. И, честно сказать, получилось не совсем то, на что я рассчитывал по обоим направлениям.

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

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

Алгоритм решения Судоку

Не будем слишком усложнять себе работу и пойдем наиболее простым путем. В качестве задачи ограничимся классическим Судоку 9×9. Правила головоломки довольно просты:

  1. Поле головоломки представляет собой квадрат, состоящий из 81 клетки;
  2. Часть клеток изначально заполнены цифрами от 1 до 9;
  3. В каждом ряду и столбце любая цифра от 1 до 9 может встречаться лишь один раз;
  4. Кроме того, все поле делится еще на 9 блоков 3×3, в которых цифры от 1 до 9 тоже могут встречаться лишь один раз;
  5. Задача заключается в том, чтобы заполнить все клетки цифрами с учетом указанных ограничений.

Алгоритм построим следующим образом:

Представленная логика достаточно проста и понятна. Сначала мы пытаемся заполнить клетки, для которых все однозначно. Для этого мы используем ограничения на уникальность по рядам, столбцам и блокам. Если Судоку некорректное, то на какой-то из итераций выявится клетка, для которой вообще нет допустимых вариантов. В этом случае дальше нам делать нечего.

Для наиболее простых Судоку на этом алгоритм можно было бы и закончить. Решение бы уже нашлось. Но для более сложных не так все легко. В них появятся клетки, для которых нет однозначных вариантов. Поэтому нам приходится работать с вероятностями. Конечно, есть достаточно сложные алгоритмы, которые бы существенно могли сократить перебор, но они также сложнее в реализации. Поэтому мы используем метод грубой силы.

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

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

Реализация алгоритма решения судоку на Python

И вот что получилось у меня на Python:

Думаю, что в представленной реализации вы не увидели ничего удивительного. Она полностью соответствует нашей первоначальной задумке. Единственное, на что следует обратить внимание, — алгоритм разбит на две функции. Главную solve() и вспомогательную solveHelper(). Однако первая из них сама практически ничего не делает, и вся реализация заключена во второй. Хотя пользоваться предлагается именно ей. Но это тоже довольно стандартный прием написания рекурсивных функций, который всем известен.

Для проверки условий на уникальность мы написали четыре вспомогательные функции findPossibleValues(), getRowValues(), getColumnValues() и getBlockValues(). Последние три просто возвращают множество цифр, которые уже стоят в ряду, столбце и блоке. А первая на основе полученных множеств возвращает цифры, которые остались и могут стоять в клетке на заданной позиции.

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

Реализация алгоритма решения судоку на C++

Практически то же, что мы получили на Python, можно написать и на C++:

Для краткости здесь использовано несколько конструкций, которые появились в C++11, но код увеличился бы не так уж сильно, если бы мы обошлись без них. Кроме того, мы задействовали классы коллекций из Qt, но они также не особо влияют на реализацию и лишь обеспечивают более естественную форму записи.

А теперь сравним две этих версии.

Python vs C++: сравнение реализаций

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

Лаконичность

Я бы не сказал. что среди двух представленных реализаций есть явный лидер. Версия на Python оказалась немного короче из-за того, что отсутствует необходимость ставить закрывающиеся фигурные скобки }, как на С++. Но я не могу отнести это к явным преимуществам. Однако в целом Python проявил себя более гибким языком программирования, каким он и является. Удобными мне представляются его итераторы и генераторы, а также мощная функциональность для работы со списками и кортежами. Конечно, С++11 приближается по выразительности к Python, но все же не может ее достигнуть из-за ограничений статической типизации.

Единственная особенность алгоритма, которую на C++ получить оказалось легче, — копирование поля Судоку перед входом в рекурсию. В Python нам приходится явно использовать deepCopy(), чтобы получить именно копию, а не просто еще одну ссылку на поле головоломки. А перед возвратом результата, который вернул рекурсивный вызов, на Python и вовсе приходится копировать значения в цикле. На C++ все это за нас делает QVector, что не только упрощает реализацию, но и может сократить расход памяти.

Таким образом, хоть реализации алгоритмов получились практически идентичными, я отдаю предпочтению Python за его синтаксис. Но и C++ оправдывает свое название и имеет пару своих плюсов.

Производительность

Будем честными. Никто не ждет, что версия на Python будет работать быстрее, чем на C++. Или же произойдет чудо? Какова разница? Я думаю, вы обратили внимание, что в обоих реализациях вставлен код для замера времени выполнения. Мой компьютер выдает следующие цифры: 2.77 sec на Python и 0.76 sec на C++. Итого, версия на C++ работает более, чем в три с половиной раза быстрее.

Не думаю, что это такой уж плохой результат для Python. Но стоит признать, что с учетом плюсов и минусов, вариант на C++ выглядит более предпочтительным для той версии алгоритма, которую мы рассмотрели. Вероятно, более сложный алгоритм, который существенно снижает число переборов, не будет так чувствителен к скорости работы. Но оказалась бы его реализация на Python проще, чем на C++? Скорее всего, да.

Заключение

В этой заметке мы рассмотрели две реализации одного и того же алгоритма для решения головоломки Судоку на Python и C++. Затем мы сравнили их по двум характеристикам: лаконичность и производительность. В результате оказалось, что хоть версия на Python немного проще, чем на C++, но существенно уступает ей в скорости работы. Однако следует учитывать, что у C++ была фора, поскольку алгоритм основывается на рекурсивном переборе, где у Python не было шансов. В более сложной версии алгоритма ситуация, скорее всего, сложится уже в пользу Python, так как в ней на первое место выйдет вопрос простоты кода, а не скорости перебора.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий