web-dev-qa-db-ja.com

Python:交差点に基づく単純なリストのマージ

次のような整数のリストがあると考えてください。

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

問題は、少なくとも1つの共通要素を持つリストをマージすることです。したがって、特定の部分のみの結果は次のようになります。

#--------------------------------------
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で与えられたコードは、単純なケースでは少し速いことが示されました。 「Hooked」によって与えられた方法は完全に異なるアプローチであるため、まだテストされていません(ちなみにそれは興味深いようです)。これらすべてのテスト手順は、結果を保証するのが非常に難しいか不可能である可能性があります。私が使用する実際のデータセットは非常に大きく複雑であるため、繰り返すだけではエラーを追跡することはできません。つまり、モジュールとして大きなコード内でメソッドをその場所にプッシュする前に、メソッドの信頼性に100%満足する必要があります。単純に今のところNiklasの方法はより速く、単純集合の答えはもちろん正しいです。
ただし、実際の大きなデータセットでうまく機能することをどのように確認できますか?エラーを視覚的に追跡できないためです。

更新3:この問題では、速度よりもメソッドの信頼性の方がはるかに重要であることに注意してください。最終的に最大のパフォーマンスを得るために、PythonコードをFortranに変換できることを願っています。

更新4:
この投稿には多くの興味深い点があり、寛大に答え、建設的なコメントが与えられています。すべてをよく読むことをお勧めします。質問の展開、驚くべき答え、建設的なコメントと議論に感謝します。

40
Developer

私の試み:

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
22
Niklas B.

私はこの質問と 重複したもの でこのトピックについて言われ、行われたすべてを要約しようとしました。

私はtesttimeすべてのソリューション(すべてのコード- ここ )。

テスト

これは、テストモジュールの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

私がテストできたものの中で、2つ失敗しました

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

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

タイミング

パフォーマンスは、採用されたデータテストと強く関連しています。

これまでのところ、3つの回答が彼らと他の解決策の時間を計ろうとしました。彼らは異なるテストデータを使用したため、異なる結果が得られました。

  1. Niklasベンチマーク 非常に調整可能です。彼のベンチマークを使用すると、いくつかのパラメーターを変更してさまざまなテストを実行できます。

    私は彼が彼自身の答えで使用したのと同じ3セットのパラメーターを使用し、それらを3つの異なるファイルに入れました。

    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で終わるすべての関数はテスト中に自動ロードされるため、追加/テスト/改善するのは難しいことではありません。独自のソリューション。

何か問題があるかどうかも教えてください、それはたくさんのコーディングであり、私はいくつかの新鮮な目を使うことができました:)

13
Rik Poggi

行列操作の使用

この回答の前に次のコメントを付けましょう。

これISこれを行うための間違った方法。ITIS数値的不安定性を引き起こし、ISはるかに遅い提示されている他の方法よりも、ATあなた自身のリスク。を使用してください。

そうは言っても、私は動的な観点から問題を解決することに抵抗できませんでした(そしてあなたが問題について新鮮な視点を得ることができることを願っています)。 theoryでは、これは常に機能するはずですが、固有値の計算は失敗することがよくあります。アイデアは、リストを行から列へのフローと考えることです。 2つの行が共通の値を共有する場合、それらの間に接続フローがあります。これらの流れを水と考えると、それらの間に接続パスがある場合、流れは小さなプールに集まっていることがわかります。簡単にするために、より小さなセットを使用しますが、データセットでも機能します。

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)

プールの流れを表すため、この回答の3x3の部分のみを保持します。実際、この行列の絶対値のみが必要です。このclusterスペースにフローがあるかどうかだけが気になります。

M  = abs(S[2])

この行列Mをマルコフ行列と見なし、行の正規化によって明示的にすることができます。これができたら、(左)固有値decompを計算します。この行列の。

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

これで、切断された(非エルゴード)マルコフ行列には、接続されていないクラスターごとに1の固有値が存在するNiceプロパティがあります。これらの単一性の値に関連付けられた固有ベクトルは、必要なものです。

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]

Xlstに変更すると、次のようになります:[ 0 1 3 4 5 10 11 16] [2 8]


補遺

なぜこれが役立つのでしょうか?基になるデータがどこから来ているのかわかりませんが、接続が絶対的でない場合はどうなりますか?行1にエントリ3が80%の確率で含まれているとすると、問題をどのように一般化しますか?上記のフロー方法は問題なく機能し、その.999値によって完全にパラメーター化されます。単一性から離れるほど、関連付けは緩くなります。


視覚的表現

写真は1Kワードの価値があるので、これが私の例とあなたのlstのそれぞれの行列AとVのプロットです。各例には2つの一意のリストしかないため、Vが2つのクラスターに分割されることに注意してください(これは、順列後に2つのブロックを持つブロック対角行列です)。

My ExampleYour sample data


より迅速な実装

後から考えると、SVDステップをスキップして、1つのdecompのみを計算できることに気付きました。

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

この方法の利点(速度以外)は、Mが対称になっているため、計算がより高速で正確になることです(虚数の心配がありません)。

7
Hooked

編集:OK、他の質問は閉じられました。ここに投稿してください。

いい質問です!グラフの連結成分問題と考えると、はるかに簡単です。次のコードは、優れた 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の各サブリストについて、その要素をグラフのノードと見なし、それらの間にエッジを追加します。 (接続性のみを考慮しているため、allエッジを追加する必要はありません-隣接するエッジのみです!)add_Edgeは、2つのオブジェクトを受け取り、それらをノードとして扱い(まだ存在しない場合は追加し)、それらの間にエッジを追加します。

