web-dev-qa-db-ja.com

隣接する等しい要素のないリストのすべての順列を生成します

リストを並べ替えるとき、

a = [1,2,3,3,2,2,1]
sorted(a) => [1, 1, 2, 2, 2, 3, 3]

結果のリストでは、等しい要素は常に隣接しています。

反対のタスクを達成するにはどうすればよいですか-リストをシャッフルして、等しい要素が決して隣接しないようにします(または、可能な限り)。

たとえば、上記のリストの場合、考えられる解決策の1つは

p = [1,3,2,3,2,1,2]

より正式には、リストaが与えられると、ペアの数を最小化する順列pを生成しますp[i]==p[i+1]

リストは大きいため、すべての順列を生成およびフィルタリングすることはオプションではありません。

ボーナスの質問:そのようなすべての順列を効率的に生成する方法は?

これは、ソリューションをテストするために使用しているコードです。 https://Gist.github.com/gebrkn/9f550094b3d24a35aebd

UPD:多くの人が優れた回答を投稿したため、ここで勝者を選ぶのは難しい選択でした。 @ VincentvanderWeele@ David Eisenstat@ Coady@ enrico.bacis および @ srgerg 可能な限り最高の順列を完璧に生成する機能を提供。 @ tobias_k そして、デイビッドもボーナスの質問に答えました(すべての順列を生成します)。正当性の証明のためのデイビッドへの追加のポイント。

@VincentvanderWeeleのコードは最速のようです。

86
georg

これは、Thijserの現在不完全な疑似コードに沿ったものです。考えは、それがちょうど取られた場合を除き、残りの項目タイプの中で最も頻繁に取ることです。 (このアルゴリズムの Coadyの実装 も参照してください。)

import collections
import heapq


class Sentinel:
    pass


def david_eisenstat(lst):
    counts = collections.Counter(lst)
    heap = [(-count, key) for key, count in counts.items()]
    heapq.heapify(heap)
    output = []
    last = Sentinel()
    while heap:
        minuscount1, key1 = heapq.heappop(heap)
        if key1 != last or not heap:
            last = key1
            minuscount1 += 1
        else:
            minuscount2, key2 = heapq.heappop(heap)
            last = key2
            minuscount2 += 1
            if minuscount2 != 0:
                heapq.heappush(heap, (minuscount2, key2))
        output.append(last)
        if minuscount1 != 0:
            heapq.heappush(heap, (minuscount1, key1))
    return output

正当性の証明

カウントがk1とk2の2つのアイテムタイプの場合、k1 <k2の場合、k2-k1-1個の欠陥、k1 = k2の場合は0個の欠陥、k1> k2の場合はk1-k2-1個の欠陥があります。 =のケースは明らかです。その他は対称です。少数要素の各インスタンスは、可能なk1 + k2-1の合計のうち最大2つの欠陥を防ぎます。

この貪欲なアルゴリズムは、次のロジックによって最適なソリューションを返します。最適なソリューションに拡張される場合、プレフィックス(部分ソリューション)safeを呼び出します。明らかに、空のプレフィックスは安全であり、安全なプレフィックスがソリューション全体である場合、そのソリューションは最適です。各欲張りなステップが安全を維持することを帰納的に示すことで十分です。

貪欲なステップが欠陥を導入する唯一の方法は、アイテムタイプが1つだけ残っている場合です。この場合、続行する方法は1つだけであり、その方法は安全です。そうでない場合は、Pを考慮中のステップの直前の(安全な)プレフィックス、P 'を直後のプレフィックス、SをPを拡張する最適なソリューションとします。SがP'を拡張する場合、これで完了です。それ以外の場合、P '= Px and S = PQ and Q = yQ'とします。xとyはアイテム、QとQ 'はシーケンスです。

