Python: простое объединение списков на основе пересечений

Учтите, что есть некоторые списки целых чисел как:

#--------------------------------------
0 [0,1,3]
1 [1,0,3,4,5,10,...]
2 [2,8]
3 [3,1,0,...]
...
n []
#--------------------------------------

Вопрос состоит в том, чтобы объединить списки, имеющие хотя бы один общий элемент.Таким образом, результаты только для данной части будут следующими:

#--------------------------------------
0 [0,1,3,4,5,10,...]
2 [2,8]
#--------------------------------------

Каков наиболее эффективный способ сделать это для больших данных (элементы - это просто числа)? Стоит ли думать о структуре дерева ? Сейчас я делаю свою работу, конвертируя списки в наборы и выполняя итерацию для пересечений, но это медленно! К тому же у меня такое элементарное ощущение! Кроме того, в реализации чего-то не хватает (неизвестного), потому что некоторые списки иногда остаются не объединенными! Сказав это, если вы предлагали самореализацию, пожалуйста, проявите щедрость и предоставьте простой пример кода [очевидно Python - мой любимчик :)] или псевдокод.
Обновление 1: Вот код, который я использовал:

#--------------------------------------
lsts = [[0,1,3],
        [1,0,3,4,5,10,11],
        [2,8],
        [3,1,0,16]];
#--------------------------------------

Функция ( глючит !! ):

#--------------------------------------
def merge(lsts):
    sts = [set(l) for l in lsts]
    i = 0
    while i < len(sts):
        j = i+1
        while j < len(sts):
            if len(sts[i].intersection(sts[j])) > 0:
                sts[i] = sts[i].union(sts[j])
                sts.pop(j)
            else: j += 1                        #---corrected
        i += 1
    lst = [list(s) for s in sts]
    return lst
#--------------------------------------

Результат:

#--------------------------------------
>>> merge(lsts)
>>> [0, 1, 3, 4, 5, 10, 11, 16], [8, 2]]
#--------------------------------------

Обновление 2: По моему опыту, приведенный ниже код Никлас Баумстарк оказался немного быстрее для простых случаев. Еще не тестировал метод, предлагаемый "Hooked", так как это совершенно другой подход (кстати, кажется интересным). Процедура тестирования для всего этого может быть очень сложной или невозможной, чтобы гарантировать результаты. Реальный набор данных, который я буду использовать, настолько велик и сложен, что невозможно отследить ошибку, просто повторив ее. То есть мне нужно быть на 100% удовлетворенным надежностью метода, прежде чем вставлять его на место в большом коде в качестве модуля. Просто пока метод Никласа работает быстрее, и ответ для простых наборов, конечно, правильный.
Однако как я могу быть уверен, что он хорошо работает для действительно большого набора данных? Поскольку я не смогу отслеживать ошибки визуально!

Обновление 3: Учтите, что для этой задачи надежность метода гораздо важнее скорости. Надеюсь, я наконец смогу перевести код Python на Фортран для максимальной производительности.

Обновление 4:
В этом посте много интересных моментов, щедрых ответов и конструктивных комментариев. Я бы рекомендовал прочитать все внимательно. Примите мою признательность за разработку вопроса, прекрасные ответы, конструктивные комментарии и обсуждение.

39
задан Developer 22 February 2012 в 05:48
поделиться

6 ответов

Моя попытка:

def merge(lsts):
    sets = [set(lst) for lst in lsts if lst]
    merged = True
    while merged:
        merged = False
        results = []
        while sets:
            common, rest = sets[0], sets[1:]
            sets = []
            for x in rest:
                if x.isdisjoint(common):
                    sets.append(x)
                else:
                    merged = True
                    common |= x
            results.append(common)
        sets = results
    return sets

lst = [[65, 17, 5, 30, 79, 56, 48, 62],
       [6, 97, 32, 93, 55, 14, 70, 32],
       [75, 37, 83, 34, 9, 19, 14, 64],
       [43, 71],
       [],
       [89, 49, 1, 30, 28, 3, 63],
       [35, 21, 68, 94, 57, 94, 9, 3],
       [16],
       [29, 9, 97, 43],
       [17, 63, 24]]
print merge(lst)

Тест:

