Алгоритм поиска с префиксами

تم الإنشاء في: ٣٠ ديسمبر ٢٠٢٤

تمت الإجابة باستخدام OpenAI 01 بواسطة OpenAI01

سؤال

Наибольший общий префикс двух слов — это самое длинное слово, которое является началом как первого слова, так и второго. Например, слова ”хобот” и ”хорошо” имеют наибольший общий префикс ”хо”.

Наибольшие общие префиксы широко используются в поисковых технологиях, например, в подсказках при поиске. База данных содержит
N
N слов, которые нужно показывать в качестве поисковых подсказок. Стажёру было поручено проверить, входит ли введённое пользователем слово в базу данных. Стажёр реализовал поиск следующим образом: он идёт по словам из базы данных в том порядке, в котором они там записаны, и сравнивает очередное слово со словом из запроса буква за буквой, до тех пор, пока не найдутся отличающиеся буквы. Если слово из запроса совпало со словом из базы данных — поиск прекращается. Количество действий для этого алгоритма можно определить как количество слов из базы данных, с которыми проводилось сравнение, плюс сумма длин всех наибольших общих префиксов сравниваемых слов из словаря и слова из запроса.

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

Формат ввода
В первой строке содержится число
N
N (
1

N

30000
1≤N≤30000) — количество слов в базе данных.

В каждой из следующих
N
N строк записано по одному слову. Слова состоят из прописных английских букв, их длина не превосходит 30.

В следующей строке записано число
Q
Q (
1

Q

30000
1≤Q≤30000) — количество запросов.

В каждой из следующих
Q
Q строк записано по одному запросу. Запросы состоят из прописных английских букв, их длина не превосходит 30.

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

الفكرة

Vergleich von Wörtern

Ich schaue mir eine Datenbank von Wörtern an, vergleiche diese mit jeder Abfrage und zähle Übereinstimmungen bis zum ersten Unterschied. Das Ziel ist eine effiziente Berechnung der Ergebnisse.

Lösung festlegen

Ich entschlüssle das Einrichten einer Umgebung für C++ Code unter Windows 11. Das umfasst die Installation eines Editors, die Einrichtung von Git Bash und die richtige Apdchecktierung von Fenstern für eine produktive Entwicklungsumgebung.

Optimierung der Sequenzsuche

Ich schaue mir jetzt eine effizientere Vorgehensweise an: Das Vorverarbeiten der Datenbank. Dabei iteriere ich durch die Datenbank und summiere die Übereinstimmungen, bis ein Treffer gefunden oder das Ende erreicht ist.

Summe der LCPs

Ich plane, die LCPs für jede Abfrage effizient vorzuverarbeiten, um schnell die Übereinstimmungen zu berechnen. Dabei berücksichtige ich die Position des Matchs und die Anzahl der verglichenen Wörter.

Trie und Hashing

Ich denke darüber nach, die Datenbankwörter in einem Trie zu speichern und an jedem Knoten die Indizes der Wörter mit diesem Präfix zu speichern. Ein Hashing-Ansatz könnte ebenfalls nützlich sein.

Suchen nach Alternativen

Ich denke darüber nach, wie man jedes Wort der Datenbank vor-summieren könnte. Mögliche Lösungen beinhalten: Präfixe vorverarbeiten, eine binäre Indexbaumstruktur gleichmäßig füllen oder ein Hashing-Verfahren anwenden.

Überlege mir mögliche Lösungen

Ich denke darüber nach, wie die Vorverarbeitung für große Datenmengen effizient gestaltet werden kann. Klare Ansätze zur Optimierung sind erst auf den zweiten Blick zu erkennen.

Trie verwenden

Ich denke darüber nach, wie man einen Trie aus den Datenbankwörtern erstellen könnte. Dabei könnte jeder Knoten das früheste Index und die Summe der LCPs bis zu diesem Punkt speichern.

Überlege verschiedene Ansätze

Ich schaue mir Hashing und Trie-basierte Methoden an, um die Anzahl der Vokabeln mit einem bestimmten Präfix und die Summe der Präfixlängen effizient zu berechnen.

Effiziente LCP-Summe berechnen

Ich denke darüber nach, wie man für jedes Wort den Index finden und die LCP-Summe berechnen kann, indem man die Übereinstimmungen bis zum ersten Unterschied zählt.

Vorverarbeiten und optimieren

Ich denke darüber nach, wie man für jedes Wort LCPs bis zu einem bestimmten Index vorverarbeiten kann, um die effiziente Berechnung von Übereinstimmungen zu ermöglichen.

Prefix-Zählung anpassen

Ich denke darüber nach, wie man die Anzahl der Vokabeln mit einem bestimmten Präfix effizient berechnen kann, indem man für jedes Präfix bis Index i die Wortanzahl zählt.

Vorverarbeiten und analysieren

Ich denke darüber nach, wie man Vokabeln mit bestimmten Präfixen vorverarbeiten kann. Eine Möglichkeit wäre, für jedes Präfix eine Liste von Indizes zu speichern, um die Wortanzahl effizient zu bestimmen.

Vorverarbeiten und analysieren

Die Idee ist, jedes Präfix aufzulisten und zu speichern, wo es vorkommt. Dann kann man die Summe der LCPs für jede Abfrage effizient berechnen, indem man Indizes zählt und Präfixe gruppiert.

Trie und Präfixe

Ich schaue mir einen Trie an, bei dem jeder Knoten ein Präfix und eine sortierte Liste von Indizes gespeichert hat. So kann ich effizient LCPs für jede Abfrage berechnen.

Trie aufbauen