最初にPがyで終わらないと仮定します。アルゴリズムの選択により、xは少なくともQでyと同じ頻度になります。 xとyのみを含むQの最大部分文字列を考えます。最初の部分文字列に少なくともyと同じ数のxがある場合、xで始まる追加の欠陥を導入することなく書き換えることができます。最初の部分文字列にxよりもyが多い場合、他の部分文字列にはyよりも多くのxがあります。これらの部分文字列を追加の欠陥なしに書き換えて、xが最初になるようにすることができます。どちらの場合も、必要に応じてP 'を拡張する最適なソリューションTを見つけます。

Pがyで終わると仮定します。 xの最初の出現を前に移動してQを変更します。その際、最大で1つの欠陥を導入し(xは以前)、1つの欠陥(yy)を除去します。

すべてのソリューションを生成する

これは tobias_kの答え に加えて、現在検討中の選択が何らかの方法でグローバルに制約されている場合を検出するための効率的なテストです。生成のオーバーヘッドは出力の長さのオーダーであるため、漸近的な実行時間が最適です。残念ながら、最悪の場合の遅延は2次です。より良いデータ構造で線形(最適)に減らすことができます。

from collections import Counter
from itertools import permutations
from operator import itemgetter
from random import randrange


def get_mode(count):
    return max(count.items(), key=itemgetter(1))[0]


def enum2(prefix, x, count, total, mode):
    prefix.append(x)
    count_x = count[x]
    if count_x == 1:
        del count[x]
    else:
        count[x] = count_x - 1
    yield from enum1(prefix, count, total - 1, mode)
    count[x] = count_x
    del prefix[-1]


def enum1(prefix, count, total, mode):
    if total == 0:
        yield Tuple(prefix)
        return
    if count[mode] * 2 - 1 >= total and [mode] != prefix[-1:]:
        yield from enum2(prefix, mode, count, total, mode)
    else:
        defect_okay = not prefix or count[prefix[-1]] * 2 > total
        mode = get_mode(count)
        for x in list(count.keys()):
            if defect_okay or [x] != prefix[-1:]:
                yield from enum2(prefix, x, count, total, mode)


def enum(seq):
    count = Counter(seq)
    if count:
        yield from enum1([], count, sum(count.values()), get_mode(count))
    else:
        yield ()


def defects(lst):
    return sum(lst[i - 1] == lst[i] for i in range(1, len(lst)))


def test(lst):
    perms = set(permutations(lst))
    opt = min(map(defects, perms))
    slow = {perm for perm in perms if defects(perm) == opt}
    fast = set(enum(lst))
    print(lst, fast, slow)
    assert slow == fast


for r in range(10000):
    test([randrange(3) for i in range(randrange(6))])
29
David Eisenstat

擬似コード:

  1. リストを並べ替える
  2. ソート済みリストの前半をループし、結果リストのすべての偶数インデックスを埋めます
  3. ソート済みリストの後半をループし、結果リストのすべての奇数インデックスを埋めます

入力の半分以上が同じ要素で構成されている場合にのみp[i]==p[i+1]があります。この場合、同じ要素を連続したスポットに配置する以外の選択肢はありません(ピジョンホールの原理による)。


コメントで指摘したように、このアプローチでは、要素の1つが少なくともn/2回(または奇数nの場合はn/2+1;これが(n+1)/2)偶数と奇数の両方)。そのような要素は最大で2つあり、2つある場合、アルゴリズムは正常に機能します。唯一の問題は、少なくとも半分の時間で発生する要素が1つある場合です。要素を見つけて最初に処理することで、この問題を簡単に解決できます。

pythonについてこれを適切に書くのに十分な知識がないので、githubから前バージョンのOPの実装をコピーするために自由を取りました:

# Sort the list
a = sorted(lst)

# Put the element occurring more than half of the times in front (if needed)
n = len(a)
m = (n + 1) // 2
for i in range(n - m + 1):
    if a[i] == a[i + m - 1]:
        a = a[i:] + a[:i]
        break

result = [None] * n

# Loop over the first half of the sorted list and fill all even indices of the result list
for i, elt in enumerate(a[:m]):
    result[2*i] = elt

# Loop over the second half of the sorted list and fill all odd indices of the result list
for i, elt in enumerate(a[m:]):
    result[2*i+1] = elt

return result
22

