Python: простое слияние списков на основе пересечений - PullRequest
38 голосов
/ 02 февраля 2012

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

#--------------------------------------
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]
#--------------------------------------

Какой самый эффективный способ сделать это для больших данных (элементы - просто числа)? Является ли tree структурировать что-то для размышления?Я делаю работу сейчас, преобразовывая списки в sets и повторяя для пересечений, но это медленно!Кроме того, у меня такое ощущение, что это так элементарно!Кроме того, в реализации чего-то не хватает (неизвестно), потому что некоторые списки иногда остаются не объединенными!Сказав это, если вы предлагаете самореализацию, пожалуйста, будьте щедрыми и предоставьте простой пример кода [очевидно, 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: По моему опыту код, приведенный ниже Niklas Baumstark , показался немного быстрее для простых случаев.Еще не проверен метод, данный «Крючком», поскольку это совершенно другой подход (кстати, он кажется интересным).Процедура тестирования для всего этого может быть очень трудной или невозможной для обеспечения результатов.Реальный набор данных, который я буду использовать, настолько велик и сложен, что невозможно отследить любую ошибку, просто повторив.То есть я должен быть на 100% удовлетворен надежностью метода, прежде чем поместить его на место в большом коде в качестве модуля.Просто пока Niklas метод быстрее, и ответ для простых наборов, конечно, правильный.
Однако как я могу быть уверен, что он работает хорошо для действительно большого набора данных? Поскольку я не смогу визуально отследить ошибки!

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

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

Ответы [ 15 ]

21 голосов
/ 02 февраля 2012

Моя попытка:

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)

Benchmark:

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
13 голосов
/ 26 февраля 2012

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

Я пытался проверить и время каждое решение (весь код здесь ).

Тестирование

Это TestCase из модуля тестирования:

class MergeTestCase(unittest.TestCase):

    def setUp(self):
        with open('./lists/test_list.txt') as f:
            self.lsts = json.loads(f.read())
        self.merged = self.merge_func(deepcopy(self.lsts))

    def test_disjoint(self):
        """Check disjoint-ness of merged results"""
        from itertools import combinations
        for a,b in combinations(self.merged, 2):
            self.assertTrue(a.isdisjoint(b))

    def test_coverage(self):    # Credit to katrielalex
        """Check coverage original data"""
        merged_flat = set()
        for s in self.merged:
            merged_flat |= s

        original_flat = set()
        for lst in self.lsts:
            original_flat |= set(lst)

        self.assertTrue(merged_flat == original_flat)

    def test_subset(self):      # Credit to WolframH
        """Check that every original data is a subset"""
        for lst in self.lsts:
            self.assertTrue(any(set(lst) <= e for e in self.merged))

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

Я не смог проверить следующее:

katrielalex
steabert

Среди тех, что я мог проверить, два не удалось :

  -- Going to test: agf (optimized) --
Check disjoint-ness of merged results ... FAIL

  -- Going to test: robert king --
Check disjoint-ness of merged results ... FAIL

Время