import random

# adapt parameters to your own usage scenario
class_count = 50
class_size = 1000
list_count_per_class = 100
large_list_sizes = list(range(100, 1000))
small_list_sizes = list(range(0, 100))
large_list_probability = 0.5

if False:  # change to true to generate the test data file (takes a while)
    with open("/tmp/test.txt", "w") as f:
        lists = []
        classes = [
            range(class_size * i, class_size * (i + 1)) for i in range(class_count)
        ]
        for c in classes:
            # distribute each class across ~300 lists
            for i in xrange(list_count_per_class):
                lst = []
                if random.random() < large_list_probability:
                    size = random.choice(large_list_sizes)
                else:
                    size = random.choice(small_list_sizes)
                nums = set(c)
                for j in xrange(size):
                    x = random.choice(list(nums))
                    lst.append(x)
                    nums.remove(x)
                random.shuffle(lst)
                lists.append(lst)
        random.shuffle(lists)
        for lst in lists:
            f.write(" ".join(str(x) for x in lst) + "\n")

setup = """
# Niklas'
def merge_niklas(lsts):
    sets = [set(lst) for lst in lsts if lst]
    merged = 1
    while merged:
        merged = 0
        results = []
        while sets:
            common, rest = sets[0], sets[1:]
            sets = []
            for x in rest:
                if x.isdisjoint(common):
                    sets.append(x)
                else:
                    merged = 1
                    common |= x
            results.append(common)
        sets = results
    return sets

# Rik's
def merge_rik(data):
    sets = (set(e) for e in data if e)
    results = [next(sets)]
    for e_set in sets:
        to_update = []
        for i, res in enumerate(results):
            if not e_set.isdisjoint(res):
                to_update.insert(0, i)

        if not to_update:
            results.append(e_set)
        else:
            last = results[to_update.pop(-1)]
            for i in to_update:
                last |= results[i]
                del results[i]
            last |= e_set
    return results

# katrielalex's
def pairs(lst):
    i = iter(lst)
    first = prev = item = i.next()
    for item in i:
        yield prev, item
        prev = item
    yield item, first

import networkx

def merge_katrielalex(lsts):
    g = networkx.Graph()
    for lst in lsts:
        for edge in pairs(lst):
            g.add_edge(*edge)
    return networkx.connected_components(g)

# agf's (optimized)
from collections import deque

def merge_agf_optimized(lists):
    sets = deque(set(lst) for lst in lists if lst)
    results = []
    disjoint = 0
    current = sets.pop()
    while True:
        merged = False
        newsets = deque()
        for _ in xrange(disjoint, len(sets)):
            this = sets.pop()
            if not current.isdisjoint(this):
                current.update(this)
                merged = True
                disjoint = 0
            else:
                newsets.append(this)
                disjoint += 1
        if sets:
            newsets.extendleft(sets)
        if not merged:
            results.append(current)
            try:
                current = newsets.pop()
            except IndexError:
                break
            disjoint = 0
        sets = newsets
    return results

# agf's (simple)
def merge_agf_simple(lists):
    newsets, sets = [set(lst) for lst in lists if lst], []
    while len(sets) != len(newsets):
        sets, newsets = newsets, []
        for aset in sets:
            for eachset in newsets:
                if not aset.isdisjoint(eachset):
                    eachset.update(aset)
                    break
            else:
                newsets.append(aset)
    return newsets

# alexis'
def merge_alexis(data):
    bins = range(len(data))  # Initialize each bin[n] == n
    nums = dict()

    data = [set(m) for m in data]  # Convert to sets
    for r, row in enumerate(data):
        for num in row:
            if num not in nums:
                # New number: tag it with a pointer to this row's bin
                nums[num] = r
                continue
            else:
                dest = locatebin(bins, nums[num])
                if dest == r:
                    continue  # already in the same bin

                if dest > r:
                    dest, r = r, dest  # always merge into the smallest bin

                data[dest].update(data[r])
                data[r] = None
                # Update our indices to reflect the move
                bins[r] = dest
                r = dest

    # Filter out the empty bins
    have = [m for m in data if m]
    return have

def locatebin(bins, n):
    while bins[n] != n:
        n = bins[n]
    return n

lsts = []
size = 0
num = 0
max = 0
for line in open("/tmp/test.txt", "r"):
    lst = [int(x) for x in line.split()]
    size += len(lst)
    if len(lst) > max:
        max = len(lst)
    num += 1
    lsts.append(lst)
"""

