Найти все конкатенации двух строк в огромном наборе - PullRequest
0 голосов
/ 10 сентября 2018

Учитывая набор из 50 тыс. Строк, мне нужно найти все пары (s, t), чтобы s, t и s + t содержались в этом наборе.

Что я пробовал

, есть дополнительное ограничение: s.length() >= 4 && t.length() >= 4. Это позволяет группировать строки по префиксам длиной 4 и, по отдельности, суффиксам. Затем для каждой строки composed длиной не менее 8 я ищу набор кандидатов для s, используя первые четыре символа composed, и набор кандидатов для t, используя последние четыре символа. Это работает, но нужно найти 30M пар-кандидатов (s, t), чтобы найти результаты 7k.

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

Что мне нужно

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

«Отсутствующий» вопрос

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

Ответы [ 4 ]

0 голосов
/ 10 сентября 2018

Вы можете улучшить ответ Эрика , избегая большинства суб-String создания с использованием CharBuffer представлений и изменяя их положение и ограничение:

Set<CharBuffer> strings = Stream.of(
    "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
    "bear", "hug", "bearhug", "cur", "curlique", "curl",
    "down", "downstream", "stream"
 )
.filter(s -> s.length() >= 4) // < 4 is irrelevant
.map(CharBuffer::wrap)
.collect(Collectors.toSet());

strings
    .stream()
    .filter(s -> s.length() >= 8)
    .map(CharBuffer::wrap)
    .flatMap(cb -> IntStream.rangeClosed(4, cb.length() - 4)
        .filter(i -> strings.contains(cb.clear().position(i))&&strings.contains(cb.flip()))
        .mapToObj(i -> cb.clear()+" = "+cb.limit(i)+" + "+cb.clear().position(i))
    )
    .forEach(System.out::println);

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

Разумеется, различия становятся значительными только в том случае, если вы используете терминальную операцию, отличную от печати спичек, так как печать является тихой дорогой операцией. Аналогичным образом, когда источником является поток над большим файлом, ввод-вывод будет доминировать в операции. Если вы не идете в совершенно ином направлении, например, используя отображение памяти и рефакторинг, эта операция работает в течение ByteBuffer s.

0 голосов
/ 10 сентября 2018

Не уверен, что это лучше, чем ваше решение, но я думаю, что стоит попробовать.

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

Идите вперед Trie от глубины 4 внутрь и используйте остаток листа, чтобы определить суффикс (или что-то в этом роде) и найдите его в обратном направлении Trie.

Я уже публиковал Trie реализацию здесь https://stackoverflow.com/a/9320920/823393.

0 голосов
/ 10 сентября 2018

Алгоритм 1: тестовые пары, а не одиночные

Одним из способов может быть, вместо работы со всеми возможными парами для всех возможных составных строк, содержащих эти пары, работать со всеми возможными составными строками и посмотреть, содержат ли они пары. Это меняет проблему с n^2 поисков (где n - количество строк> = 4 символа) на m * n поисков (где m - средняя длина всех строк> = 8 символов, минус 7 и n теперь число строк> = 8 символов). Вот одна из реализаций этого:

int minWordLength = 4;
int minPairLength = 8;

Set<String> strings = Stream
   .of(
      "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
      "bear", "hug", "bearhug", "cur", "curlique", "curl",
      "down", "downstream", "stream"
   )
   .filter(s -> s.length() >= minWordLength)
   .collect(ImmutableSet.toImmutableSet());

strings
   .stream()
   .filter(s -> s.length() >= minPairLength)
   .flatMap(s -> IntStream
      .rangeClosed(minWordLength, s.length() - minWordLength)
      .mapToObj(splitIndex -> ImmutableList.of(
         s.substring(0, splitIndex),
         s.substring(splitIndex)
      ))
      .filter(pair ->
          strings.contains(pair.get(0))
          && strings.contains(pair.get(1))
      )
   )
   .map(pair ->
      pair.get(0) + pair.get(1) + " = " + pair.get(0) + " + " + pair.get(1)
   )
   .forEach(System.out::println);

Дает результат:

downstream = down + stream

