Быстрое пересечение двух наборов N^4

Я ищу алгоритм, который возвращает (длину) пересечение двух заданных списков/наборов N^4 (точнее, в [|0;7|]^4). Я не ожидаю ответа на конкретном языке программирования (я делаю это на OCaml, но поскольку этот язык не очень популярен, я не ожидаю, что ответы будут написаны на Caml...). Обратите внимание, что решения, требующие больших предварительных вычислений (например, полное изменение структуры множеств), в моем случае не являются проблемой.

Я пытался рассматривать множества N^4 как классические целочисленные множества, чтобы затем пересекать их с помощью встроенных пересечений множеств (но это было слишком медленно для моей цели), а также я пытался рассматривать их как множества N^2, чтобы применить быстрое Лебега N^2 пересекаются (это могло бы сработать, но оказывается, что точки не переставляются случайным образом по плоскости, что делает этот алгоритм весьма неэффективным).

Вы уверены, что не хотите спрашивать на cs.stackexchange.com?

Chris 08.04.2024 21:23

Можно ли бросить их в деревья KD и затем таким образом пересечь их? Я не совсем уверен, что такое список N^4, но если это прямоугольник, то его будет легко использовать.

btilly 08.04.2024 23:59

@btilly — это математическое обозначение четырехкортежей натуральных чисел. N обычно пишется следующим шрифтом: ℕ

Dillon Davis 09.04.2024 00:45
Стоит ли изучать 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
3
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Непонятно, к какой цели производительности вы стремитесь. Обычное пересечение множеств, содержащее не более 4096 элементов, должно быть не таким медленным, по крайней мере, для пересечения общих множеств. Обычно наихудший сценарий (пересечение полного набора сам с собой) занимает около 70 мкс при стандартном наборе OCaml на моем компьютере, тогда как сценарий наилучшего случая (пересечение двух пустых наборов) занимает 6 нс. В частности, вычисление только длины в O(n) и отказ от построения набора результатов, похоже, не улучшают производительность в худшем случае.

Таким образом, мы рассматриваем оптимизацию констант путем подгонки алгоритма к набору данных. Поскольку ваши наборы содержат не более 4096 элементов, их можно представить в виде строк длиной 4096/8 = 512. При таком представлении пересечение двух наборов сводится к взятию логического and двух строк. Мы можем оптимизировать дальше, вычисляя длину на лету:

 let inter x y =
    let r = ref 0 in
    for i = 0 to set_size - 1 do
      r := !r + popcount (Char.code x.[i] land Char.code y.[i])
    done;
    !r

Этого достаточно, чтобы уменьшить время вычислений до 200 нс с помощью реализации popcount в OCaml (которая подсчитывает количество 1 бита в символе).

Можно пойти дальше, но на этом этапе нам нужно либо использовать большую структуру набора данных, либо нам нужно адаптировать алгоритм к аппаратному обеспечению, перейдя к C, чтобы использовать аппаратную popcount инструкцию и векторные SIMD-инструкции для вычисления логический и на большей партии байтов.

Спасибо за ваш ответ ! Я не думал о преобразовании строк, и это кажется более эффективным, чем пересечение целых наборов длиной 4096. Кстати, я прошу прощения за отсутствие ясности в моем посте (мне следовало четко изложить свое дело, чтобы люди могли подумать о более конкретных решениях)...

SerialCookie 09.04.2024 09:50

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

number = tuple[0] | tuple[1] << 3 | tuple[2] << 6 | tuple[3] << 9

Что касается пересечения сетов, то в большинстве случаев вам не удастся побить время O(n) в меньшем из двух сетов. Если вы знаете что-нибудь о распределении наборов или о том, насколько уникальными они должны быть, возможно, мы сможем придумать какую-нибудь эвристику, чтобы ускорить пересечение на практике.


Обновлять

Вот пример того, как можно ускорить вычисления, зная распределение. Допустим, мы знаем, что подавляющее большинство элементов встречаются между наборами, которые вы собираетесь предварительно обработать, очень редко. Что вы могли бы сделать, так это разбить наборы на две отдельные структуры данных: одна, которая сопоставляет пару наборов с общими для них необычными элементами, а вторая содержит оставшиеся (общие) элементы для каждого набора. Первый вариант будет простым O(1) поиском и может быть ограничен небольшим управляемым размером. Второй потребует O(n) наивного пересечения, как и раньше, но наборы будут значительно меньше, поскольку мы сказали, что подавляющее большинство элементов являются необычными.

Вот реализация на Python:

from itertools import combinations
from collections import Counter, defaultdict
from random import sample

N = 10 ** 7
K = 10 ** 5
S = 100

input_sets = [set(sample(range(N), k=K)) for _ in range(S)]

# get frequency of each element
counts = Counter()
for input_set in input_sets:
    counts.update(input_set)

# mark all elements occurring in less than 5 sets as uncommon
uncommon = {val for val, count in counts.items() if count < 5}

# break each input_set into common / uncommon parts
common_sets   = [input_set - uncommon for input_set in input_sets]
uncommon_sets = [input_set & uncommon for input_set in input_sets]

# create reverse lookup table from values to sets containing them
uncommon_lookup = defaultdict(list)
for idx, uncommon_set in enumerate(uncommon_sets):
    for val in uncommon_set:
        uncommon_lookup[val].append(idx)

# create mapping of input set pairs to shared uncommon elements
uncommon_matrix = defaultdict(set)
for val, indices in uncommon_lookup.items():
    for i, j in combinations(indices, 2): 
        uncommon_matrix[i, j].add(val)

При выполнении теста производительности мы видим, что он работает примерно в 30 раз быстрее.

from timeit import timeit

test_pairs = [sample(range(S), k=2) for _ in range(100)]

def naive_benchmark():
    for i, j in test_pairs:
        input_sets[i] & input_sets[j]

def fast_benchmark():
    for i, j in test_pairs:
        (common_sets[i] & common_sets[j]) | uncommon_matrix[i, j]

print(timeit("naive_benchmark()", setup = "from __main__ import naive_benchmark", number=10) / 10)
print(timeit("fast_benchmark()", setup = "from __main__ import fast_benchmark", number=10) / 10)

Выход:

0.3008148681998136
0.009065380399988499

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

Спасибо за ваш ответ ! На самом деле разделение необычных/общих элементов могло бы быть многообещающим решением, но в моем случае все элементы распределены однородно... Тем не менее, я обязательно попробую!

SerialCookie 09.04.2024 09:44

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

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

Найдите максимально допустимое количество негативов
Эффективно подсчитать количество равносторонних и равнобедренных треугольников, которые можно построить из набора точек
Транспортная задача и метод ветвей и границ
Логика, которую нужно реализовать, чтобы получить максимальное количество очков
Что происходит с потерянной частью односвязного списка, когда я добавляю цикл в середине?
Эффективный способ найти сумму наибольших элементов x в подмассиве
Инициализируйте двумерный массив значениями, указанными в таблице ниже
Как я могу придумать алгоритм для разделения ряда чисел в заданном соотношении, чтобы округленные разделения складывались в исходное число?
Рекурсивное построение древовидной структуры данных из массива строк, сохраняющих порядок приоритета
Алгоритм разделения целого числа на группы определенных меньших целых чисел