가중 셔플을 구현하는 방법


22

최근에 비효율적이라고 생각되는 코드를 작성했지만 몇 가지 값만 포함했기 때문에 승인했습니다. 그러나 여전히 다음과 같은 더 나은 알고리즘에 관심이 있습니다.

  1. X 객체의 목록. 각 객체에는 "무게"가 할당됩니다.
  2. 무게 요약
  3. 0에서 합계까지 난수 생성
  4. 합계가 양수가 아닌 때까지 합계에서 가중치를 빼고 객체를 반복합니다.
  5. 목록에서 개체를 제거한 다음 새 목록의 끝에 추가하십시오.

항목 2, 4 및 5는 모두 n시간 이 걸리므로 O(n^2)알고리즘입니다.

이것을 개선 할 수 있습니까?

가중 셔플의 예로서, 요소는 가중이 높은 전방에있을 가능성이 더 높다.

예 (난수를 생성하여 현실로 만들 것입니다) :

무게가 6,5,4,3,2,1 인 6 개의 물체; 합계는 21입니다

나는 19를 골랐다. 19-6-5-4-3-2 = -1따라서 2는 첫 번째 위치에 있으며 가중치는 이제 6,5,4,3,1이다. 합계는 19입니다

나는 16을 골랐다. 16-6-5-4-3 = -2따라서 3은 두 번째 위치로 간다. 무게는 이제 6,5,4,1이다. 합계는 16

나는 3을 골랐다. 3-6 = -3따라서 6은 3 위를 차지하고 무게는 5,4,1이다. 합계는 10

나는 8을 ​​골랐다. 8-5-4 = -1따라서 4는 4 번째 위치에 있으며, 가중치는 5,1이다. 합계는 6

나는 5를 골랐다. 5-5=0따라서 5는 5 번째 위치에 있으며, 가중치는 1이다. 합계는 1

나는 1을 골랐다. 1-1=0따라서 1은 마지막 위치에 간다. 나는 더 이상 무게가 없다.


6
가중치 셔플은 정확히 무엇입니까? 무게가 높을수록 물체가 데크의 상단에있을 가능성이 더 높습니까?
Doval

호기심에서 단계 (5)의 목적은 무엇입니까? 목록이 정적 인 경우이를 개선 할 수있는 방법이 있습니다.
로봇 고트

예, 도발 셔플 된 목록에 두 번 이상 표시되지 않도록 목록에서 항목을 제거했습니다.
Nathan Merrill

목록에있는 항목의 무게가 일정합니까?

한 품목의 무게가 다른 품목보다 크지 만 X 품목의 무게는 항상 같습니다. (물론 아이템을 제거하면 더 큰 무게가 비례 적으로 커질 것입니다)
Nathan Merrill

답변:


13

이것은 O(n log(n))트리 를 사용하여 구현할 수 있습니다 .

먼저, 각 노드에서 각 노드의 오른쪽과 왼쪽에있는 모든 하위 노드의 누적 합계를 유지하면서 트리를 만듭니다.

항목을 샘플링하려면 누적 합계를 사용하여 루트 노드에서 재귀 적으로 샘플링하여 현재 노드, 왼쪽에서 노드 또는 오른쪽에서 노드를 반환할지 여부를 결정합니다. 노드를 샘플링 할 때마다 가중치를 0으로 설정하고 부모 노드도 업데이트하십시오.

이것은 파이썬에서 구현 한 것입니다.

import random

def weigthed_shuffle(items, weights):
    if len(items) != len(weights):
        raise ValueError("Unequal lengths")

    n = len(items)
    nodes = [None for _ in range(n)]

    def left_index(i):
        return 2 * i + 1

    def right_index(i):
        return 2 * i + 2

    def total_weight(i=0):
        if i >= n:
            return 0
        this_weigth = weights[i]
        if this_weigth <= 0:
            raise ValueError("Weigth can't be zero or negative")
        left_weigth = total_weight(left_index(i))
        right_weigth = total_weight(right_index(i))
        nodes[i] = [this_weigth, left_weigth, right_weigth]
        return this_weigth + left_weigth + right_weigth

    def sample(i=0):
        this_w, left_w, right_w = nodes[i]
        total = this_w + left_w + right_w
        r = total * random.random()
        if r < this_w:
            nodes[i][0] = 0
            return i
        elif r < this_w + left_w:
            chosen = sample(left_index(i))
            nodes[i][1] -= weights[chosen]
            return chosen
        else:
            chosen = sample(right_index(i))
            nodes[i][2] -= weights[chosen]
            return chosen

    total_weight() # build nodes tree

    return (items[sample()] for _ in range(n - 1))

용법:

In [2]: items = list(range(10))
   ...: weights = list(range(10, 0, -1))
   ...:

In [3]: for _ in range(10):
   ...:     print(list(weigthed_shuffle(items, weights)))
   ...:
[5, 0, 8, 6, 7, 2, 3, 1, 4]
[1, 2, 5, 7, 3, 6, 9, 0, 4]
[1, 0, 2, 6, 8, 3, 7, 5, 4]
[4, 6, 8, 1, 2, 0, 3, 9, 7]
[3, 5, 1, 0, 4, 7, 2, 6, 8]
[3, 7, 1, 2, 0, 5, 6, 4, 8]
[1, 4, 8, 2, 6, 3, 0, 9, 5]
[3, 5, 0, 4, 2, 6, 1, 8, 9]
[6, 3, 5, 0, 1, 2, 4, 8, 7]
[4, 1, 2, 0, 3, 8, 6, 5, 7]

weigthed_shuffle생성기이므로 최상위 k항목을 효율적으로 샘플링 할 수 있습니다 . 전체 배열을 섞으려면 고갈 될 때까지 ( list함수 사용) 생성기를 반복하십시오 .

최신 정보:

가중 랜덤 샘플링 (2005; Efraimidis, Spirakis)은이를 위한 매우 우아한 알고리즘을 제공합니다. 구현은 매우 간단하며 다음과 같이 실행됩니다 O(n log(n)).

def weigthed_shuffle(items, weights):
    order = sorted(range(len(items)), key=lambda i: -random.random() ** (1.0 / weights[i]))
    return [items[i] for i in order]

마지막 업데이트 는 잘못된 one-liner 솔루션 과 매우 유사합니다 . 맞습니까?
Giacomo Alzetta

19

편집 : 이 답변은 예상대로 가중치를 해석하지 않습니다. 즉, 무게가 2 인 품목은 무게가 1 인 품목의 두 배가되지 않습니다.

목록을 셔플하는 한 가지 방법은 목록의 각 요소에 임의의 숫자를 할당하고 해당 숫자를 기준으로 정렬하는 것입니다. 우리는 그 아이디어를 확장 할 수 있습니다. 우리는 가중 난수를 선택해야합니다. 예를 들어을 사용할 수 있습니다 random() * weight. 다른 선택은 다른 분포를 생성합니다.

파이썬과 같은 경우 다음과 같이 간단해야합니다.

items.sort(key = lambda item: random.random() * item.weight)

키가 다른 값으로 끝나므로 키를 한 번 이상 평가하지 않도록주의하십시오.


2
이것은 단순하기 때문에 솔직히 천재입니다. nlogn 정렬 알고리즘을 사용한다고 가정하면 잘 작동합니다.
Nathan Merrill

무게의 무게는 얼마입니까? 그것들이 높으면, 물체는 단순히 무게로 분류됩니다. 그것들이 낮 으면 물체는 무게에 따라 약간의 섭동만으로 거의 임의적입니다. 어느 쪽이든,이 방법은 항상 사용했지만 정렬 위치 계산에는 약간의 조정이 필요할 것입니다.
david.pfx

@ david.pfx 가중치 범위는 임의의 숫자 범위 여야합니다. 그렇게하면 max*min = min*max모든 순열이 가능하지만 일부는 훨씬 더 가능성이 높습니다 (특히 가중치가 균일하게 분산되지 않은 경우)
Nathan Merrill

2
실제로이 방법은 잘못되었습니다! 가중치 75와 25를 상상해보십시오. 75의 경우 2/3는 숫자> 25를 선택합니다. 나머지 1/3의 시간은 25의 50 %를 "이길"것입니다. 75는 시간의 처음 2/3 + (1/3 * 1/2)입니다 : 83 %. 아직 해결되지 않았습니다.
Adam Rabung

1
이 솔루션은 랜덤 샘플링의 균일 분포를 지수 분포로 대체하여 작동합니다.
P-Gn

5

먼저, 정렬 할 목록에서 주어진 요소의 가중치가 일정하다는 점에서 작업 해 봅시다. 반복간에 변경되지는 않습니다. 그렇다면 ... 더 큰 문제입니다.

예를 들어 앞면에 페이스 카드에 가중치를 부여하려는 데크 카드를 사용할 수 있습니다. weight(card) = card.rank. 우리가 가중치의 분포를 모른다면 이것을 합산하면 실제로 O (n)입니다.

이 요소들은 주어진 노드에서 레벨의 모든 인덱스에 액세스 할 수 있도록 인덱서 블 스킵 목록 의 수정과 같은 정렬 된 구조로 저장됩니다 .

   1 10
 o ---> o -------------------------------------------- -------------> o 최상위
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o 레벨 3
   12 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o 레벨 2
   11 11 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o 최하위

헤드 1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th NIL
      노드 노드 노드 노드 노드 노드 노드 노드 노드 노드

그러나이 경우 각 노드는 무게만큼의 공간을 차지합니다.

이제이 목록에서 카드를 찾을 때 O (log n) 시간의 목록에서 해당 위치에 액세스하여 O (1) 시간의 관련 목록에서 제거 할 수 있습니다. 좋아, O (1)이 아닐 수도 있고 O (log log n) 시간 일 수도 있습니다 (이에 대해 더 많이 생각해야 할 것입니다). 위 예제에서 6 번째 노드를 제거하면 4 가지 레벨 모두가 업데이트됩니다. 4 가지 레벨은 레벨에 구현 된 방법에 따라 목록에있는 요소 수와 무관합니다.

