Разминка для мозгов: генерация всех возможных слов заданной длины с произвольным алфавитом
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.
Дан алфавит:
Дано некое N:
Необходимо перебрать все возможные слова (комбинации) заданной длины (N),
которые можно составить с помощью заданного алфавита (abc).
Пример:
Алфавит: «1», «2».
Длина слов: 3
Результат вывода в консоль (порядок не важен):
1 1 1
1 1 2
1 2 1
1 2 2
2 1 1
2 1 2
2 2 1
2 2 2
Дополнительно:
1) Возможность составления букв алфавита из нескольких символов:
2) Возможность работы с большими алфавитами и длинами слов.
3) Многопоточная генерация и обработка.
4) Возможность из клиентского кода задать действие, которое будет выполняться с каждым словом.
5) Буквы алфавита могут частично или полностью совпадать.
Использовать можно стандартные библиотеки C и C++ (в т.ч. из C++17).
Так что можете тоже мозгами пораскинуть. :)
upd: исправлен алфавит
Видимо имелось ввиду
std::string abc[] = { ... };
или дажеconst std::string abc[] = { ... };
?Я бы даже сказал std::vector<std::string>
Я так прикинул, программка получилась на ~30 строк с реализацией 1, 2, 5 из «дополнительно». Без использования STL, кроме string и vector.
С многопоточностью не заморачивался.
Пункт 4 — не понял. Что имеется ввиду?
Ну у нас же идет только вывод комбинаций,
а пунтк 4 предполагает, что клиент может задать поведение,
т.е. передать в нашу функцию какой-нибудь функтор,
который будет принимать строку с текстом (комбинацию),
и как-то её обрабатывать.
Есть вот такая, глупенькая реализация: http://rextester.com/HPLG75261
Включает в себя 1, 3, 4, 5.
А вот реализация с 1, 2, 5: http://rextester.com/LZTR96135
Добавить 4 сюда не проблема.
Посмотрел код по ссылкам — жуть. Особенно вариант с потоками. Я как-то обошёлся гораздо меньшей кровью. Но мой вариант сложно перевести на многопоточную обработку. Прикрутил пункт 4.
Поскольку ты уже опубликовал ссылки на варианты решения, и интриги больше нет, выкладываю свой вариант.
А у тебя, видимо, припасено решение в темплейтно-функциональном (привет, STL!) стиле под флагом C++17? Выкладывай. Интересно посмотреть.
А вот вариация на тему http://rextester.com/LZTR96135. Вроде даже попроще получилось.
Прикрутить 4 — не проблема. Можно параллелить аналогично http://rextester.com/HPLG75261.
За счет рекурсии, от которой я как раз отказался.
В результате получили проблему — отвалился пункт 2.
Мы не можем теперь работать,
если количество комбинаций превышает максимум в long long. :)
Я в первом сообщении изначально писал,
что хотелось бы без длинной арифметики обойтись,
но потом убрал и как результат — Вы нарвались на те же грабли что и я. )))
Если это убрать, то уже многопоточность прикрутить хз как даже. )))
Хотя у меня есть одна идейка, но пока еще покурю на досуге эту тему. :)
Нету ничего такого. )))
Ы-ы, жутковато, да. :D
А зачем лямбдой-то? )))
С чего бы? У меня глубина рекурсии определяется количеством слов в «предложении», т.е. N. Длина слов вообще не при делах, размер алфавита обрабатывается в цикле. Параметры вызова функции
gen
не «распухают» — единственно, что увеличивается, это содержимое строкиs
, но оно в куче. Размер стека по умолчанию в MSVC++, если не ошибаюсь, 1Мб. Так что N может быть достаточно большим. Понятно, что можно задать такое N, что стека не хватит, но эту ситуацию можно отловить не доводя дела до runtime error. Если уж углубляться, то и памяти в куче может не хватить.Ну тогда остаётся только способ, предложенный в твоих ссылках, когда отлавливается «переполнение» при работе с массивом индексов. Кстати, в коде с многопоточной обработкой тоже высчитывается общее количество комбинаций. И не совсем корректно: во-первых, присваивание идёт переменной типа
size_t
akaunsigned int
, который может быть меньшеunsigned long long
(я, кстати в своём коде упустил, что типlong long
можно сделать беззнаковым); а во-вторых, неявное преобразованиеdouble
, полученное отpow
, кsize_t
с потенциальной потерей значимости. Так что твой пример многопоточного кода по вышепроцитированному критерию тоже можно отправить в корзину.Ну вые@нулся ;) Не хотел вводить ещё одну функцию и не хотел писать код вычисления степени непосредственно в функции, чтобы типа «не засорять код». В итоге получилась та же фигня, только в профиль, да ещё и с обязательной поддержкой C++11. Но переписывать уже было лениво.
Как резюме: (1) исходная задача поставлена немного расплывчато. Хотелки и ограничения надо было указать более точно. (Впрочем, сам тоже грешу на эту тему.) (2) задача не для начинающих.
size_t — тип, который может выразить максимальный размер объекта на указанной платформе.
И он не обязан быть unsigned int,
ну и о x32 платформе я уже потихоньку забываю,
ибо времена её проходят на десктопах.
В начальном коде был не std::pow. :)
Это всегда мешает, да? )))
А с другой стороны помогает
от всяких оверинженирингов. )))
Есть мысль как прикрутить многопоточность.
Использовать твою функцию генерации следующей последовательности. В массиве индексов каждый индекс изменяется от 0 до
длины_алфавита - 1
. Первый индекс изменяется медленнее всего. Для каждого потока задаём диапазон обработки по первому индексу из массива индексовдлина_алфавита / общее_количество_потоков
. Эту идею можно распространить и на следующие индексы для более равномерного разделения общего объёма вычислений по потокам, но код будет сложнее.Или можно задавать на обработку потоку только очередной кусок с фиксированным первым индексом, или с фиксированными несколькими первыми индексами в массиве индексов.
А вот интересно, почему при многопоточной обработке при выводе результатов в поток не происходит «вклинивания» результатов из одного потока в результаты другого? Оператор
<<
захватывает поток в монопольное пользование?Да, оно потокобезопасно, если выставлен sync_with_stdio.
Но опять же, безопасен лишь один вывод, т.е.
уже не будет безопасно.
Есть идея, сделать шаг не в единичку, а в количество потоков.
Тогда первый поток запускаем с начальной комбинацией 1,
второй — с комбинацией 2, и т.д.
Каждый поток будет шагать по «своим комбинациям».
И ещё одно наблюдение. Твой многопоточный код из-за операции вывода на консоль при 8-ми потоках грузит проц на 15-20% (в пиках до 32%). А вот если вывод на консоль убрать — тогда честные 100% загрузки проца.
Ну так вывод же синхронизируется, так что,
по факту, другие потоки спят в это время,
поэтому и нагрузка такая — работает один поток,
остальные ждут или дорабатывают до момента синхронизации.