@Soft_touch_plastic

Как лучше оптимизировать такие действия с массивами?

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

Алгоритм оптимизирован в плане логики, однако я решил переписать его на c++ и столкнулся с удручающей потерей в скорости.
Например только подготовка данных заняла около секунды в c++, а выполнение всего алгоритма в питоне на 10 строках заняло 800ms.
Пробовал разные виды массивов заполнять, все равно выше 1 секунды на одно лишь заполнение массива 100 000 строк не удавалось выскочить.

Предполагается что в дальнейшем будут храниться структуры данных в виде словарей, в которых ключом является число - длина строк, а значением массив строк такой длинны. По идее будет использован map<int, vector<string>>, однако не убьет ли это быстродействие крестов к нулю?
Как решаются подобные задачи, неужели питон будет действительно быстрее?
  • Вопрос задан
  • 182 просмотра
Решения вопроса 1
wataru
@wataru Куратор тега C++
Разработчик на С++, экс-олимпиадник.
Во-первых, может быть проблема со вводом. cin и cout работают медленно с большими объемами данных. Или читайте через scanf, или можно отключить синхронизацию с stdio.

Во-вторых, описанная вами структура данных быстродействие не убъет, но мне не понятно, чем она поможет в задаче.

Вам точно понадобятся map-ы из строки во что-то еще. В питоне вы наверняка использовали словари (использовали строки в виде ключа в массиве), вот это оно и будет в с++. Можно поэксперементировать std::unordered_map может быть побыстрее std::map. А вообще, особенно быстрое решение вы можете получить используя cтруктуру данных бор (оно же trie). Правда, ее придется самостоятельно писать.
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
@rPman
Какое точное определение неправильного слова и как определить правильное?
Что сильнее делает слово неправильным, отсутствие буквы? перестановка? подмена? а какое более неправильное? есть ли разница, в какой позиции слова произошла ошибка, в первом символе или остальных?
Например список слов без ошибок:
море
мор
март

И вот у нас слова:
мар - это мор или март?
маре - это март или море?
так - это


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

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

Дальше алгоритм
* Если решать в лоб, никаких ресурсов не хватит, просто для каждого слова из списка вычисляешь оценку на ошибку с правильным, перебирая их до тех пор пока не встретится с оценкой 0.
Трудоемкость - квадрат на экспоненту от средней длины слова - т.е. долго
* Предварительно можно исходные анализируемые данные собрать в map слов, чтобы исключить повторения
* Можно чуть чуть оптимизировать этот алгоритм, если слов с ошибкой в исходном файле мало, перед сравнением искать слово по словарю, построив map заранее, и искать первую минимальную ошибку сравнения, т.е. для правильных слов использовать максимально быстрый алгоритм поиска, исключив их из медленного алгоритма сравнения
* Дальнейшая оптимизация - расширение последнего шага - можно заранее создать структуру в памяти для всех возможных значений строк с единичным изменением правильных слов (т.е. для каждого правильного слова поместить в map это измененное слово и ссылку на правильное) - получим массив ошибочных слов с ошибкой 1, т.е. все слова с ошибкой 1 могут быть обнаружены со скоростью работы map, так как количество изменений в данном случае сравнимо с количеством используемых символов (умножить на 3) а в задаче речь о словах, т.е. количество символов мало? то на каждое слово в map будет 3*n записей
* Точно так же можно сделать массив всех ошибочных слов для 2-ух изменений (например 1-изменение на каждую запись от списка с 1-изменением)
* 3-ех,..4-ех и т.п.

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

* Дальнейшая оптимизация - перевернуть алгоритм на поиск в ширину по графу всех возможных изменений правильных слов (это не дерево а граф, так как правильные слова за конечное количество изменений будут переходить друг в друга или другие ошибочные слова, созданные из других правильных слов), т.е. запускаем поиск и на каждом шаге делаем сравнение полученной строки с ошибкой со всеми словами из анализируемого списка, тут поиск быстрый по map)
Этот подход имеет смысл если анализируемых слов сильно много (и они все с ошибками) и накладные расходы на сравнение со всеми комбинациями ошибок - не велики, по памяти - она так же потребуется на поддержание самого поиска в ширину
Ответ написан
@res2001
Developer, ex-admin
Читайте файлы сразу большими блоками (вплоть до всего файла сразу). Под большие блоки можете использовать std::vector<char> с предварительно установленным размером вектора (std::vector::reserve())
Затем вручную делите прочтенный блок на строки, видимо, заменой \n на 0.
Все указатели на найденные строки сразу складывайте в используемую в алгоритме структуру данных.
Не используйте std::string, т.к. он реаллоцирует память на каждый чих, это приводит к повторному выделению того же самого объема памяти но порезанному на мелкие куски и дополнительному копированию строк. Используйте std::string_view (есть в С++17) или вообще сырые Сишные строки, как самый быстрый вариант.

Вообще все массивы в плюсах (vector, array, "сырые" динамические и статические массивы) работают одинаково быстро, если рассматривать операцию обращения к элементу массива по индексу. Но в vectorе многие другие операции могут приводить к реаллокации памяти и копированию массива. В сырых динамических массивах вы не можете просто так изменить размер массива, это надо делать явно с помощью вызова realloc, а потому тут вы эту операцию явно контролируете, в векторах же (как и строках) это происходит не явно, поэтому часто разработчики не придают этому значения, тогда как обращения к менеджеру памяти достаточно дороги в плане производительности.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы