2009-06-27 2 views
4

У меня есть большое количество наборов чисел. Каждый набор содержит 10 номеров, и мне нужно удалить все наборы, которые имеют 5 или более номеров (неупорядоченных), с любым другим набором.эффективный алгоритм для сравнения подобия между наборами чисел?

Например:

set 1: {12,14,222,998,1,89,43,22,7654,23} 
set 2: {44,23,64,76,987,3,2345,443,431,88} 
set 3: {998,22,7654,345,112,32,89,9842,31,23} 

Учитывая 3 набора 10 чисел выше наборов 1 и наборы 3 будут рассмотрены дубликаты, поскольку они имеют 5 чисел, соответствующих. Итак, в этом случае я бы удалил набор 3 (потому что он считается аналогичным множеству 1).

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

любые идеи? Благодаря!

Майк

+2

Hm, известен ли диапазон чисел? В вашем примере можно предположить, что используются только числа от 1 до 10000 или около того, если это правда, у меня есть идея, как это решить. – Lucero

+0

Мы говорим о эффективности времени, эффективности пространства или обоим? –

+0

Я думаю, что в основном эффективность времени, так как это, кажется, требование здесь. – Lucero

ответ

2

Вы не говорите много о том, что диапазон чисел, которые могут появиться есть, но у меня есть две идеи:

  • инвертированный список, который отображает число, которое появляется в списки в списки, которые его содержат, затем пересечь эти списки, чтобы найти те, которые имеют более одного общего числа.

  • делить числа или группировать их в диапазоны «близких» чисел, а затем уточнять (узкие) списки с номерами в этих диапазонах. Вы уменьшаете диапазоны для совпадающих списков, у вас есть управляемое количество списков, и вы можете точно сравнить списки. Я думаю, это будет подход «близости».

-2

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

+0

По-прежнему сравнивая каждое значение каждого набора друг с другом, он далеко не линейный по времени. Это классический образец, в котором вы продаете пространство против производительности; с большим вычислительным пространством вы можете достичь линейного времени. – Lucero

+0

@ Lucero: Я не предлагал этого! Я просто предложил структуру данных, которая подходит. Было ли это действительно справедливым? : P – Noldorin

+0

Это не помогает в заданном вопросе, не так ли? – Lucero

1

Поскольку вам нужно сравнить все пары множеств, алгоритм о O (N^2), где N размер набора.

Для каждого сравнения вы можете сделать около O (X + Y), где X и Y - размер двух наборов, в вашем случае 10 каждый, поэтому он является постоянным. Но для этого требуется, чтобы вы сортировали все наборы заранее, так что добавляет O (N * xlgx), снова xlgx является постоянным в вашем случае.

Алгоритм линейного сравнения для двух наборов довольно прост, так как сортировки отсортированы в настоящее время, вы можете выполнять итерацию обоих наборов одновременно. Подробнее см. C++ std :: set_intersection.

Весь алгоритм - это O (N^2), что будет довольно медленным для 10000 наборов.

+0

Конечно, вы можете сделать лучше, чем O (N^2). См. Мой ответ. –

5

Еще одна отличная работа для Signature Tree. Еще раз я ошеломлен тем, что там нет библиотеки, которая их реализует. Дайте мне знать, напишите ли вы.

Из реферата первой бумаги в результатах поиска выше:

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

27

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

set 1: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 
set 2: {6, 7, 8, 9, 10, 11, 12, 13, 14, 15} 
set 3: {11, 12, 13, 14, 15, 16, 17, 18, 19, 20} 

Если вы сначала рассмотрим 1 и 2, чтобы быть «дубликатов» и ликвидировать набор 1, затем 2 и 3 также «дубликаты», и вы остались только один оставшийся набор. Но если вы сначала исключаете набор 2, то 1 и 3 не имеют совпадений, и вы остаетесь с остальными двумя наборами.