Ich baue einen Trie, bei dem jeder Knoten 26 Kinder hat, sortierte Listen von Indexe speichert und markiere, wenn ein Wort endet. పోలీస

Effiziente Suche

Ein Trie wird erstellt, wo jeder Knoten eine Liste von Indizes läuft, gefolgt von einer Abfrage, um den ersten Index zu finden, wo die Abfrage erscheint.

Trie durchsuchen

Ich schaue mir an, wie man den ersten Index K im Trie findet und die Übereinstimmungen der Präfixe bis zu diesem Punkt zählt, um die LCP-Summe zu bestimmen.

Überprüfen und analysieren

Ich schaue mir an, ob die Muplanen ZIP-Datei korrekt ist, indem ich die Dateigroße und Integrität prüfe, um sicherzustellen, dass alles ordnungsgemäß funktioniert.

Daten sammeln und analysieren

Ich denke über Schritte nach, um N Wörter zu speichern, sie in einem Trie einfügen und Q Abfragen lesen. Dann analysiere ich die Trie-Traversierung und summiere die LCPs.

Traversieren und abzählen

Ich schaue mir die Trie-Traversierung an, summiere die Präfix-Prefix-Indizes für jedes Präfix und berechne die LCP-Summe. Dabei berücksichtige ich alle Übereinstimmungen bis zum ersten Unterschied.

Liste für Indizes

Ich denke darüber nach, wie eine Liste von Indizes in Trie-Knoten als Vektor<int> effizient Speicher spart und schnelles Traversieren unterstützt. Das führt mich zu einer besseren Implementierung der Trie-Datenstruktur.

Entwicklung einer Trie-Datenstruktur

Ich plane, eine Tries-Datenstruktur mit Knoten, Array für 26 Kind-Indizes und einem Vektor<int> für Indizes zu nutzen, um die Trie-Elemente effizient zu speichern und abzufragen.

Trie erstellen und abfragen

Ich beschreibe Schritte zur Erstellung eines Trie, in dem Wörter und Präfix-Indizes gespeichert sind. Dann informiere ich mich über Abfragen und wie die LCP-Summe effizient berechnet wird.

Gedanken für 35 Sekunden

الإجابة

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

Подход:

  1. Построение Trie (префиксного дерева):

    • Построим префиксное дерево (Trie) из всех слов базы данных.
    • На каждом узле Trie будем хранить список индексов слов, которые проходят через этот узел (то есть имеют соответствующий префикс).
    • Также будем сохранять первое вхождение каждого слова для быстрого поиска.
  2. Обработка запросов:

    • Для каждого запроса найдем индекс первого вхождения слова в базе данных (если оно есть). Если слово отсутствует, будем считать, что сравниваются все N слов.
    • Затем, пройдём по префиксам запроса и для каждого префикса посчитаем количество слов в базе данных, которые имеют этот префикс и находятся до индекса K (индекс первого вхождения или N).
    • Суммируем все такие количества, а также количество сравниваемых слов, чтобы получить итоговое количество действий.

Реализация:

Ниже представлен код на языке C++, реализующий описанный подход:

#include <bits/stdc++.h> using namespace std; // Структура узла Trie struct TrieNode { int children[26]; vector<int> indices; TrieNode() { for(int i=0;i<26;i++) children[i]=-1; } }; int main(){ ios::sync_with_stdio(false); cin.tie(0); int N; cin >> N; // Инициализация Trie с корневым узлом vector<TrieNode> trie; trie.emplace_back(); // корень имеет индекс 0 // Карта для хранения первого вхождения слова unordered_map<string, int> word_map; for(int i=1;i<=N;i++){ string word; cin >> word; if(word_map.find(word) == word_map.end()){ word_map[word] = i; } // Вставка слова в Trie int current = 0; // начинаем с корня for(char c: word){ int c_idx = c - 'A'; if(trie[current].children[c_idx] == -1){ trie[current].children[c_idx] = trie.size(); trie.emplace_back(); } current = trie[current].children[c_idx]; trie[current].indices.push_back(i); } } int Q; cin >> Q; while(Q--){ string query; cin >> query; int K; auto it = word_map.find(query); if(it != word_map.end()){ K = it->second; } else{ K = N; } long long sum = 0; int current = 0; // начинаем с корня for(char c: query){ int c_idx = c - 'A'; if(trie[current].children[c_idx] == -1){ break; } current = trie[current].children[c_idx]; // Используем upper_bound для поиска количества индексов <= K // Так как индексы в trie[current].indices отсортированы int cnt = upper_bound(trie[current].indices.begin(), trie[current].indices.end(), K) - trie[current].indices.begin(); sum += cnt; } // Количество действий: количество сравниваемых слов + сумма LCP cout << K + sum << "\n"; } }

Пояснение к коду:

  1. Построение Trie:

    • Для каждого слова из базы данных мы проходим по его символам и создаём соответствующие узлы в Trie, если они ещё не существуют.
    • В каждом узле хранится список индексов слов, которые имеют данный префикс.
  2. Обработка запросов:

    • Для каждого запроса определяем K — индекс первого вхождения слова или N, если слово отсутствует.
    • Проходим по символам запроса, продвигаясь по Trie.
    • Для каждого префикса запроса используем upper_bound, чтобы найти количество слов с таким префиксом до индекса K.
    • Суммируем все такие количества и добавляем к ним K, получая итоговое количество действий.

Сложность:

  • Построение Trie: O(N * L), где L — средняя длина слова (максимум 30).
  • Обработка запросов: O(Q * L * log(N)), так как для каждого запроса и каждого префикса выполняется бинарный поиск.

Таким образом, данный подход эффективно решает задачу в рамках заданных ограничений.

مشاركة هذا السؤال والجواب