前の項目ではない、最も一般的な左の項目を取得するアルゴリズムが既に指定されていますが正しいです。ヒープを使用して最も一般的なものを追跡する最適な実装を次に示します。

import collections, heapq
def nonadjacent(keys):
    heap = [(-count, key) for key, count in collections.Counter(a).items()]
    heapq.heapify(heap)
    count, key = 0, None
    while heap:
        count, key = heapq.heapreplace(heap, (count, key)) if count else heapq.heappop(heap)
        yield key
        count += 1
    for index in xrange(-count):
        yield key

>>> a = [1,2,3,3,2,2,1]
>>> list(nonadjacent(a))
[2, 1, 2, 3, 1, 2, 3]
10
A. Coady

all再帰的なバックトラッキングアルゴリズムを使用して、「完全に並べ替えられていない」順列(隣接する位置に2つの等しい要素がない)を生成できます。実際、すべての順列を生成する唯一の違いは、最後の数値を追跡し、それに応じていくつかのソリューションを除外することです。

def unsort(lst, last=None):
    if lst:
        for i, e in enumerate(lst):
            if e != last:
                for perm in unsort(lst[:i] + lst[i+1:], e):
                    yield [e] + perm
    else:
        yield []

この形式では、関数は多くのサブリストを作成するため、あまり効率的ではありません。また、最初に最も制約の多い数値(最もカウントの大きい数値)を調べることで、速度を上げることができます。これは、数字のcountsのみを使用した、より効率的なバージョンです。

def unsort_generator(lst, sort=False):
    counts = collections.Counter(lst)
    def unsort_inner(remaining, last=None):
        if remaining > 0:
            # most-constrained first, or sorted for pretty-printing?
            items = sorted(counts.items()) if sort else counts.most_common()
            for n, c in items:
                if n != last and c > 0:
                    counts[n] -= 1   # update counts
                    for perm in unsort_inner(remaining - 1, n):
                        yield [n] + perm
                    counts[n] += 1   # revert counts
        else:
            yield []
    return unsort_inner(len(lst))

これを使用して、next完全な順列だけを生成することも、すべてを保持するlistを生成することもできます。ただし、no完全にソートされていない順列がある場合、このジェネレーターは結果としてnoを生成することに注意してください。

>>> lst = [1,2,3,3,2,2,1]
>>> next(unsort_generator(lst))
[2, 1, 2, 3, 1, 2, 3]
>>> list(unsort_generator(lst, sort=True))
[[1, 2, 1, 2, 3, 2, 3], 
 ... 36 more ...
 [3, 2, 3, 2, 1, 2, 1]]