setup += """
print "%i lists, {class_count} equally distributed classes, average size %i, max size %i" % (num, size/num, max)
""".format(class_count=class_count)

import timeit
print "niklas"
print timeit.timeit("merge_niklas(lsts)", setup=setup, number=3)
print "rik"
print timeit.timeit("merge_rik(lsts)", setup=setup, number=3)
print "katrielalex"
print timeit.timeit("merge_katrielalex(lsts)", setup=setup, number=3)
print "agf (1)"
print timeit.timeit("merge_agf_optimized(lsts)", setup=setup, number=3)
print "agf (2)"
print timeit.timeit("merge_agf_simple(lsts)", setup=setup, number=3)
print "alexis"
print timeit.timeit("merge_alexis(lsts)", setup=setup, number=3)

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

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

=====================
# many disjoint classes, large lists
class_count = 50
class_size = 1000
list_count_per_class = 100
large_list_sizes = list(range(100, 1000))
small_list_sizes = list(range(0, 100))
large_list_probability = 0.5
=====================

niklas
5000 lists, 50 equally distributed classes, average size 298, max size 999
4.80084705353
rik
5000 lists, 50 equally distributed classes, average size 298, max size 999
9.49251699448
katrielalex
5000 lists, 50 equally distributed classes, average size 298, max size 999
21.5317108631
agf (1)
5000 lists, 50 equally distributed classes, average size 298, max size 999
8.61671280861
agf (2)
5000 lists, 50 equally distributed classes, average size 298, max size 999
5.18117713928
=> alexis
=> 5000 lists, 50 equally distributed classes, average size 298, max size 999
=> 3.73504281044

===================
# less number of classes, large lists
class_count = 15
class_size = 1000
list_count_per_class = 300
large_list_sizes = list(range(100, 1000))
small_list_sizes = list(range(0, 100))
large_list_probability = 0.5
===================

niklas
4500 lists, 15 equally distributed classes, average size 296, max size 999
1.79993700981
rik
4500 lists, 15 equally distributed classes, average size 296, max size 999
2.58237695694
katrielalex
4500 lists, 15 equally distributed classes, average size 296, max size 999
19.5465381145
agf (1)
4500 lists, 15 equally distributed classes, average size 296, max size 999
2.75445604324
=> agf (2)
=> 4500 lists, 15 equally distributed classes, average size 296, max size 999
=> 1.77850699425
alexis
4500 lists, 15 equally distributed classes, average size 296, max size 999
3.23530197144

===================
# less number of classes, smaller lists
class_count = 15
class_size = 1000
list_count_per_class = 300
large_list_sizes = list(range(100, 1000))
small_list_sizes = list(range(0, 100))
large_list_probability = 0.1
===================

niklas
4500 lists, 15 equally distributed classes, average size 95, max size 997
0.773697137833
rik
4500 lists, 15 equally distributed classes, average size 95, max size 997
1.0523750782
katrielalex
4500 lists, 15 equally distributed classes, average size 95, max size 997
6.04466891289
agf (1)
4500 lists, 15 equally distributed classes, average size 95, max size 997
1.20285701752
=> agf (2)
=> 4500 lists, 15 equally distributed classes, average size 95, max size 997
=> 0.714507102966
alexis
4500 lists, 15 equally distributed classes, average size 95, max size 997
1.1286110878
21
ответ дан 27 November 2019 в 02:43
поделиться

Это медленнее, чем решение, предлагаемое Никласом (я получил 3,9 с на test.txt вместо 0,5 с для его решения), но дает тот же результат и может быть легче реализовать, например, в. Fortran, поскольку он не использует наборы, только сортировку общего количества элементов, а затем один проход по всем из них.

Возвращает список с идентификаторами объединенных списков, поэтому также отслеживает пустые списки, они остаются не объединенными.