요소의 무게가 일정 sum -= weight(removed)하기 때문에 구조를 다시 횡단 하지 않고도 간단하게 할 수 있습니다 .

따라서 일회성 비용 O (n), 조회 값 O (log n) 및 목록 비용 O (1)에서 제거됩니다. 이것은 O (n) + n * O (log n) + n * O (1)이되어 O (n log n)의 전체 성능을 제공합니다.


내가 위에서 사용한 것이기 때문에 이것을 카드로 볼 수 있습니다.

      10
상위 3 -----------------------> 4d
                                .
       3 7.
    2 ---------> 2d ---------> 4d
                  . .
       1 2. 3 4.
봇 1-> 광고-> 2d-> 3d-> 4d

이것은 단지 4 장의 카드 만 있는 정말 작은 데크입니다. 이것이 어떻게 확장 될 수 있는지 쉽게 알 수 있어야합니다. 52 개의 카드를 사용하면 이상적인 구조는 6 단계 (로그 2 (52) ~ = 6)가되지만 건너 뛰기 목록을 파는 경우에도 더 적은 수로 줄일 수 있습니다.

모든 가중치의 합은 10입니다. 따라서 [1 .. 10) 및 4 에서 임의의 숫자를 얻습니다. 건너 뛰기 목록을 걸어서 ceiling (4)에있는 항목을 찾습니다. 4가 10보다 작으므로 최상위 레벨에서 두 번째 레벨로 이동합니다. 4는 3보다 큽니다. 이제 우리는 다이아몬드 2에 있습니다. 4는 3 + 7보다 작습니다. 그래서 우리는 최하위 수준으로 내려 가고 4는 3 + 3보다 작습니다. 그래서 우리는 3 개의 다이아몬드를가집니다.

구조에서 다이아몬드 3 개를 제거한 후 구조는 다음과 같습니다.

       7
톱 3 ----------------> 4d
                         .
       3 4.
    2 ---------> 2d-> 4d
                  . .
       1 2. 4.
봇 1-> 광고-> 2d-> 4d

노드는 구조에서 가중치에 비례하여 '공간'을 차지합니다. 이것은 가중치 선택을 허용합니다.

이것은 균형 잡힌 이진 트리와 비슷 하기 때문에, 이것의 조회는 맨 아래 레이어 (O (n) 일 것입니다)를 걸을 필요가 없으며 대신 맨 위에서 올라가면 찾고있는 것을 찾기 위해 구조를 빠르게 건너 뛸 수 있습니다 에 대한.

이 중 상당수는 대신 일종의 균형 잡힌 나무로 수행 할 수 있습니다. 노드가 제거 될 때 구조가 재조정되는 문제는 이것이 고전적인 트리 구조가 아니기 때문에 혼동을 일으켜 혼란에 빠졌습니다. 4 개의 다이아몬드가 현재 위치 [6 7 8 9]에서 [3 4 5 6]은 나무 구조의 이점보다 더 많은 비용이들 수 있습니다.

그러나 건너 뛰기 목록은 O (log n) 시간 내에 목록을 건너 뛸 수있는 이진 트리와 비슷하지만 대신 연결된 목록으로 작업하는 것이 간단합니다.

이것은이 모든 것을 쉽게 수행 할 있다고 말하는 것은 아닙니다 (요소를 제거 할 때 수정 해야하는 모든 링크에 탭을 유지해야 함). 그러나 많은 레벨과 링크를 업데이트해야합니다. 올바른 트리 구조의 오른쪽에있는 모든 것보다


나는 아니에요 확인 방법이 일치에게 건너 뛰기 목록을 설명하고 있습니다 (하지만, 내가 무슨 않았다 그냥 스킵리스트를 볼). 내가 Wikipedia에서 이해하는 것에서, 높은 가중치는 낮은 가중치보다 오른쪽에 더 가깝습니다. 그러나 건너 뛰기의 너비는 가중치 여야한다고 설명하고 있습니다. 또 다른 질문은 ...이 구조를 사용하여 무작위 요소를 어떻게 선택합니까?
Nathan Merrill

1
따라서 @MrTi 는 인덱싱 가능한 스킵 목록을 수정 했습니다. 핵심은 이전 요소의 가중치가 O (n) 시간이 아닌 O (log n) 시간에서 <23으로 합산되는 요소에서 요소에 액세스 할 수 있어야한다는 것입니다. 설명하는 방식으로 임의의 요소를 선택하고 [0, sum (weights)]에서 임의의 숫자를 선택한 다음 목록에서 해당 요소를 가져옵니다. 더 무거운 가중치 항목이 차지하는 '공간'이 더 중요하기 때문에 건너 뛰기 목록에 노드 / 카드의 순서가 중요하지 않습니다.

아 이해합니다 나는 그것을 좋아한다.
Nathan Merrill
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.