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

каков наиболее эффективный способ сделать это на больших данных (элементы-это просто числа)? Is 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: По моему опыту код, данный Никлас Baumstark ниже показано, что это немного быстрее для простых случаев. Не тестировал метод, данный "зацепил" еще, так как это совершенно другой подход (кстати, он кажется интересным). Процедура тестирования для всех из них может быть действительно трудной или невозможной для обеспечения результатов. Реальный набор данных, который я буду использовать, настолько велик и сложен, что его невозможно отследить любая ошибка просто повторяется. То есть мне нужно быть на 100% удовлетворенным надежностью метода, прежде чем толкать его на свое место в большом коде как модуль. Просто пока Никласметод s быстрее, и ответ для простых наборов, конечно, правильный.
однако как я могу быть уверен, что он хорошо работает для реального большого набора данных? так как я не смогу отследить ошибки визуально!

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

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

15 ответов


моя попытка:

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

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

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

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

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

это 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))

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

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

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 benchmark очень twakable. С его banchmark можно было делать разные тесты, изменяя некоторые параметры.

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

    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. С Свенданные тестирования я получил следующие результаты:

    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 будет будьте автоматически загружены во время тестов, поэтому не должно быть сложно добавить/протестировать/улучшить собственное решение.

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


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

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

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

теория этот должно работать все время, но вычисления на собственные значения часто могут завершиться неудачей. Идея состоит в том, чтобы думать о вашем списке как поток из строк в столбцы. Если две строки имеют общее значение, между ними существует соединительный поток. Если бы мы думали об этих потоках как о воде, мы бы увидели, что они собираются в маленькие лужицы, когда между ними есть соединительный путь. Для простоты, я буду использовать меньший набор, хотя он работает с набором данных хорошо:
from numpy import where, newaxis
from scipy import linalg, array, zeros

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

нам нужно преобразовать данные в потоковую диаграмму. Если строки я впадает в значение 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)

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

M  = abs(S[2])

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

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


дополнительное соглашение

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


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

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

My ExampleYour sample data


Более Быстрая Реализация

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

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

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


EDIT: хорошо, другие вопросы были закрыты, разместив здесь.

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


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

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

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

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

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


мой ответ не помогает с основной проблемой поиска улучшенного алгоритма перекодирования в FORTRAN; это как раз то, что мне кажется самым простым и элегантным способом реализации алгоритма в 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

нет необходимости для ООН-подходящие для Python счетчики (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)

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

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% уверены.


вот функция (Python 3.1), чтобы проверить, является ли результат функции слияния в порядке. Он проверяет:

  • являются ли результирующие наборы непересекающимися? (количество элементов объединения = = сумма чисел элементов)
  • являются ли элементы результирующих наборов такими же, как и входных списков?
  • все список входное подмножество результирующий набор?
  • является ли каждый результирующий набор минимальным, т. е. невозможно разделить его на два меньших набора?
  • это не проверьте, есть ли пустые результирующие наборы - я не знаю, хотите ли вы их или нет...

.

from itertools import chain

def check(lsts, result):
    lsts = [set(s) for s in lsts]
    all_items = set(chain(*lsts))
    all_result_items = set(chain(*result))
    num_result_items = sum(len(s) for s in result)
    if num_result_items != len(all_result_items):
        print("Error: result sets overlap!")
        print(num_result_items, len(all_result_items))
        print(sorted(map(len, result)), sorted(map(len, lsts)))
    if all_items != all_result_items:
        print("Error: result doesn't match input lists!")
    if not all(any(set(s).issubset(t) for t in result) for s in lst):
        print("Error: not all input lists are contained in a result set!")

    seen = set()
    todo = list(filter(bool, lsts))
    done = False
    while not done:
        deletes = []
        for i, s in enumerate(todo): # intersection with seen, or with unseen result set, is OK
            if not s.isdisjoint(seen) or any(t.isdisjoint(seen) for t in result if not s.isdisjoint(t)):
                seen.update(s)
                deletes.append(i)
        for i in reversed(deletes):
            del todo[i]
        done = not deletes
    if todo:
        print("Error: A result set should be split into two or more parts!")
        print(todo)

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

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

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

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


это медленнее, чем решение, предложенное Никласом (я получил 3.9 s на тесте.txt вместо 0.5 s для его решения), но дает тот же результат и может быть проще реализовать, например, в 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

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

добавить следующий код в начало функции:

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

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

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

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

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

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

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

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

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()

этот подход сопоставим с другими лучшими алгоритмами на тестах Rik.


мое решение, хорошо работает на небольших списках и вполне читабельна, без зависимостей.

def merge_list(starting_list):
    final_list = []
    for i,v in enumerate(starting_list[:-1]):
        if set(v)&set(starting_list[i+1]):
            starting_list[i+1].extend(list(set(v) - set(starting_list[i+1])))
        else:
            final_list.append(v)
    final_list.append(starting_list[-1])
    return final_list

бенчмаркинг это:

списки = [[1,2,3],[3,5,6],[8,9,10],[11,12,13]]

%timeit merge_list (списки)

100000 петель, Самое лучшее 3: 4.9 µs в петлю


Это можно решить в O (n) с помощью алгоритма union-find. Учитывая первые две строки ваших данных, ребра для использования в union-find являются следующими парами: (0,1),(1,3),(1,0),(0,3),(3,4),(4,5),(5,10)