эффективный алгоритм для сравнения сходства между наборами чисел? - PullRequest
4 голосов
/ 28 июня 2009

У меня есть большое количество наборов чисел. Каждый набор содержит 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 наборов для сравнения, и я хочу сделать это очень эффективно. Я переворачиваю это, и я просто не могу придумать эффективный способ выполнить это сравнение (было бы здорово сделать это за один проход).

есть идеи? Спасибо!

Mike

Ответы [ 12 ]

28 голосов
/ 28 июня 2009

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

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 наборов. Я не думаю, что это то, что вы хотите.

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

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

6 голосов
/ 28 июня 2009

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

Из аннотации первой статьи в результатах поиска выше:

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

2 голосов
/ 28 июня 2009

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

Если моя математика верна, каждая комбинация из 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 наборов друг с другом.

2 голосов
/ 28 июня 2009

Я не думаю, что есть хороший и красивый способ сделать это. В большинстве других ответов вы сделаете сравнение между большинством пар 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) операций. Это должно сработать практически, и я подозреваю, что теоретически это тоже самое лучшее.

2 голосов
/ 28 июня 2009

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

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

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

1 голос
/ 29 июня 2009

это простая проблема, потому что ваш набор ограничен размером десять. Для каждого набора из десяти чисел у вас есть менее 1000 подмножеств набора, которые содержат как минимум пять чисел. Выберите хеш-функцию, которая хэширует целочисленные последовательности, скажем, в 32-битные числа. Для каждого набора из десяти целых чисел вычислите значение этой хеш-функции для каждого подмножества целых чисел с пятью или более элементами. Это дает менее 1000 значений хеша на один набор из десяти чисел. Добавьте указатель на набор из десяти целых чисел в хеш-таблицу в all этих 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 подсчитывает количество допустимых записей. Это, конечно, константа, но я не подсчитал это здесь численно.

1 голос
/ 28 июня 2009

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

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-трудной, но поправьте меня, если я ошибаюсь.

1 голос
/ 28 июня 2009

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

1 голос
/ 28 июня 2009

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

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

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

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

0 голосов
/ 01 июля 2009

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

Это значительно уменьшает сравнение O (N ^ 2). Сколько я позволю Аналитик алгоритма решает, но код, приведенный ниже, сравнивает ~ 400 Кб вместо 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

тогда возможно элементы, связанные с подписями иметь как минимум 5 дубликатов. Но более того, если вышеупомянутое не выполняется, тогда элементы не могут иметь 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); подпись слишком генеральный. Мы решаем эту проблему , создавая подпись для sig [: - 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))
...