def merge(lsts):
        # this is an index list that stores the joined id for each list
        joined = range(len(lsts))
        # create an ordered list with indices
        indexed_list = sorted((el,index) for index,lst in enumerate(lsts) for el in lst)
        # loop throught the ordered list, and if two elements are the same and
        # the lists are not yet joined, alter the list with joined id
        el_0,idx_0 = None,None
        for el,idx in indexed_list:
                if el == el_0 and joined[idx] != joined[idx_0]:
                        old = joined[idx]
                        rep = joined[idx_0]
                        joined = [rep if id == old else id for id in joined]
                el_0, idx_0 = el, idx
        return joined
1
ответ дан 27 November 2019 в 02:43
поделиться

Вот реализация, использующая структуру данных с непересекающимся множеством (в частности, непересекающийся лес), благодаря подсказке прибывающей бури в объединяющих множествах, которые имеют даже один общий элемент . Я использую сжатие пути для небольшого (~ 5%) улучшения скорости; это не совсем необходимо (и это предотвращает рекурсивную работу find, что может замедлить процесс). Обратите внимание, что я использую dict для представления непересекающегося леса; учитывая, что данные int с, массив также будет работать, хотя это может быть не намного быстрее.

def merge(data):
    parents = {}
    def find(i):
        j = parents.get(i, i)
        if j == i:
            return i
        k = find(j)
        if k != j:
            parents[i] = k
        return k
    for l in filter(None, data):
        parents.update(dict.fromkeys(map(find, l), find(l[0])))
    merged = {}
    for k, v in parents.items():
        merged.setdefault(find(v), []).append(k)
    return merged.values()

Этот подход сопоставим с другими лучшими алгоритмами в тестах Рика.

2
ответ дан 27 November 2019 в 02:43
поделиться

Вот мой ответ. Я не проверял это в сравнении с сегодняшней партией ответов.

Алгоритмы, основанные на пересечении, O (N ^ 2), так как они проверяют каждый новый набор по всем существующим, поэтому я использовал подход, который индексирует каждое число и работает близко к O (N) (если мы примем, что словарь ищет O (1)). Затем я выполнил тесты и почувствовал себя полным идиотом, потому что он работал медленнее, но при более внимательном рассмотрении оказалось, что тестовые данные заканчиваются лишь несколькими различными наборами результатов, поэтому квадратичным алгоритмам не нужно много работать для делать. Протестируйте его с более чем 10-15 различными бинами, и мой алгоритм намного быстрее. Попробуйте тестовые данные с более чем 50 различными ячейками, и они чрезвычайно быстрее.

(Редактировать: была также проблема с тем, как выполняется тест, но я ошибся в своем диагнозе. Я изменил свой код для работы с тем, как выполняются повторные тесты).

def mergelists5(data):
    """Check each number in our arrays only once, merging when we find
    a number we have seen before.
    """

    bins = range(len(data))  # Initialize each bin[n] == n
    nums = dict()

    data = [set(m) for m in data ]  # Convert to sets    
    for r, row in enumerate(data):
        for num in row:
            if num not in nums:
                # New number: tag it with a pointer to this row's bin
                nums[num] = r
                continue
            else:
                dest = locatebin(bins, nums[num])
                if dest == r:
                    continue # already in the same bin

                if dest > r:
                    dest, r = r, dest   # always merge into the smallest bin

                data[dest].update(data[r]) 
                data[r] = None
                # Update our indices to reflect the move
                bins[r] = dest
                r = dest 

    # Filter out the empty bins
    have = [ m for m in data if m ]
    print len(have), "groups in result"
    return have


def locatebin(bins, n):
    """
    Find the bin where list n has ended up: Follow bin references until
    we find a bin that has not moved.
    """
    while bins[n] != n:
        n = bins[n]
    return n
4
ответ дан 27 November 2019 в 02:43
поделиться

Это будет мой обновленный подход:

def merge(data):
    sets = (set(e) for e in data if e)
    results = [next(sets)]
    for e_set in sets:
        to_update = []
        for i,res in enumerate(results):
            if not e_set.isdisjoint(res):
                to_update.insert(0,i)

        if not to_update:
            results.append(e_set)
        else:
            last = results[to_update.pop(-1)]
            for i in to_update:
                last |= results[i]
                del results[i]
            last |= e_set

    return results