>>> next(unsort_generator([1,1,1]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

この問題を回避するには、フォールバックとして他の回答で提案されているアルゴリズムの1つとこれを併用できます。これにより、完全にソートされていない置換があれば保証され、そうでない場合は適切な近似が返されます。

def unsort_safe(lst):
    try:
        return next(unsort_generator(lst))
    except StopIteration:
        return unsort_fallback(lst)
8
tobias_k

pythonでは、次のことができます。

ソートされたリストlがあると考えてください。

_length = len(l)
odd_ind = length%2
odd_half = (length - odd_ind)/2
for i in range(odd_half)[::2]:
    my_list[i], my_list[odd_half+odd_ind+i] = my_list[odd_half+odd_ind+i], my_list[i]
_

これらはインプレース操作であるため、かなり高速です(O(N))。 _l[i] == l[i+1]_から_l[i] == l[i+2]_にシフトすることに注意してください。したがって、最終的な順序はランダムではありませんが、質問を理解する方法からは、探しているのはランダムではありません。

ソートされたリストを中央で分割し、2つの部分で他のすべての要素を交換するという考え方です。

_l= [1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]_の場合、これは_l = [3, 1, 4, 2, 5, 1, 3, 1, 4, 2, 5]_につながります

1つの要素の存在量がリストの長さの半分以上になるとすぐに、メソッドはすべての_l[i] == l[i + 1]_を取り除くことができません。

最も頻度の高い要素の量がリストのサイズの半分よりも小さい限り、上記は正常に機能しますが、次の関数は制限ケースも処理します(有名なオフバイワンの問題)最初の要素から始まる他のすべての要素は、最も豊富な要素でなければなりません。

_def no_adjacent(my_list):
    my_list.sort()
    length = len(my_list)
    odd_ind = length%2
    odd_half = (length - odd_ind)/2
    for i in range(odd_half)[::2]:
        my_list[i], my_list[odd_half+odd_ind+i] = my_list[odd_half+odd_ind+i], my_list[i]

    #this is just for the limit case where the abundance of the most frequent is half of the list length
    if max([my_list.count(val) for val in set(my_list)]) + 1 - odd_ind > odd_half:
        max_val = my_list[0]
        max_count = my_list.count(max_val)
        for val in set(my_list):
            if my_list.count(val) > max_count:
               max_val = val
               max_count = my_list.count(max_val)
        while max_val in my_list:
            my_list.remove(max_val)
        out = [max_val]
        max_count -= 1
        for val in my_list:
            out.append(val)
            if max_count:
                out.append(max_val)
                max_count -= 1
        if max_count:
            print 'this is not working'
            return my_list
            #raise Exception('not possible')
        return out
    else:
        return my_list
_
6
jojo

これが良いアルゴリズムです:

  1. まず、すべての数値が発生する頻度をカウントします。答えをマップに配置します。

  2. 最も頻繁に発生する番号が最初になるように、このマップを並べ替えます。

  3. 回答の最初の番号は、ソートされたマップの最初の番号です。

  4. 最初のものが1つ小さくなったマップを再利用します。

効率を改善したい場合は、ソート手順の効率を高める方法を探してください。

5
Thijser

ボーナスの質問に対する答えとして、これは、隣接する要素が同一にならないセットのすべての順列を見つけるアルゴリズムです。これは概念的には最も効率的なアルゴリズムであると考えています(ただし、他のアルゴリズムはより単純なコードに変換されるため、実際にはより高速になる場合があります)。ブルートフォースを使用せず、一意の順列のみを生成し、ソリューションに至らないパスは最も早い時点で切断されます。

他のすべての要素を組み合わせた場合よりも頻繁に発生するセット内の要素に対して「豊富な要素」という用語を使用し、豊富な要素の数から他の要素の数を引いた数に対して「豊富」という用語を使用します。
たとえば、セットabacには豊富な要素がなく、セットabacaおよびaabcaaには豊富な要素としてaがあります。および豊富度1および2それぞれ)

  1. 次のようなセットから始めます。

aaabbcd

  1. 最初の出現を繰り返しから分離します。

最初:abcd
繰り返し:aab

  1. 繰り返しの中に豊富な要素があれば、それを見つけて、豊富さを計算します。

豊富な要素:a
豊富:1

  1. 豊富な要素の後の要素の数が豊富さより少なくない最初のすべての順列を生成します:(例では、「a」は最後にはなりません)

abcd、abdc、acbd、acdb、adbc、adcb、bacd、badc、bcad、 bcda、bdac、 bDCA
cabd、cadb、cbad、 cbda、cdab、 cdba、dabc、dacb、abac、 dbca、dcab、 dcba

  1. 順列ごとに、次の規則に従って、繰り返し文字のセットを1つずつ挿入します。

5.1。これまでの順列での豊富な要素の最後の出現後、セットの豊富さが要素数よりも大きい場合は、次の順列にスキップします。
たとえば、これまでの順列がabcである場合、豊富な要素aを含むセットは、豊富度が2以下の場合にのみ挿入できます。したがって、aaaabcは大丈夫、aaaaabcは大丈夫です。

5.2。順列の最後の出現が最初になるセットから要素を選択します。
例:これまでの順列がabcbaで、セットがabの場合、bを選択

5.3。選択された要素を、順列の最後の出現の右側に少なくとも2箇所挿入します。
たとえば、bを置換babcaに挿入すると、結果はbabcbaおよびbabcab

5.4。結果の各順列とセットの残りでステップ5を繰り返します。

EXAMPLE:
set = abcaba
firsts = abc
repeats = aab

perm3  set    select perm4  set    select perm5  set    select perm6

abc    aab    a      abac   ab     b      ababc  a      a      ababac  
                                                               ababca  
                                          abacb  a      a      abacab  
                                                               abacba  
                     abca   ab     b      abcba  a      -
                                          abcab  a      a      abcaba  
acb    aab    a      acab   ab     a      acaba  b      b      acabab  
                     acba   ab     b      acbab  a      a      acbaba  
bac    aab    b      babc   aa     a      babac  a      a      babaca  
                                          babca  a      -
                     bacb   aa     a      bacab  a      a      bacaba  
                                          bacba  a      -  
bca    aab    -
cab    aab    a      caba   ab     b      cabab  a      a      cababa  
cba    aab    -

このアルゴリズムは、一意の順列を生成します。順列の総数(aを切り替えることができるためabaが2回カウントされる)を知りたい場合は、一意の順列の数に係数を掛けます。

F = N1! * N2! * ... * Nn

nは、セット内の各要素の出現回数です。セットabcdabcabaの場合、これは4になります! * 3! * 2! * 1!または288。これは、一意のアルゴリズムだけではなく、すべての順列を生成するアルゴリズムの非効率性を示しています。この場合のすべての順列をリストするには、一意の順列を288回だけリストしてください:-)

