Головоломка Судоку
Судоку — это популярная головоломка на поле 9×9, где нужно заполнить клетки цифрами от 1 до 9. Условия:
- каждая строка содержит все числа от 1 до 9 без повторов;
- каждая колонка также содержит все числа от 1 до 9 без повторов;
- каждая область 3×3 содержит все числа от 1 до 9 без повторов.
Часть клеток заполнена заранее и служит подсказками. Необходимо заполнить все пустые клетки так, чтобы правила соблюдались.
Решение задачи
1. Выбор модели
Для решения задачи судоку идеально подходит модель ограничений (Constraint Satisfaction Problem, CSP). В рамках этой модели мы:
- Определяем переменные (ячейки судоку, которые нужно заполнить).
- Определяем домены (множество допустимых значений для каждой переменной, в нашем случае это цифры от 1 до 9).
- Определяем ограничения (правила судоку, которые связывают эти переменные: все значения в строке, столбце и блоке 3x3 должны быть уникальными).
Модель ограничений из библиотеки O2 как раз предназначена для описания и решения таких задач. Ее механизм не перебирает варианты "в лоб", а использует умные алгоритмы для отсечения заведомо неверных путей поиска, что делает решение очень эффективным.
2. Создание модели
Создадим экземпляр модели, в котором будут регистрироваться переменные и ограничения. Это «контейнер», через который мы описываем задачу и затем вызываем метод решения.
// Создаем объект модели
Модель = О2
.Модели()
.МодельОграничений()
.СоздатьМодель();
3. Ввод данных
Определяем размерность судоку (9×9) и размер блока (3×3). В массив Подсказки заносим исходные данные, где 0 означает пустую клетку.
// Вводим начальные данные
РазмерПоля = 9;
РазмерБлока = 3;
// Вводим значения подсказок (0 означает пустую ячейку)
Подсказки = Новый Массив(РазмерПоля);
Подсказки[0] = О2.Утилиты().МассивЧиселИзСтроки("5,3,0, 0,7,0, 0,0,0");
Подсказки[1] = О2.Утилиты().МассивЧиселИзСтроки("6,0,0, 1,9,5, 0,0,0");
Подсказки[2] = О2.Утилиты().МассивЧиселИзСтроки("0,9,8, 0,0,0, 0,6,0");
Подсказки[3] = О2.Утилиты().МассивЧиселИзСтроки("8,0,0, 0,6,0, 0,0,3");
Подсказки[4] = О2.Утилиты().МассивЧиселИзСтроки("4,0,0, 8,0,3, 0,0,1");
Подсказки[5] = О2.Утилиты().МассивЧиселИзСтроки("7,0,0, 0,2,0, 0,0,6");
Подсказки[6] = О2.Утилиты().МассивЧиселИзСтроки("0,6,0, 0,0,0, 2,8,0");
Подсказки[7] = О2.Утилиты().МассивЧиселИзСтроки("0,0,0, 4,1,9, 0,0,5");
Подсказки[8] = О2.Утилиты().МассивЧиселИзСтроки("0,0,0, 0,8,0, 0,7,9");
4. Регистрация переменных
Создадим переменные модели. Ключевой момент: для ячеек с подсказкой мы создаем константу, а для пустых ячеек — переменную, которая может принимать значения от 1 до 9.
// Создаем двумерный массив для хранения переменных модели, соответствующих ячейкам судоку
ЗначенияЯчеек = Новый Массив(РазмерПоля, РазмерПоля);
// Обходим все ячейки поля
Для Строка = 0 По РазмерПоля - 1 Цикл
Для Колонка = 0 По РазмерПоля - 1 Цикл
// Смотрим, есть ли в ячейке подсказка
ЗначениеПодсказки = Подсказки[Строка][Колонка];
Если ЗначениеПодсказки = 0 Тогда
// Если подсказки нет, создаем переменную с диапазоном допустимых значений 1-9
ЗначениеЯчейки = Модель.ПеременнаяДиапазона(1, РазмерПоля);
Иначе
// Если подсказка есть, создаем константу (фиксированное значение)
ЗначениеЯчейки = Модель.Константа(ЗначениеПодсказки);
КонецЕсли;
// Сохраняем созданную переменную или константу в массив
ЗначенияЯчеек[Строка][Колонка] = ЗначениеЯчейки;
КонецЦикла;
КонецЦикла;
5. Описание ограничений
Это самый важный этап, на котором мы формализуем правила судоку на языке модели.
Установим ограничение на уникальность в строках:
// Для каждой строки добавляем ограничение: все элементы должны быть разными
Для Строка = 0 По РазмерПоля - 1 Цикл
Модель.Ограничения().ВсеРазличные(ЗначенияЯчеек[Строка]);
КонецЦикла;
Установим ограничение на уникальность в столбцах. Поскольку наш массив ЗначенияЯчеек организован по строкам, для работы со столбцами мы сначала собирем все переменные одного столбца во временный массив.
// Для каждого столбца...
Для Колонка = 0 По РазмерПоля - 1 Цикл
// ...создаем массив, содержащий все переменные этого столбца
КолонкаПеременных = Новый Массив(РазмерПоля);
Для Строка = 0 По РазмерПоля - 1 Цикл
КолонкаПеременных[Строка] = ЗначенияЯчеек[Строка][Колонка];
КонецЦикла;
// ...и добавляем для этого массива ограничение на уникальность
Модель.Ограничения().ВсеРазличные(КолонкаПеременных);
КонецЦикла;
Установим ограничение на уникальность в блоках 3x3. Алгоритм чуть сложнее. Мы проходим по всем девяти блокам, для каждого блока собираем все 9 переменных в массив и накладываем ограничение.
// Создаем массив для сбора переменных блока
БлокПеременных = Новый Массив(РазмерБлока * РазмерБлока);
// Цикл по вертикальным блокам (0,1,2)
Для БлокСтрока = 0 По РазмерБлока - 1 Цикл
// Цикл по горизонтальным блокам (0,1,2)
Для БлокКолонка = 0 По РазмерБлока - 1 Цикл
Индекс = 0; // Индекс для заполнения массива БлокПеременных
// Внутренние циклы по ячейкам внутри одного блока 3x3
Для Строка = БлокСтрока * РазмерБлока По (БлокСтрока + 1) * РазмерБлока - 1 Цикл
Для Колонка = БлокКолонка * РазмерБлока По (БлокКолонка + 1) * РазмерБлока - 1 Цикл
// Собираем переменные ячеек блока в массив
БлокПеременных[Индекс] = ЗначенияЯчеек[Строка][Колонка];
Индекс = Индекс + 1;
КонецЦикла;
КонецЦикла;
// Добавляем ограничение на уникальность для текущего блока
Модель.Ограничения().ВсеРазличные(БлокПеременных);
КонецЦикла;
КонецЦикла;
6. Решение модели
После того как все переменные и ограничения объявлены, мы запускаем "решатель".
// Выполняем поиск решения
Решение = Модель.Решить();
В данный задаче отсутсвует целевая функция — мы ищем любое допустимое решение. Модель ограничений без целевой функции — это задача выполнимости (satisfaction). Решатель вернёт либо допустимую конфигурацию, либо сообщит, что решений нет.
7. Вывод результатов
На последнем этапе мы проверяем, было ли найдено решение, и если да, то выводим его в удобочитаемом виде.
// Проверяем статус решения
Если Решение.РешениеНайдено() Тогда
// Если решение найдено, формируем красивый вывод
Сообщение = "Решение судоку:" + Символы.ПС + Символы.ПС;
Для Строка = 0 По РазмерПоля - 1 Цикл
Для Колонка = 0 По РазмерПоля - 1 Цикл
// Получаем значение переменной из найденного решения
ЗначениеЯчейки = Решение.ЗначениеПеременной(ЗначенияЯчеек[Строка][Колонка]);
// Добавляем его в текстовую строку
Сообщение = Сообщение + Строка(ЗначениеЯчейки);
КонецЦикла;
// Переходим на новую строку после вывода каждой строки поля
Сообщение = Сообщение + Символы.ПС;
КонецЦикла;
// Показываем результат пользователю
Сообщить(Сообщение);
Иначе
// Если решение не найдено (например, головоломка составлена неверно)
Сообщить("Решение не найдено.");
КонецЕсли;
Полный код решения задачи
Ниже представлен полный код решения с текстовым выводом результатов. Вы также можете скачать пример в виде готовой обработки, в которую добавлен графический вывод решения в табличном документе.
// Создаем объект модели
Модель = О2
.Модели()
.МодельОграничений()
.СоздатьМодель();
// Вводим начальные данные
РазмерПоля = 9;
РазмерБлока = 3;
// Вводим значения подсказок (0 означает пустую ячейку)
Подсказки = Новый Массив(РазмерПоля);
Подсказки[0] = О2.Утилиты().МассивЧиселИзСтроки("5,3,0, 0,7,0, 0,0,0");
Подсказки[1] = О2.Утилиты().МассивЧиселИзСтроки("6,0,0, 1,9,5, 0,0,0");
Подсказки[2] = О2.Утилиты().МассивЧиселИзСтроки("0,9,8, 0,0,0, 0,6,0");
Подсказки[3] = О2.Утилиты().МассивЧиселИзСтроки("8,0,0, 0,6,0, 0,0,3");
Подсказки[4] = О2.Утилиты().МассивЧиселИзСтроки("4,0,0, 8,0,3, 0,0,1");
Подсказки[5] = О2.Утилиты().МассивЧиселИзСтроки("7,0,0, 0,2,0, 0,0,6");
Подсказки[6] = О2.Утилиты().МассивЧиселИзСтроки("0,6,0, 0,0,0, 2,8,0");
Подсказки[7] = О2.Утилиты().МассивЧиселИзСтроки("0,0,0, 4,1,9, 0,0,5");
Подсказки[8] = О2.Утилиты().МассивЧиселИзСтроки("0,0,0, 0,8,0, 0,7,9");
// Создаем двумерный массив для хранения переменных модели, соответствующих ячейкам судоку
ЗначенияЯчеек = Новый Массив(РазмерПоля, РазмерПоля);
// Обходим все ячейки поля
Для Строка = 0 По РазмерПоля - 1 Цикл
Для Колонка = 0 По РазмерПоля - 1 Цикл
// Смотрим, есть ли в ячейке подсказка
ЗначениеПодсказки = Подсказки[Строка][Колонка];
Если ЗначениеПодсказки = 0 Тогда
// Если подсказки нет, создаем переменную с диапазоном допустимых значений 1-9
ЗначениеЯчейки = Модель.ПеременнаяДиапазона(1, РазмерПоля);
Иначе
// Если подсказка есть, создаем константу (фиксированное значение)
ЗначениеЯчейки = Модель.Константа(ЗначениеПодсказки);
КонецЕсли;
// Сохраняем созданную переменную или константу в массив
ЗначенияЯчеек[Строка][Колонка] = ЗначениеЯчейки;
КонецЦикла;
КонецЦикла;
// Для каждой строки добавляем ограничение: все элементы должны быть разными
Для Строка = 0 По РазмерПоля - 1 Цикл
Модель.Ограничения().ВсеРазличные(ЗначенияЯчеек[Строка]);
КонецЦикла;
// Для каждого столбца...
Для Колонка = 0 По РазмерПоля - 1 Цикл
// ...создаем массив, содержащий все переменные этого столбца
КолонкаПеременных = Новый Массив(РазмерПоля);
Для Строка = 0 По РазмерПоля - 1 Цикл
КолонкаПеременных[Строка] = ЗначенияЯчеек[Строка][Колонка];
КонецЦикла;
// ...и добавляем для этого массива ограничение на уникальность
Модель.Ограничения().ВсеРазличные(КолонкаПеременных);
КонецЦикла;
// Создаем массив для сбора переменных блока
БлокПеременных = Новый Массив(РазмерБлока * РазмерБлока);
// Цикл по вертикальным блокам (0,1,2)
Для БлокСтрока = 0 По РазмерБлока - 1 Цикл
// Цикл по горизонтальным блокам (0,1,2)
Для БлокКолонка = 0 По РазмерБлока - 1 Цикл
Индекс = 0; // Индекс для заполнения массива БлокПеременных
// Внутренние циклы по ячейкам внутри одного блока 3x3
Для Строка = БлокСтрока * РазмерБлока По (БлокСтрока + 1) * РазмерБлока - 1 Цикл
Для Колонка = БлокКолонка * РазмерБлока По (БлокКолонка + 1) * РазмерБлока - 1 Цикл
// Собираем переменные ячеек блока в массив
БлокПеременных[Индекс] = ЗначенияЯчеек[Строка][Колонка];
Индекс = Индекс + 1;
КонецЦикла;
КонецЦикла;
// Добавляем ограничение на уникальность для текущего блока
Модель.Ограничения().ВсеРазличные(БлокПеременных);
КонецЦикла;
КонецЦикла;
// Выполняем поиск решения
Решение = Модель.Решить();
// Проверяем статус решения
Если Решение.РешениеНайдено() Тогда
// Если решение найдено, формируем красивый вывод
Сообщение = "Решение судоку:" + Символы.ПС + Символы.ПС;
Для Строка = 0 По РазмерПоля - 1 Цикл
Для Колонка = 0 По РазмерПоля - 1 Цикл
// Получаем значение переменной из найденного решения
ЗначениеЯчейки = Решение.ЗначениеПеременной(ЗначенияЯчеек[Строка][Колонка]);
// Добавляем его в текстовую строку
Сообщение = Сообщение + Строка(ЗначениеЯчейки);
КонецЦикла;
// Переходим на новую строку после вывода каждой строки поля
Сообщение = Сообщение + Символы.ПС;
КонецЦикла;
// Показываем результат пользователю
Сообщить(Сообщение);
Иначе
// Если решение не найдено (например, головоломка составлена неверно)
Сообщить("Решение не найдено.");
КонецЕсли;