Поиск самого длинного словаря. Ключевое совпадение во фразе

У меня есть SortedDictionary<string, string>, упорядоченный по убыванию длины ключа, в форме:

  • рыжая лиса - адрес1
  • ласка - адрес2
  • лисы - адрес3
  • лиса - адрес3 и т. д.
    и список фраз, например
    "сегодня рыжая лиса дома"
    "сегодня лис не будет"
    и т. д.

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

Примеры выше должны вернуться:
"рыжая лиса" - адрес1
"лисы" - адрес3
У меня есть решение методом перебора ключей, но, учитывая количество ключей и необходимость выполнять поиск по десяткам фраз, я ищу более умный подход. -1

Я провел кое-какие измерения производительности.
При словаре из 100 ключей поиск совпадений по 100 фразам занимал в среднем 65 мс.
Если время выполнения линейно зависит от длины словаря и количества фраз, то весь процесс займет около 20 секунд (только время поиска).
Учитывая время, необходимое для чтения фраз из файлов и использования совпадений, на весь прогон уйдет примерно 3-4 минуты.
Пользователю за это время придется посмотреть какую-нибудь анимацию, а можно выпить стаканчик чего-нибудь.
Будет ли это приемлемо?
Если да, то подход грубой силы подойдет.
Если нет, то я все еще открыт для предложений.

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

JonasH 25.07.2024 09:41

@JonasH На самом деле мне приходится перебирать около 300 файлов, каждый из которых в среднем содержит 10 фраз. В сумме получается 500 x 300 x 10 = 1,5 миллиона строк, операций Содержит(), довольно много.

alexb 25.07.2024 10:34

Я бы, вероятно, порекомендовал какую-нибудь стороннюю библиотеку для полнотекстового поиска. Может быть, Lucene.Net. Но требования к производительности и то, как часто вам нужно это делать, по-прежнему имеют значение. Создание поискового индекса также займет некоторое время.

JonasH 25.07.2024 10:47

Вы ищете какую-либо подстроку, скажем, d fox is ho или ограниченный/предварительно обработанный набор из них, например. только целые слова, без знаков препинания - red fox is home, fox is home, red fox is, но не d fox is ho

Dmitry Bychenko 25.07.2024 11:00

Похоже, вам нужен индекс ngram. Какова минимальная и максимальная длина строк поиска?

Mathias R. Jessen 25.07.2024 13:22

@Dmitry Фразы из одного или двух слов, только целые слова, без знаков препинания, без учета регистра.

alexb 25.07.2024 13:30

QMathias Одно слово мин. два слова максимум. Я не знаком с нграмом.

alexb 25.07.2024 13:32
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
105
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Судя по вашему требованию, подход грубой силы действительно не лучший способ сделать это. Гораздо более эффективным подходом было бы использование типа «Trie» (дерево префиксов). Использование «Trie» может помочь вам эффективно найти самый длинный совпадающий ключ в заданной фразе. Просто создайте структуру «Tire», а затем вставьте каждый ключ из вашего «SortedDictionary» в «Trie». Каждый узел в структуре «Trie» будет представлять символ ключа. В конце каждого ключа сохраните соответствующее значение (адрес). Для поиска самого длинного совпадения вы можете реализовать следующее: Для каждой фразы переберите каждую подстроку и найдите ее в структуре «Trie». Затем просто отслеживайте самое длинное совпадение, найденное для каждой фразы. Я считаю, что это то, что вам нужно. Посмотрите здесь: Реализация Trie на C#, Использование класса Trie для эффективного поиска по тексту в C#

Я просто поместил код ниже. Я думаю, что в целом он делает то, что вам нужно:

public sealed class Trie
{
    private readonly TrieNode _root = new();

    public void Insert(string key, string address)
    {
        var node = _root;
        foreach (var ch in key)
        {
            if (!node.Children.ContainsKey(ch))
            {
                node.Children[ch] = new TrieNode();
            }

            node = node.Children[ch];
        }

        node.Address = address;
    }