Показатели тесно связаны сИспользуется проверка данных.

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

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

    Я использовал те же три набора параметров, которые он использовал в своем ответе, и поместил их в три разных файла:

    filename = './lists/timing_1.txt'
    class_count = 50,
    class_size = 1000,
    list_count_per_class = 100,
    large_list_sizes = (100, 1000),
    small_list_sizes = (0, 100),
    large_list_probability = 0.5,
    
    filename = './lists/timing_2.txt'
    class_count = 15,
    class_size = 1000,
    list_count_per_class = 300,
    large_list_sizes = (100, 1000),
    small_list_sizes = (0, 100),
    large_list_probability = 0.5,
    
    filename = './lists/timing_3.txt'
    class_count = 15,
    class_size = 1000,
    list_count_per_class = 300,
    large_list_sizes = (100, 1000),
    small_list_sizes = (0, 100),
    large_list_probability = 0.1,
    

    Вот результаты, которые я получил:

    Из файла: timing_1.txt

    Timing with: >> Niklas << Benchmark
    Info: 5000 lists, average size 305, max size 999
    
    Timing Results:
    10.434  -- alexis
    11.476  -- agf
    11.555  -- Niklas B.
    13.622  -- Rik. Poggi
    14.016  -- agf (optimized)
    14.057  -- ChessMaster
    20.208  -- katrielalex
    21.697  -- steabert
    25.101  -- robert king
    76.870  -- Sven Marnach
    133.399  -- hochl
    

    Из файла: timing_2.txt

    Timing with: >> Niklas << Benchmark
    Info: 4500 lists, average size 305, max size 999
    
    Timing Results:
    8.247  -- Niklas B.
    8.286  -- agf
    8.637  -- Rik. Poggi
    8.967  -- alexis
    9.090  -- ChessMaster
    9.091  -- agf (optimized)
    18.186  -- katrielalex
    19.543  -- steabert
    22.852  -- robert king
    70.486  -- Sven Marnach
    104.405  -- hochl
    

    Из файла:timing_3.txt

    Timing with: >> Niklas << Benchmark
    Info: 4500 lists, average size 98, max size 999
    
    Timing Results:
    2.746  -- agf
    2.850  -- Niklas B.
    2.887  -- Rik. Poggi
    2.972  -- alexis
    3.077  -- ChessMaster
    3.174  -- agf (optimized)
    5.811  -- katrielalex
    7.208  -- robert king
    9.193  -- steabert
    23.536  -- Sven Marnach
    37.436  -- hochl
    
  2. С данными испытаний Sven я получил следующие результаты:

    Timing with: >> Sven << Benchmark
    Info: 200 lists, average size 10, max size 10
    
    Timing Results:
    2.053  -- alexis
    2.199  -- ChessMaster
    2.410  -- agf (optimized)
    3.394  -- agf
    3.398  -- Rik. Poggi
    3.640  -- robert king
    3.719  -- steabert
    3.776  -- Niklas B.
    3.888  -- hochl
    4.610  -- Sven Marnach
    5.018  -- katrielalex
    
  3. И, наконец, с помощью теста Agf я получил:

    Timing with: >> Agf << Benchmark
    Info: 2000 lists, average size 246, max size 500
    
    Timing Results:
    3.446  -- Rik. Poggi
    3.500  -- ChessMaster
    3.520  -- agf (optimized)
    3.527  -- Niklas B.
    3.527  -- agf
    3.902  -- hochl
    5.080  -- alexis
    15.997  -- steabert
    16.422  -- katrielalex
    18.317  -- robert king
    1257.152  -- Sven Marnach
    

Как я сказал в начале, весь код доступен в этом репозитории git.Все функции слияния находятся в файле с именем core.py, каждая функция с именем, оканчивающимся на _merge, будет автоматически загружаться во время тестов, поэтому не составит труда добавить / проверить / улучшить собственное решение.

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

7 голосов
/ 02 февраля 2012

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

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

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

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

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

Я должен использовать .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, чем дальше от единицы, тем слабее ассоциация.


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

Поскольку картинка стоит 1 тыс. Слов, здесь приведены графики матриц 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 теперь симметричен, следовательно, вычисления могут быть более быстрыми и более точными (без мнимых значений, о которых можно беспокоиться).

5 голосов
/ 20 февраля 2012

РЕДАКТИРОВАТЬ: ОК, другие вопросы были закрыты, размещение здесь.

Хороший вопрос!Намного проще, если вы думаете о ней как о проблеме связанных компонентов в графе.Следующий код использует превосходную библиотеку графов networkx и функцию pairs из этого вопроса .

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

lists = [[1,2,3],[3,5,6],[8,9,10],[11,12,13]]

import networkx
g = networkx.Graph()
for sub_list in lists:
    for edge in pairs(sub_list):
            g.add_edge(*edge)

networkx.connected_components(g)
[[1, 2, 3, 5, 6], [8, 9, 10], [11, 12, 13]]

Объяснение

Создаем новый (пустой) график g.Для каждого подсписка в lists рассмотрите его элементы как узлы графа и добавьте ребро между ними.(Поскольку нас заботит только связность, нам не нужно добавлять все ребра - только соседние!) Обратите внимание, что add_edge принимает два объекта, обрабатывает их как узлы (и добавляет их, если онине существует), и добавляет ребро между ними.

Затем мы просто находим связанные компоненты графа - решенная проблема!- и выведите их как наши пересекающиеся множества.

4 голосов
/ 26 февраля 2012

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

Алгоритмы, основанные на пересечении, 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
3 голосов
/ 22 февраля 2012

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

from collections import deque