Вы можете легко развернуть это до полных 10000 наборов, чтобы было возможно, что в зависимости от того, какие наборы вы сначала сравниваете и устраняете, вы можете оставить только один набор или 5000 наборов. Я не думаю, что это то, чего ты хочешь.

Математически говоря, ваша проблема заключается в том, что вы пытаетесь найти equivalence classes, но отношение «подобие», которое вы используете для их определения, не является equivalence relation. В частности, это не транзитивно. В терминах непрофессионала, если множество A «похоже» на множество B, а B является «похожим» на множество C, то ваше определение не гарантирует, что A также «похоже» на C, и поэтому вы не можете значительно устранить аналогичные множества.

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

+1

Ничего себе. Великий математический взгляд! :) – darkrain

+2

Это то, что вы научитесь делать во сне, когда вы изучаете CS или математику в университете, а затем вряд ли когда-либо сможете использовать его на работе. –

+0

Хорошее объяснение.Хотя я кратко упомянул именно этот вопрос за короткое время двумя часами ранее и получил вниз. – Lucero

-1

Предположим, у вас есть класс NumberSet, который реализует ваш неупорядоченный набор (и может перечислить int s, чтобы получить номера). Затем понадобятся следующие структуры данных и алгоритм:

  • Map<int, Set<NumberSet>> numberSets
  • Map<Pair<NumberSet, NumberSet>, int> matchCount
  • Pair<X,Y> является ключевым объектом, который возвращает тот же хэш-код и равенство для каждого экземпляра с тем же X и Y (не имеет значения, если они поменяны местами)

Теперь для каждого набора, которые будут добавлены/по сравнению сделать следующее (псевдокод !!!):

for (int number: setToAdd) { 
    Set<NumberSet> numbers = numberSets.get(number); 
    if (numbers == null) { 
     numbers = new HashSet<NumberSet>(); 
     numberSets.put(number, numbers); 
    } else { 
     for (NumberSet numberSet: numbers) { 
     Pair<NumberSet, NumberSet> pairKey = new Pair<NumberSet, NumberSet>(numberSet, setToAdd); 
     matchCount.put(pairKey, matchCount.get(pairKey)+1); // make sure to handle null as 0 here in real code ;) 
     } 
    } 
    numbers.add(number); 
} 

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

Примечание: удаление наборов может быть плохой идеей, потому что если А считается дубликатом B и B дубликатом С, поэтому C не должно быть дубликатом А. Так что, если вы удалите B, вы не удалите C, и порядок, в котором вы добавите свои наборы, станет важным.

+0

Теперь это конструктивное, downvoting без каких-либо комментариев. – Lucero

1

Вы должны найти Коэффициент Пирсона между двумя наборами данных. Этот метод сделает вашу программу легко масштабируемой для огромных наборов данных.

2

Я не думаю, что есть красивый и красивый способ сделать это. В большинстве других ответов вы проведете сравнение между большинством пар x,y, который будет O(N^2). Вы можете сделать это быстрее.

Алгоритм: сохранить массив всех 5-ти кортежей. Для каждого нового разбиения на все возможные 5-кортежи добавьте к этому массиву. В конце сортируйте и проверьте наличие дубликатов.

Есть C(10, 5) = 10*9*8*7*6/120 = 9*4*7, примерно 250 подмножеств длины 5 из набора 10. Таким образом, вы держите таблицу, которая составляет 10^3 раз больше ваших данных, но выполняет только O(250*N) операций. Это должно работать практически, и я подозреваю, что это лучше всего теоретически.

1

Существует способ сделать это с высокой эффективностью времени, но чрезвычайно низкой эффективностью пространства.

Если моя математика верна, каждая комбинация из 5 чисел из набора из 10 результатов в 10! (10-5)! 5! = 252 комбинаций, умноженных на 10000 наборов = 2,52 миллиона комбинаций. Набор из 5 целых чисел будет потреблять 20 байт, поэтому вы можете поставить каждую комбинацию для каждого набора в HashSet. и использовать только 5 мегабайт (плюс накладные расходы, которые выдувают его минимум в 2-3 раза).