以下は、Javascriptでの(やや不器用な)実装です。 Pythonのような言語がこの種のものにより適している可能性があります。「abracadabra」の分離された順列を計算するためにコードスニペットを実行します。

// FIND ALL PERMUTATONS OF A SET WHERE NO ADJACENT ELEMENTS ARE IDENTICAL
function seperatedPermutations(set) {
    var unique = 0, factor = 1, firsts = [], repeats = [], abund;

    seperateRepeats(set);
    abund = abundance(repeats);
    permutateFirsts([], firsts);
    alert("Permutations of [" + set + "]\ntotal: " + (unique * factor) + ", unique: " + unique);

    // SEPERATE REPEATED CHARACTERS AND CALCULATE TOTAL/UNIQUE RATIO
    function seperateRepeats(set) {
        for (var i = 0; i < set.length; i++) {
            var first, elem = set[i];
            if (firsts.indexOf(elem) == -1) firsts.Push(elem)
            else if ((first = repeats.indexOf(elem)) == -1) {
                repeats.Push(elem);
                factor *= 2;
            } else {
                repeats.splice(first, 0, elem);
                factor *= repeats.lastIndexOf(elem) - first + 2;
            }
        }
    }

    // FIND ALL PERMUTATIONS OF THE FIRSTS USING RECURSION
    function permutateFirsts(perm, set) {
        if (set.length > 0) {
            for (var i = 0; i < set.length; i++) {
                var s = set.slice();
                var e = s.splice(i, 1);
                if (e[0] == abund.elem && s.length < abund.num) continue;
                permutateFirsts(perm.concat(e), s, abund);
            }
        }
        else if (repeats.length > 0) {
            insertRepeats(perm, repeats);
        }
        else {
            document.write(perm + "<BR>");
            ++unique;
        }
    }

    // INSERT REPEATS INTO THE PERMUTATIONS USING RECURSION
    function insertRepeats(perm, set) {
        var abund = abundance(set);
        if (perm.length - perm.lastIndexOf(abund.elem) > abund.num) {
            var sel = selectElement(perm, set);
            var s = set.slice();
            var elem = s.splice(sel, 1)[0];
            for (var i = perm.lastIndexOf(elem) + 2; i <= perm.length; i++) {
                var p = perm.slice();
                p.splice(i, 0, elem);
                if (set.length == 1) {
                    document.write(p + "<BR>");
                    ++unique;
                } else {
                    insertRepeats(p, s);
                }
            }
        }
    }

    // SELECT THE ELEMENT FROM THE SET WHOSE LAST OCCURANCE IN THE PERMUTATION COMES FIRST
    function selectElement(perm, set) {
        var sel, pos, min = perm.length;
        for (var i = 0; i < set.length; i++) {
            pos = perm.lastIndexOf(set[i]);
            if (pos < min) {
                min = pos;
                sel = i;
            }
        }
        return(sel);
    }

    // FIND ABUNDANT ELEMENT AND ABUNDANCE NUMBER
    function abundance(set) {
        if (set.length == 0) return ({elem: null, num: 0});
        var elem = set[0], max = 1, num = 1;
        for (var i = 1; i < set.length; i++) {
            if (set[i] != set[i - 1]) num = 1
            else if (++num > max) {
                max = num;
                elem = set[i];
            }
        }
        return ({elem: elem, num: 2 * max - set.length});
    }
}