Примечание: Во время слияния пустые списки будут удалены.

Обновление: Надежность.

Вам нужны два теста для 100% надежности успеха:

  • Убедитесь, что все получающиеся наборы не связаны друг с другом:

    merged = [{0, 1, 3, 4, 5, 10, 11, 16}, {8, 2}, {8}]
    
    from itertools import combinations
    for a,b in combinations(merged,2):
        if not a.isdisjoint(b):
            raise Exception(a,b)    # just an example
    
  • Убедитесь, что объединенный набор охватывает исходные данные. (как предложено katrielalex)

Я думаю, что это займет некоторое время, но, возможно, оно того стоит, если вы хотите быть на 100% уверены.

1
ответ дан 27 November 2019 в 02:43
поделиться

Использование матричных манипуляций

Позвольте мне предвосхитить этот ответ следующим комментарием:

ЭТО НЕПРАВИЛЬНЫЙ СПОСОБ ДЕЛАТЬ ЭТО. ЭТО НЕСКОЛЬКО ЧИСЛЕННОЙ НЕУСТОЙЧИВОСТИ И МНОГО МЕНЬШЕ, ЧЕМ ПРЕДСТАВЛЕНЫ ДРУГИЕ МЕТОДЫ, ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК.

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

from numpy import where, newaxis
from scipy import linalg, array, zeros

X = [[0,1,3],[2],[3,1]]

Нам нужно преобразовать данные в потоковый граф. Если строка i переходит в значение j , мы помещаем его в матрицу. Здесь у нас есть 3 строки и 4 уникальных значения:

A = zeros((4,len(X)), dtype=float)
for i,row in enumerate(X):
    for val in row: A[val,i] = 1

В общем, вам нужно изменить 4, чтобы получить количество уникальных значений, которые у вас есть. Если набор является списком целых чисел, начиная с 0, как у нас, вы можете просто сделать это наибольшим числом. Теперь выполним разложение по собственным значениям. Точнее SVD, поскольку наша матрица не квадратная.

S  = linalg.svd(A)

Мы хотим оставить только 3х3 часть этого ответа, так как он будет представлять поток пулов. На самом деле мы хотим только абсолютные значения этой матрицы; мы заботимся только о том, есть ли поток в этом кластерном пространстве.

M  = abs(S[2])

Мы можем думать об этой матрице M как о марковской матрице и сделать ее явной путем нормализации строк. Как только мы получим это, мы вычислим декомпозицию собственного значения (слева). этой матрицы.

M /=  M.sum(axis=1)[:,newaxis]
U,V = linalg.eig(M,left=True, right=False)
V = abs(V)

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

idx = where(U > .999)[0]
C = V.T[idx] > 0

Я должен использовать 0,999 из-за вышеупомянутой численной нестабильности. На этом мы закончили! Каждый независимый кластер теперь может вытягивать соответствующие строки:

for cluster in C:
    print where(A[:,cluster].sum(axis=1))[0]

Что дает, как и предполагалось:

[0 1 3]
[2]

Замените X на lst, и вы получите: [ 0 1 3 4 5 10 11 16] [2 8].


Приложение

Почему это может быть полезно? Я не знаю, откуда берутся ваши базовые данные, но что происходит, когда соединения не абсолютны? Скажем, в строке 1 есть запись 3 80% времени - как бы вы обобщили проблему? Метод потока, описанный выше, будет работать очень хорошо и будет полностью параметризован этим значением .999, чем дальше от единицы, тем слабее ассоциация.


Визуальное представление

Так как картинка стоит 1K слов, здесь приведены графики матриц A и V для моего примера и вашего lst соответственно , Обратите внимание, как в V разбивается на два кластера (это блок-диагональная матрица с двумя блоками после перестановки), поскольку для каждого примера было только два уникальных списка!

My Example Your sample data


Ускоренная реализация

Оглядываясь назад, я понял, что вы можете пропустить шаг SVD и вычислить только одну декомпозицию:

M = dot(A.T,A)
M /=  M.sum(axis=1)[:,newaxis]
U,V = linalg.eig(M,left=True, right=False)

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

7
ответ дан 27 November 2019 в 02:43
поделиться
Другие вопросы по тегам:

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