Теперь это может показаться дорогостоящим, но если альтернатива, когда вы проверяете новый набор из 10 против существующего 10000 номеров, это то, что вы вычисляете 252 набора из 5 и видите, есть ли какой-либо из них в наборе, тогда он должен будь лучше.

В основном:

public class SetOf5 { 
    private final static HashSet<Integer> numbers; 
    private final int hashCode; 

    public SetOf5(int... numbers) { 
    if (numbers.length != 5) { 
     throw new IllegalArgumentException(); 
    } 
    Set<Integer> set = new HashSet<Integer>(); 
    hashCode = 19; 
    for (int i : numbers) { 
     set.add(i); 
     hashCode = 31 * i + hashCode; 
    } 
    this.numbers = Collections.unmodifiableSet(set); 
    } 

    // other constructors for passing in, say, an array of 5 or a Collectio of 5 

    // this is precalculated because it will be called a lot 
    public int hashCode() { 
    return numbers.hashCode(); 
    } 

    public boolean equals(Object ob) { 
    if (!(ob instanceof SetOf5)) return false; 
    SetOf5 setOf5 = (SetOf5)ob; 
    return numbers.containsAll(setOf5.numbers); 
    } 
} 

Вы тогда просто должны сделать две вещи:

  1. Создать HashSet<SetOf5> для всех существующих кортежах 5; и
  2. Создать алгоритм для создания всех возможных наборов 5.

Ваш алгоритм становится: для каждого набора из 10 чисел, создать все возможные наборы из 5, проверьте каждый из них, чтобы увидеть, если это в задавать. Если это так, отклоните набор из 10. Если это не так, добавьте набор из 5 в «набор множеств». В противном случае продолжайте.

Я думаю, вы обнаружите, что это будет ужасно много дешевле - по крайней мере, в случае 5 чисел из 10 - чем сравнение грубой силы 10000 наборов друг с другом.

0

Возможно, вам нужен такой алгоритм (как я понимаю вашу проблему)?

import java.util.Arrays; 
import java.util.HashSet; 
import java.util.LinkedList; 
import java.util.List; 
import java.util.Set; 

/** 
* @author karnokd, 2009.06.28. 
* @version $Revision 1.0$ 
*/ 
public class NoOverlappingSets { 
    // because of the shortcomings of java type inference, O(N) 
    public static Set<Integer> setOf(Integer... values) { 
     return new HashSet<Integer>(Arrays.asList(values)); 
    } 
    // the test function, O(N) 
    public static boolean isNumberOfDuplicatesAboveLimit(
      Set<Integer> first, Set<Integer> second, int limit) { 
     int result = 0; 
     for (Integer i : first) { 
      if (second.contains(i)) { 
       result++; 
       if (result >= limit) { 
        return true; 
       } 
      } 
     } 
     return false; 
    } 
    /** 
    * @param args 
    */ 
    public static void main(String[] args) { 
     // TODO Auto-generated method stub 
     List<Set<Integer>> sets = new LinkedList<Set<Integer>>() {{ 
      add(setOf(12,14,222,998,1,89,43,22,7654,23)); 
      add(setOf(44,23,64,76,987,3,2345,443,431,88)); 
      add(setOf(998,22,7654,345,112,32,89,9842,31,23)); 
     }}; 
     List<Set<Integer>> resultset = new LinkedList<Set<Integer>>(); 
     loop: 
     for (Set<Integer> curr : sets) { 
      for (Set<Integer> existing : resultset) { 
       if (isNumberOfDuplicatesAboveLimit(curr, existing, 5)) { 
        continue loop; 
       } 
      } 
      // no overlapping with the previous instances 
      resultset.add(curr); 
     } 
     System.out.println(resultset); 
    } 

} 

Я не эксперт в области Big O нотации, но я думаю, что этот алгоритм O (N * M^2), где N это количество элементов в наборе и M является общим количеством наборов (основанный на числе циклов, которые я использовал в алгоритме). Я позволил себе определить, что я считаю перекрывающимися наборами.

Я думаю, что ваша проблема полиномиальна. Поскольку я помню свои лекции, решение основано на версии NP-hard - но исправьте меня, если я ошибаюсь.

+0

И с точки зрения эффективности памяти, я думаю, что алгоритм потребляет около M * k байт ОЗУ, где k находится где-то между 8 и 20. [edit] – akarnokd

+0

Замечание. Я хочу сохранить исходный порядок числа в наборах, заменить HashSet на LinkedHashSet. – akarnokd

0

Это простая проблема, потому что ваши наборы ограничены размером десять. Для каждого набора из десяти чисел у вас есть менее 1000 подмножеств набора, которые содержат не менее пяти чисел. Выберите хеш-функцию, которая хэширует целые последовательности, например, в 32-битных числах. Для каждого набора из десяти целых чисел вычислите значение этой хэш-функции для каждого подмножества целых чисел с пятью или более элементами. Это дает менее 1000 значений хэша на один набор из десяти чисел. Добавьте указатель на набор из десяти целых чисел в хеш-таблицу под все этих 1000 ключей.Как только вы это сделаете, ваша хеш-таблица имеет 1000 * 10 000 = 10 миллионов записей, что вполне выполнимо; и этот первый проход является линейным (O (n)), потому что индивидуальный размер набора ограничен на 10.

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

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

static int hash_index; 

void calculate_hash(int *myset, unsigned int *hash_values) 
{ 
    hash_index = 0; 
    hrec(myset, hash_values, 0, 0, 0); 
} 

void hrec(int *myset, unsigned int *hash_values, 
      unsigned int h, int idx, int card) 
{ 
    if (idx == 10) { 
    if (card >= 5) { 
     hash_values[hash_index++] = h; 
    } 
    return; 
    } 
    unsigned int hp = h; 
    hp += (myset[idx]) + 0xf0f0f0f0; 
    hp += (hp << 13) | (hp >> 19); 
    hp *= 0x7777; 
    hp += (hp << 13) | (hp >> 19); 
    hrec(myset, hash_values, hp, idx + 1, card + 1); 
    hrec(myset, hash_values, h, idx + 1, card); 
} 

Это рекурсивно через все 1024 подмножеств и хранит хэш-значения для подмножеств с мощностью 5 или более в hash_values массиве. В конце hash_index подсчитывает количество действительных записей. Это, конечно, постоянный, но я не вычислил его здесь численно.

0

Мы возьмем набор данных, украсим каждый элемент подписью, и сортируем его. Подпись имеет свойство, что сортировка будет группировать эти элементы вместе, которые могут имеют дубликаты. Когда сравнивает data_set [j] с элементами в data_set [j + 1 ...], когда первая подпись в [j + 1 ...] дублирует проверку, мы продвигаем i. Этот «критерий смежности» гарантирует, что нам не нужно смотреть дальше; Ни один элемент за пределами этого не может быть дубликатом.

Это значительно уменьшает сравнение O (N^2). Сколько я дам , аналитик алгоритма решает, но приведенный ниже код делает ~ 400k сравнения вместо 100 м наивного O (N^2).

Подпись начинается с разбивки элементов. Мы разделим диапазон чисел на N конусов равного размера: 1..k, k + 1..2k, 2k + 1..3k, ... При повторении по элементам мы увеличиваем счетчик, если число попадает в конкретное ведро. Это дает начальную подпись формы (0,0,0,1,3,0,0, ... 4,2).

Сигнатура обладает тем свойством, что если

sum(min(sig_a[i], sig_b[i]) for i in range(10)) >= 5 