seperatedPermutations(["a","b","r","a","c","a","d","a","b","r","a"]);
5
m69

アイデアは、最も一般的なものから最も一般的なものに要素をソートし、最も一般的なものを取り、その数を減らし、降順を維持してリストに戻すことです(ただし、可能な限り繰り返しを防ぐために最後に使用した要素を最初に置くことは避けてください) 。

これは、 Counter および bisect を使用して実装できます。

from collections import Counter
from bisect import bisect

def unsorted(lst):
    # use elements (-count, item) so bisect will put biggest counts first
    items = [(-count, item) for item, count in Counter(lst).most_common()]
    result = []

    while items:
        count, item = items.pop(0)
        result.append(item)
        if count != -1:
            element = (count + 1, item)
            index = bisect(items, element)
            # prevent insertion in position 0 if there are other items
            items.insert(index or (1 if items else 0), element)

    return result

>>> print unsorted([1, 1, 1, 2, 3, 3, 2, 2, 1])
[1, 2, 1, 2, 1, 3, 1, 2, 3]

>>> print unsorted([1, 2, 3, 2, 3, 2, 2])
[2, 3, 2, 1, 2, 3, 2]
4
enrico.bacis
  1. リストを並べ替えます。
  2. このアルゴリズム を使用して、リストの「最良のシャッフル」を生成します

元の場所のリストからアイテムの最小値を(アイテムの値によって)与えるため、たとえば、ソートされた位置から1、2、3を離そうとします。

2
Paddy3118

「私も」スタイルの回答はお許しください。 Coadyの回答 これに簡略化できませんでしたか?

from collections import Counter
from heapq import heapify, heappop, heapreplace
from itertools import repeat

def srgerg(data):
    heap = [(-freq+1, value) for value, freq in Counter(data).items()]
    heapify(heap)

    freq = 0
    while heap:
        freq, val = heapreplace(heap, (freq+1, val)) if freq else heappop(heap)
        yield val
    yield from repeat(val, -freq)

Edit:python 2リストを返すバージョン:

def srgergpy2(data):
    heap = [(-freq+1, value) for value, freq in Counter(data).items()]
    heapify(heap)

    freq = 0
    result = list()
    while heap:
        freq, val = heapreplace(heap, (freq+1, val)) if freq else heappop(heap)
        result.append(val)
    result.extend(repeat(val, -freq))
    return result
2
srgerg

長さnのソート済みリストから始めます。 m = n/2とします。 0、m、1、m + 1、2、m + 2などの値を取得します。数字の半分以上が同じでない限り、連続した順序で同等の値を取得することはありません。

2
rchuso
  1. 各値が現れる回数を数える
  2. 最も頻度の高い順に値を選択します
  3. 選択した値を最終出力に追加し、毎回インデックスを2ずつ増やします
  4. インデックスが範囲外の場合、インデックスを1にリセットします
from heapq import heapify, heappop
def distribute(values):
    counts = defaultdict(int)
    for value in values:
        counts[value] += 1
    counts = [(-count, key) for key, count in counts.iteritems()]
    heapify(counts)
    index = 0
    length = len(values)
    distributed = [None] * length
    while counts:
        count, value = heappop(counts)
        for _ in xrange(-count):
            distributed[index] = value
            index = index + 2 if index + 2 < length else 1
    return distributed
2
Joel