次に、グラフの連結成分を見つけるだけです-解決された問題です! -そしてそれらを交差するセットとして出力します。

5
Katriel

これが私の答えです。私はそれを今日の一連の回答と照合していません。

交差ベースのアルゴリズムは、既存のすべてのセットに対して新しいセットをチェックするため、O(N ^ 2)です。そこで、各数値にインデックスを付け、O(N)の近くで実行するアプローチを使用しました。 =(辞書検索がO(1)であると認めた場合)次に、ベンチマークを実行し、実行速度が遅いため完全な馬鹿のように感じましたが、詳しく調べてみると、テストデータはほんの一握りの明確なものになってしまうことがわかりました結果セットなので、2次アルゴリズムで行う作業はそれほど多くありません。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
alexis

この新しい関数は、必要最小限の数の非結合性テストのみを実行します。これは、他の同様のソリューションでは実行できないことです。また、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とオーバーラップする場合、3に対する2のテストを安全にスキップするために、2を再テストして1とオーバーラップするかどうかを確認する必要があります。

これに対処するには2つの方法があります。 1つ目は、オーバーラップしてマージするたびに、他のセットに対してセット1のテストを再開することです。 2つ目は、1と4を比較してテストを続行し、戻って再テストすることです。後者の場合、1回のパスでより多くのマージが発生するため、非結合テストが少なくなります。したがって、再テストパスでは、テストするセットが少なくなります。

問題は、どのセットを再テストする必要があるかを追跡することです。上記の例では、4が最初にテストされる前に1がすでに現在の状態にあったため、1は2に対して再テストする必要がありますが、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

で使用される非Pythonicカウンター(irange)または複雑なミューテーション(delpopinsert)は必要ありません他の実装。単純な反復のみを使用し、重複するセットを最も単純な方法でマージし、データを通過するたびに1つの新しいリストを作成します。

私の(より速くてより単純な)バージョンのテストコード:

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)
3
agf

disjoint-set data structure (具体的にはdisjoint forest)を使用した実装です。 comingstormのヒント at 共通の要素が1つでもあるセットのマージ 。わずかな(〜5%)速度向上のためにパス圧縮を使用しています。完全に必要というわけではありません(そして、findが末尾再帰になるのを防ぎ、速度が低下する可能性があります)。互いに素なフォレストを表すためにdictを使用していることに注意してください。データが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のベンチマークの他の最良のアルゴリズムに匹敵します。

2
ecatmur

マージ関数の結果がOKかどうかをチェックする関数(Python 3.1)は次のとおりです。それはチェックします:

  • 結果セットは互いに素ですか? (和集合の要素数==要素数の合計)
  • 結果セットの要素は入力リストの要素と同じですか?
  • すべての入力リストは結果セットのサブセットですか?
  • すべての結果セットは最小限ですか?つまり、2つの小さなセットに分割することは不可能ですか?
  • それはnot空の結果セットがあるかどうかをチェックします-あなたがそれらを望むかどうかはわかりません...

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

したがって、最速の11倍の速度ですが、コードははるかに単純で読みやすくなっています。

1
robert king

これは私のupdatedアプローチになります:

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%の信頼性を得るには、次の2つのテストが必要です。

  • 結果のすべてのセットが相互に分離されていることを確認します。

    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
Rik Poggi

まず、ベンチマークが公正であるかどうかは正確にはわかりません。

関数の先頭に次のコードを追加します。

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

これは、すべてのリストのすべての値のうち、88個の異なる値しかないことを意味します。通常、現実の世界では重複はまれであり、より明確な値が期待されます。 (もちろん、私はあなたのデータがどこから来たのかわからないので、仮定を立てることはできません)。

重複がより一般的であるため、セットがばらばらになる可能性が低くなります。これは、set.isdisjoint()メソッドがはるかに高速になることを意味します。これは、いくつかのテストの後でのみ、セットが互いに素ではないことがわかるためです。

そうは言っても、とにかく互いに素を使用する方法が最も速いと思いますが、20倍速くなるのではなく、ベンチマークテストが異なる他の方法よりも10倍速くなるはずです。

とにかく、私はこれを解決するために少し異なる手法を試すと思いましたが、マージソートは遅すぎました。この方法は、ベンチマークを使用する2つの最速の方法よりも約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
robert king

これは、Niklasが提供するソリューションよりも低速です(彼のソリューションでは0.5秒ではなく3.9秒をtest.txtで取得しました)が、同じ結果が得られ、たとえば次のように実装する方が簡単な場合があります。 Fortranは、セットを使用しないため、要素の合計量を並べ替えてから、すべての要素を1回実行するだけです。

マージされたリストのIDを含むリストを返すため、空のリストも追跡し、マージされないままにします。

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
steabert

ただ楽しみのために...

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
1
ChessMaster

flagを使用して、最終的な相互排他的な結果を確実に取得します

def merge(lists): 
    while(1):
        flag=0
        for i in range(0,len(lists)):
            for j in range(i+1,len(lists)):
                if len(intersection(lists[i],lists[j]))!=0:
                    lists[i]=union(lists[i],lists[j])
                    lists.remove(lists[j])
                    flag+=1
                    break
        if flag==0:
            break
    return lists
0
Zhenzhen Wang