    public string SearchLongestMatch(string phrase, out string address)
    {
        address = null;
        var maxLength = 0;
        TrieNode longestMatchNode = null;

        for (var i = 0; i < phrase.Length; i++)
        {
            var node = _root;
            var length = 0;
            for (var j = i; j < phrase.Length; j++)
            {
                var ch = phrase[j];
                if (node.Children.TryGetValue(ch, out var child))
                {
                    node = child;
                    length++;
                    if (length > maxLength)
                    {
                        maxLength = length;
                        longestMatchNode = node;
                    }
                }
                else
                {
                    break;
                }
            }
        }

        if (longestMatchNode != null)
        {
            address = longestMatchNode.Address;
            var index = phrase.IndexOf(longestMatchNode.Address, StringComparison.Ordinal);
            if (index != -1)
            {
                return phrase.Substring(index, maxLength);
            }
        }

        return null;
    }
}
public class TrieNode
{
    public Dictionary<char, TrieNode> Children { get; set; } = new();
    public string Address { get; set; }
}

public sealed class DescendingComparer<T> : IComparer<T> where T : IComparable<T>
{
    public int Compare(T? x, T? y)
    {
        return y.CompareTo(x);
    }
}
public class Program
{
    public static void Main()
    {
        var dict = new SortedDictionary<string, string>(new DescendingComparer<string>())
            { { "red fox", "address1" }, { "weasel", "address2" }, { "foxes", "address3" }, { "fox", "address4" } };
        var trie = new Trie();
        foreach (var kvp in dict)
        {
            trie.Insert(kvp.Key, kvp.Value);
        }

        var phrases = new List<string> { "today the red fox is home", "no foxes today" };
        foreach (var phrase in phrases)
        {
            var match = trie.SearchLongestMatch(phrase, out var address);
            Console.WriteLine($"Phrase: \"{phrase}\" - Match: \"{match}\" - Address: \"{address}\"");
        }
    }
}

Мне неясно, как Trie поможет при поиске подстрок, т. е. .Contains(...). Однако суффиксное дерево может сделать это.

JonasH 25.07.2024 09:59

Я имею в виду, что это может быть иерархический шаблон Root-Node, поэтому он должен состоять из классов Tire - TireNode. Где класс TrieNode представляет каждый узел в Trie со словарем для дочерних элементов и строкой для адреса. А класс Trie занимается вставкой ключей и поиском самого длинного совпадения. Таким образом, Tire должен реализовать что-то вроде «GetLongest(строковая фраза, адрес выходной строки), который находит самый длинный совпадающий ключ в заданной фразе и возвращает самый длинный адрес узла совпадения.

Andrii 25.07.2024 10:17
Ответ принят как подходящий

Я решил это другим подходом:
Данный:

  • набор F файлов, каждый из которых содержит P фраз по W слов; обычно F составляет 300, P=10 в среднем и W=5 в среднем.
  • Словарь, содержащий N статей; Н=1000

Для каждой фразы в файлах найдите самую длинную подфразу из 1–3 слов, соответствующую одному из ключей словаря.

Решение:
Для каждой фразы p в F создайте список, содержащий все подфразы из 1, 2 и 3 слов, упорядоченные по убыванию длины; будут подфразы 3W-3.
Для каждой подфразы в списке проверьте, соответствует ли она ключу в словаре.

(Напротив, метод грубой силы для каждой фразы p в каждом файле перебирает ключи словаря, используя p.Contains(key) для поиска совпадения.)

Производительность: Затраченное время линейно пропорционально F, P и W и постоянно для поиска по словарю.
Это улучшение по сравнению с методом грубой силы, который также пропорционален размеру словаря. Таким образом, прирост производительности увеличивается с увеличением размера словаря. Кроме того, метод грубой силы использует string.Contains(), что также отнимает много времени. Подводя итог, решение грубой силы — это O(N), а решение подфразы — O(1).
Я думаю, нет необходимости публиковать какой-либо код, это всего лишь простой двойной цикл, создающий список подфраз.

Другие вопросы по теме