Многопоточный алгоритм решения судоку? - PullRequest
17 голосов
/ 12 мая 2009

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

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

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


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

Кроме того, не может быть единственного решения - допустимым входным значением может быть абсолютно пустая доска. Я должен сообщить min(1000, number of solutions) и отобразить один из них (если он существует)

Ответы [ 13 ]

17 голосов
/ 12 мая 2009

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

Теперь создайте поток для каждого варианта и попробуйте оба варианта одновременно. Создавать новый поток можно только в том случае, если в системе уже имеется <некоторое количество потоков (это будет ваш входной аргумент), в противном случае просто используйте простое (т.е. существующее) однопоточное решение. Для дополнительной эффективности получите эти рабочие потоки из пула потоков. </p>

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

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

Каждая подпрограмма, которая вызывает несколько потоков для выполнения работы, должна вызывать n-1 потоков, когда есть n частей работы, выполнять n-ную часть работы и затем ждать с объектом синхронизации, пока все остальные потоки не будут завершены. Затем вы оцениваете их результаты - у вас есть n состояний доски, вы возвращаете тот, у которого наименьшее количество ходов.

9 голосов
/ 12 мая 2009

Многопоточность полезна в любой ситуации, когда один поток должен ждать ресурс, а тем временем вы можете запустить другой поток. Это включает в себя поток, ожидающий запрос ввода-вывода или доступ к базе данных, в то время как другой поток продолжает работу ЦП.

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

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

Но я помню некоторую домашнюю работу, которую я сделал в Uni, и она была также бесполезна (код на Фортране, чтобы увидеть, как глубоко проложился туннель, когда вы копали под углом 30 градусов на одну милю, затем 15 градусов на другую милю - да Я довольно старый :-). Суть в том, чтобы показать, что вы можете сделать это, а не то, что это полезно.

к алгоритму.

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

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

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

Что бы я предложил, это выделить каждое правило для потока и сделать так, чтобы они совместно использовали сетку. Каждый поток будет выполнять свое собственное правило и только это правило.

Обновление:

Джон, исходя из ваших правок:

[edit] Я забыл упомянуть, что количество используемых потоков указано в качестве аргумента программы, так что, насколько я могу судить, это никак не связано с состоянием головоломки ...

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

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

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

Кроме того, поскольку вы ограничиваете количество потоков, вам нужно будет поработать над магией пула потоков, чтобы добиться этого.

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

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

Итак, допустим, есть только одно решение (правда, Судоку :-). Первый рабочий поток откажется от решения, не найдя вилок, и это будет точно так же, как в вашей текущей ситуации.

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

Другая нить возьмет доску с 3 в этой ячейке и продолжит. Таким образом, у вас одновременно работают два потока, которые обрабатывают две возможности.

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

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

Когда все потоки простаивают, а рабочая очередь пуста, main либо найдет, либо не найдет решение. Он также будет иметь количество решений.

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

5 голосов
/ 12 мая 2009

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

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

4 голосов
/ 12 мая 2009

Вот жадный однопоточный решатель с перебором:

  1. Выберите следующую пустую ячейку. Если больше нет пустых клеток, победа!
  2. Возможное значение ячейки = 1
  3. Проверка на недопустимое частичное решение (дубликаты в строке, столбце или блоке 3x3).
  4. Если частичное решение недопустимо, увеличьте значение ячейки и вернитесь к шагу 3. В противном случае перейдите к шагу 1.

Если вы посмотрите на схему выше, комбинация шагов 2 и 3 - очевидные кандидаты для многопоточности. Более амбициозные решения включают создание рекурсивного исследования, которое порождает задачи, переданные в пул потоков.

РЕДАКТИРОВАТЬ, чтобы ответить на этот вопрос: «Я не понимаю, как вы можете найти разные решения одной и той же проблемы в одно и то же время, не сохраняя несколько копий головоломки».

Вы не можете. В этом весь смысл. Однако конкретный 9-ниточный пример может сделать преимущества более очевидными:

  1. Начните с примера проблемы.
  2. Найдите первую пустую ячейку.
  3. Создайте 9 потоков, где каждый поток имеет свою собственную копию проблемы со своим собственным индексом в качестве значения кандидата в пустой ячейке.
  4. В каждом потоке запустите исходный однопоточный алгоритм для этой локальной измененной копии потока.
  5. Если один из потоков находит ответ, остановите все остальные темы.

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

2 голосов
/ 12 мая 2009

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

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

2 голосов
/ 12 мая 2009

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

Для первого из них подход Пакса на основе правил или Том Лейс "возьмет на себя многопоточность существующего алгоритма обратного отслеживания" может быть подходящим вариантом.

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

1 голос
/ 12 мая 2009

Вы сказали, что использовали обратное отслеживание для решения проблемы. Что вы можете сделать, это разделить пространство поиска на два и обработать каждое пространство потоком, тогда каждый поток будет делать то же самое, пока вы не достигнете последнего узла. Я сделал решение, которое можно найти на www2.cs.uregina.ca/~hmer200a, но используя один поток, но механизм разделения пространства поиска существует с использованием ветвления и привязки.

1 голос
/ 12 мая 2009

Некоторые общие замечания: я не запускаю процессы параллельно, если 1) проблему легко разделить 2) я знаю, что получу выгоду от этого - например, Я не попаду в другое узкое место. Я полностью избегаю разделения изменяемых значений между потоками - или минимизирую это. Некоторые люди достаточно умны, чтобы безопасно работать с мьютексами. Я не.

Вам нужно найти точки в вашем алгоритме, которые создают естественные ветви или большие единицы работы. После того, как вы определили блок для работы, вы помещаете его в очередь, чтобы поток мог его забрать. В качестве тривиального примера. 10 баз для обновления. Запустите обновление async на всех 10 серверах. Подождите, пока все завершится. Я могу легко избежать разделения состояния между потоками / процессами и легко агрегировать результаты.

Что приходит в голову для судоку, так это то, что эффективное решение судоку должно сочетать 2-3 (или более) стратегий, которые никогда не выходят за пределы определенной глубины. Когда я занимаюсь судоку, очевидно, что в любой момент разные алгоритмы обеспечивают решение с наименьшей работой. Вы можете просто запустить несколько стратегий, позволить им исследовать ограниченную глубину, дождаться отчета. Промыть, повторить. Это позволяет избежать «грубого» решения. Каждый алгоритм имеет свое собственное пространство данных, но вы комбинируете ответы.

Sciam.com опубликовал статью об этом год или два назад - похоже, она не является публичной.

1 голос
/ 12 мая 2009

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

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

0 голосов
/ 03 мая 2014

Вот моя копейка. Надеюсь, это поможет.

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

Старайтесь как можно больше избегать обмена данными между потоками. Используйте их только при необходимости

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

...