Это имеет среднюю алгоритмическую сложность m * n, как показано выше. Так в действительности, O(n). В худшем случае O(n^2). См. хеш-таблицу для получения дополнительной информации об алгоритмической сложности.

Пояснение

  1. Поместите все строки длиной четыре или более символов в набор хэшей (который требует средней сложности O (1) для поиска). Я использовал гуаву ImmutableSet для удобства. Используйте все, что вам нравится.
  2. filter: ограничивается только элементами длиной восемь или более символов, представляющих наших кандидатов в состав двух других слов в списке.
  3. flatMap: Для каждого кандидата вычислите все возможные пары подслов, убедившись, что длина каждого из них не менее 4 символов. Поскольку может быть несколько результатов, это фактически список списков, поэтому сведите его в один глубокий список.
    1. rangeClosed: Генерация всех целых чисел, представляющих количество символов, которое будет в первом слове пары, которую мы будем проверять.
    2. mapToObj: Используйте каждое целое число в сочетании с нашей строкой-кандидатом для вывода списка из двух элементов (в производственном коде вы, вероятно, захотите что-то более понятное, например, класс значений с двумя свойствами или соответствующий существующий класс).
    3. filter: ограничение только парами, в которых есть оба.
  4. map: немного подвести итоги.
  5. forEach: вывод на консоль.

Выбор алгоритма

Этот алгоритм настроен на слова, которые намного короче, чем количество элементов в списке. Если бы список был очень коротким, а слова были очень длинными, то переключение обратно на задачу композиции вместо задачи декомпозиции работало бы лучше. Учитывая, что список имеет размер 50 000 строк, а немецкие слова в то время как длинные очень вряд ли будут превышать 50 символов, это является фактором 1: 1000 в пользу этого алгоритма.

Если бы у вас было 50 строк длиной в среднем 50 000 символов, другой алгоритм был бы гораздо более эффективным.

Алгоритм 2: сортировка и ведение списка кандидатов

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

a
abc
abcdef
bear
bearhug
cur
curl
curlique
def
down ---------\
downs         |
downer        | not far away now!
downregulate  |
downstream ---/
hug
shine
stream
sun
sunshine

Таким образом, если бы сохранялся текущий набор всех проверяемых элементов, мы могли бы найти подходящие композиты за по существу постоянное время для каждого слова, а затем непосредственно исследовать хеш-таблицу для оставшегося слова:

int minWordLength = 4;

Set<String> strings = Stream
   .of(
      "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
      "bear", "hug", "bearhug", "cur", "curlique", "curl",
      "down", "downs", "downer", "downregulate", "downstream", "stream")
   .filter(s -> s.length() >= minWordLength)
   .collect(ImmutableSet.toImmutableSet());

ImmutableList<String> orderedList = strings
   .stream()
   .sorted()
   .collect(ImmutableList.toImmutableList());
List<String> candidates = new ArrayList<>();
List<Map.Entry<String, String>> pairs = new ArrayList<>();

for (String currentString : orderedList) {
   List<String> nextCandidates = new ArrayList<>();
   nextCandidates.add(currentString);
   for (String candidate : candidates) {
      if (currentString.startsWith(candidate)) {
         nextCandidates.add(candidate);
         String remainder = currentString.substring(candidate.length());
         if (remainder.length() >= minWordLength && strings.contains(remainder)) {
            pairs.add(new AbstractMap.SimpleEntry<>(candidate, remainder));
         }
      }
   }
   candidates = nextCandidates;
}
pairs.forEach(System.out::println);

Результат:

down=stream

Алгоритмическая сложность немного сложнее. Я думаю, что средняя часть поиска O(n), с наихудшим O(n^2). Самой дорогой частью может быть сортировка, которая зависит от используемого алгоритма и характеристик несортированных данных. Так что используйте это с зерном соли, но у него есть возможность. Мне кажется, что это будет намного дешевле, чем сборка Trie из огромного набора данных, потому что вы только один раз всесторонне исследуете его и не получите амортизацию стоимости сборки.

Кроме того, на этот раз я выбрал Map.Entry для удержания пары. Это совершенно произвольно, как вы это делаете. Было бы неплохо создать пользовательский класс Pair или использовать какой-нибудь существующий класс Java.

0 голосов
/ 10 сентября 2018

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

...