def merge(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

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

Вот пример случая. Если у вас 4 комплекта, вам необходимо сравнить:

1, 2
1, 3
1, 4
2, 3
2, 4
3, 4

Если 1 перекрывается с 3, то 2 необходимо повторно проверить, чтобы увидеть, перекрывается ли теперь 1 с целью безопасного пропуска проверки 2 против 3.

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

Проблема состоит в том, чтобы отследить, какие наборы должны быть повторно протестированы. В приведенном выше примере 1 необходимо повторно протестировать против 2, но не против 4, поскольку 1 уже находился в своем текущем состоянии, прежде чем 4 был протестирован в первый раз.

Счетчик disjoint позволяет отслеживать это.


Мой ответ не помогает с основной проблемой поиска улучшенного алгоритма перекодирования в Фортран; это как раз то, что мне кажется самым простым и элегантным способом реализации алгоритма в Python.

Согласно моему тестированию (или тесту в принятом ответе), оно немного (до 10%) быстрее, чем следующее быстрое решение.

def merge0(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

Нет необходимости в непифоновых счетчиках (i, range) или сложной мутации (del, pop, insert), используемых в других реализациях. Он использует только простую итерацию, объединяет перекрывающиеся наборы самым простым способом и создает один новый список при каждом проходе данных.

Моя (более быстрая и простая) версия тестового кода:

import random

tenk = range(10000)
lsts = [random.sample(tenk, random.randint(0, 500)) for _ in range(2000)]

setup = """
def merge0(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

def merge1(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

lsts = """ + repr(lsts)

import timeit
print timeit.timeit("merge0(lsts)", setup=setup, number=10)
print timeit.timeit("merge1(lsts)", setup=setup, number=10)
2 голосов
/ 22 августа 2012

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

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

1 голос
/ 24 февраля 2012

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

Добавление следующего кода в начало моей функции:

c = Counter(chain(*lists))
    print c[1]
"88"

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

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

Сказав все это, я верю, что представленные методы, использующие дизъюнкт, в любом случае являются самыми быстрыми, но я просто говорю, что вместо того, чтобы быть в 20 раз быстрее, возможно, они должны быть только в 10 раз быстрее, чем другие методы с другим тестированием.

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

Я думал, что закажу все

import heapq
from itertools import chain
def merge6(lists):
    for l in lists:
        l.sort()
    one_list = heapq.merge(*[zip(l,[i]*len(l)) for i,l in enumerate(lists)]) #iterating through one_list takes 25 seconds!!
    previous = one_list.next()

    d = {i:i for i in range(len(lists))}
    for current in one_list:
        if current[0]==previous[0]:
            d[current[1]] = d[previous[1]]
        previous=current

    groups=[[] for i in range(len(lists))]
    for k in d:
        groups[d[k]].append(lists[k]) #add a each list to its group

    return [set(chain(*g)) for g in groups if g] #since each subroup in each g is sorted, it would be faster to merge these subgroups removing duplicates along the way.


lists = [[1,2,3],[3,5,6],[8,9,10],[11,12,13]]
print merge6(lists)
"[set([1, 2, 3, 5, 6]), set([8, 9, 10]), set([11, 12, 13])]""



import timeit
print timeit.timeit("merge1(lsts)", setup=setup, number=10)
print timeit.timeit("merge4(lsts)", setup=setup, number=10)
print timeit.timeit("merge6(lsts)", setup=setup, number=10)
5000 lists, 5 classes, average size 74, max size 1000
1.26732238315
5000 lists, 5 classes, average size 74, max size 1000
1.16062907437
5000 lists, 5 classes, average size 74, max size 1000
30.7257182826
1 голос
/ 22 февраля 2012
lists = [[1,2,3],[3,5,6],[8,9,10],[11,12,13]]

import networkx as nx
g = nx.Graph()

for sub_list in lists:
    for i in range(1,len(sub_list)):
        g.add_edge(sub_list[0],sub_list[i])

print nx.connected_components(g)
#[[1, 2, 3, 5, 6], [8, 9, 10], [11, 12, 13]]

Производительность:

5000 lists, 5 classes, average size 74, max size 1000
15.2264976415

Производительность слияния1:

print timeit.timeit("merge1(lsts)", setup=setup, number=10)
5000 lists, 5 classes, average size 74, max size 1000
1.26998780571

Так что это в 11 раз медленнее, чем самый быстрый ... но код гораздо проще и удобнее для чтения!

1 голос
/ 21 февраля 2012

Просто для удовольствия ...

def merge(mylists):
    results, sets = [], [set(lst) for lst in mylists if lst]
    upd, isd, pop = set.update, set.isdisjoint, sets.pop
    while sets:
        if not [upd(sets[0],pop(i)) for i in xrange(len(sets)-1,0,-1) if not isd(sets[0],sets[i])]:
            results.append(pop(0))
    return results

и мой переписать лучший ответ

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