то есть возможные элементы, связанные с подписями имеют по крайней мере 5duplicates. Но больше, если вышесказанное делает не hold, , тогда элементы не могут иметь 5 дубликатов. Позволяет называть это «критерием соответствия подписи».

Но, сортировка по вышеуказанной подписи делает не имеет свойство смежности , упомянутое выше. Однако, если мы изменяем подпись, чтобы иметь форму два элемента:

(sum(sig[:-1]), sig[-1]) 

тогда «подпись критерий соответствия» держит. Но выполняется ли критерий смежности ? Да. Сумма этой подписи составляет 10.Если мы перечисления, мы имеем следующие возможные подписи:

(0,10) (1, 9) (2, 8) (3, 7) (4, 6) (5, 5) (6, 4) (7, 3) (8, 2) (9, 1) (10,0) 

Если мы сравним (0,10) против (1,9) .. (10,0), мы отмечаем, что раз тест подписи он больше не повторится. Критерий смежности имеет место. Кроме того, этот критерий смежности выполняется для всех положительных значений, а не только для «5».

Хорошо, но разбивка подписи на два больших ведра не обязательно приведет к уменьшению поиска O (N^2); подпись является чрезмерно общей. Мы решаем, что проблему путем создания подписи для сиг [: - 1], продуцирующие

(sum(sig[:-1]), sig[-1]), (sum(sig[:-2]), sig[-2]), ... 

и так далее. Я считаю, что эта подпись все еще удовлетворяет смежности, но Я мог ошибаться.

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

# python 3.0 
import random 

# M number of elements, N size of each element 
M = 10000 
N = 10 

# Bounds on the size of an element of each set 
Vmin,Vmax = 0, (1 << 12) 

# DupCount is number of identical numbers required for a duplicate 
DupCount = 5 

# R random number generator, same sequence each time through 
R = random.Random() 
R.seed(42) 

# Create a data set of roughly the correct size 
data_set = [list(s) for s in (set(R.randint(Vmin, Vmax) for n in range(N)) for m in range(M)) if len(s) == N] 

# Adorn the data_set signatures and sort 
def signature(element, width, n): 
"Return a signature for the element" 
    def pearl(l, s): 
     def accrete(l, s, last, out): 
      if last == 0: 
       return out 
      r = l[last] 
      return accrete(l, s-r, last-1, out+[(s-r,r)]) 
     return accrete(l, s, len(l)-1, []) 
    l = (n+1) * [0] 
    for i in element: 
     l[i // width] += 1 
    return pearl(l, len(element)) 

# O(n lg(n)) - with only 10k elements, lg(n) is a little over 13 
adorned_data_set = sorted([signature(element, (Vmax-Vmin+1)//12, 12), element] for element in data_set) 

# Count the number of possible intersections 
def compare_signatures(sig_a, sig_b, n=DupCount): 
    "Return true if the signatures are compatible" 
    for ((head_a, tail_a), (head_b, tail_b)) in zip(sig_a, sig_b): 
     n -= min(tail_a, tail_b) 
     if n <= 0: 
      return True 
    return False 

k = n = 0 
for i, (sig_a, element_a) in enumerate(adorned_data_set): 
    if not element_a: 
     continue 
    for j in range(i+1, len(adorned_data_set)): 
     sig_b, element_b = adorned_data_set[j] 
     if not element_b: 
      continue 
     k += 1 
     if compare_signatures(sig_a, sig_b): 
      # here element_a and element_b would be compared for equality 
      # and the duplicate removed by adorned_data_set[j][1] = [] 
      n += 1 
     else: 
      break 

print("maximum of %d out of %d comparisons required" % (n,k)) 
+0

Неверный критерий смежности не выполняется. Если вы сбросите «перерыв» после сопоставлений compare_signatures, вы увидите больше сравнений. Однако разделы, созданные для подписей, можно рассматривать как точки в k-мерном пространстве. Запросы ближайшего соседа _might_ работают. – themis

 Смежные вопросы

  • Нет связанных вопросов^_^