Como implementar um shuffle ponderado

22

Recentemente, escrevi um código que considerava muito ineficiente, mas como ele incluía apenas alguns valores, aceitei. No entanto, ainda estou interessado em um algoritmo melhor para o seguinte:

  1. Uma lista de objetos X, a cada um deles é atribuído um "peso"
  2. Resuma os pesos
  3. Gere um número aleatório de 0 à soma
  4. Iterar através dos objetos, subtraindo seu peso da soma até que a soma não seja positiva
  5. Remova o objeto da lista e adicione-o ao final da nova lista

Os itens 2,4 e 5 levam ntempo e, portanto, é um O(n^2)algoritmo.

Isso pode ser melhorado?

Como exemplo de uma reprodução aleatória ponderada, um elemento tem uma chance maior de estar na frente com um peso maior.

Exemplo (gerarei números aleatórios para torná-lo real):

6 objetos com pesos 6,5,4,3,2,1; Soma é 21

Escolhi 19: 19-6-5-4-3-2 = -1assim, 2 vai na primeira posição, os pesos agora são 6,5,4,3,1; Soma é 19

Escolhi 16: 16-6-5-4-3 = -2assim, 3 vai para a segunda posição, agora os pesos são 6,5,4,1; Soma é 16

Escolhi 3: 3-6 = -3assim 6 passa para a terceira posição, agora os pesos são 5,4,1; Soma é 10

Escolhi 8: 8-5-4 = -1assim, 4 vai para a quarta posição, os pesos agora são 5,1; Soma é 6

Escolhi 5: 5-5=0assim 5 passa para a quinta posição, os pesos agora são 1; Soma é 1

Escolhi 1:, 1-1=0assim, 1 vai para a última posição, não tenho mais pesos, termino

Nathan Merrill
fonte
6
O que exatamente é um shuffle ponderado? Isso significa que quanto maior o peso, maior a probabilidade de o objeto estar no topo do convés?
Doval 25/03
Por curiosidade, qual é o objetivo da etapa (5). Existem maneiras de melhorar isso se a lista for estática.
Gort the Robot
Sim, Doval. Eu removo o item da lista para que ele não apareça na lista aleatória mais de uma vez.
Nathan Merrill
O peso de um item da lista é constante?
Um item terá um peso maior que outro, mas o item X sempre terá o mesmo peso. (Obviamente, se você remover itens, o peso maior vai se tornar maior em proporção)
Nathan Merrill

Respostas:

14

Isso pode ser implementado O(n log(n))usando uma árvore.

Primeiro, crie a árvore, mantendo em cada nó a soma cumulativa de todos os nós descendentes à direita e à esquerda de cada nó.

Para obter uma amostra de um item, faça uma amostragem recursiva do nó raiz, usando as somas cumulativas para decidir se você retorna o nó atual, um nó da esquerda ou um nó da direita. Toda vez que você amostrar um nó, defina seu peso como zero e também atualize os nós pais.

Esta é a minha implementação em Python:

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

Uso:

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é um gerador, para que você possa experimentar os principais kitens com eficiência. Se você deseja embaralhar toda a matriz, basta percorrer o gerador até a exaustão (usando a listfunção).

ATUALIZAR:

A amostragem aleatória ponderada (2005; Efraimidis, Spirakis) fornece um algoritmo muito elegante para isso. A implementação é super simples e também é executada em 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]
jbochi
fonte
A última atualização parece estranhamente semelhante a uma solução incorreta de uma linha . Tem certeza de que está correto?
Giacomo Alzetta
19

Edição: Esta resposta não interpreta os pesos da maneira que seria de esperar. Ou seja, um item com peso 2 não tem duas vezes mais chances de ser o primeiro que um com peso 1.

Uma maneira de embaralhar uma lista é atribuir números aleatórios a cada elemento da lista e classificar por esses números. Podemos estender essa ideia, basta escolher números aleatórios ponderados. Por exemplo, você poderia usar random() * weight. Escolhas diferentes produzirão distribuições diferentes.

Em algo como Python, isso deve ser tão simples quanto:

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

Cuidado para não avaliar as chaves mais de uma vez, pois elas acabam com valores diferentes.

Winston Ewert
fonte
2
Isso é honestamente genial devido à sua simplicidade. Supondo que você esteja usando um algoritmo de classificação nlogn, isso deve funcionar bem.
Nathan Merrill
Qual é o peso dos pesos? Se eles são altos, os objetos são simplesmente classificados por peso. Se eles são baixos, os objetos são quase aleatórios, com apenas pequenas perturbações de acordo com o peso. De qualquer maneira, esse método que eu sempre usei, mas o cálculo da posição de classificação provavelmente precisará de alguns ajustes.
David.pfx
@ david.pfx O intervalo dos pesos deve ser o intervalo dos números aleatórios. Dessa forma max*min = min*max, e, portanto, qualquer permutação é possível, mas alguns são muito mais provável (especialmente se os pesos não são uniformemente spread)
Nathan Merrill
2
Na verdade, essa abordagem está errada! Imagine os pesos 75 e 25. Para o caso 75, 2/3 do tempo escolherá um número> 25. Nos 1/3 restantes do tempo, ele "vencerá" os 25 50% do tempo. 75 serão os primeiros 2/3 + (1/3 * 1/2) do tempo: 83%. Ainda não resolvemos a correção.
Adam Rabung
1
Esta solução deve funcionar substituindo a distribuição uniforme da amostragem aleatória por uma distribuição exponencial.
P-Gn
5

Primeiro, vamos trabalhar para que o peso de um determinado elemento na lista a ser classificada seja constante. Não vai mudar entre iterações. Se isso acontecer, então ... bem, isso é um problema maior.

Para ilustração, vamos usar um baralho de cartas onde queremos ponderar as cartas de face para a frente. weight(card) = card.rank. Resumindo, se não sabemos que a distribuição de pesos é de fato O (n) uma vez.

Esses elementos são armazenados em uma estrutura classificada, como uma modificação em uma lista de pulos indexáveis, de modo que todos os índices dos níveis possam ser acessados ​​a partir de um determinado nó:

   1 10
 o ---> o -------------------------------------------- -------------> o Nível superior
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> o Nível 3
   1 2 1 2 5
 O ---> o ---------> o ---> o ---------> o ----------------- ----------> o Nível 2
   1 1 1 1 1 1 1 1 1 1 1 
 O ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o Nível inferior

Cabeça 1 2 2 3 4 5 6 7 8 8 9 10 10 NIL
      Nó Nó Nó Nó Nó Nó Nó Nó Nó Nó

No entanto, nesse caso, cada nó também 'ocupa' tanto espaço quanto seu peso.

Agora, ao procurar um cartão nesta lista, é possível acessar sua posição na lista no horário O (log n) e removê-lo das listas associadas no horário O (1). Ok, pode não ser O (1), pode ser O (log log n) tempo (eu teria que pensar muito mais sobre isso). A remoção do sexto nó no exemplo acima envolveria a atualização dos quatro níveis - e esses quatro níveis são independentes de quantos elementos existem na lista (dependendo de como você implementa os níveis).

Como o peso de um elemento é constante, pode-se simplesmente fazer sum -= weight(removed)sem precisar atravessar a estrutura novamente.

E, portanto, você tem um custo único de O (n) e um valor de pesquisa de O (log n) e um custo de remoção da lista de O (1). Isso se torna O (n) + n * O (log n) + n * O (1), o que fornece um desempenho geral de O (n log n).


Vamos analisar isso com cartões, porque foi o que eu usei acima.

      10
top 3 -----------------------> 4d
                                .
       3 7
    2 ---------> 2d ---------> 4d
                  . .
       1 2 3 4
bot 1 -> Anúncio -> 2d -> 3d -> 4d

Este é um baralho realmente pequeno, com apenas 4 cartas. Deve ser fácil ver como isso pode ser estendido. Com 52 cartas, uma estrutura ideal teria 6 níveis (log 2 (52) ~ = 6), embora, se você procurar nas listas de pulos, mesmo isso possa ser reduzido a um número menor.

A soma de todos os pesos é 10. Portanto, você obtém um número aleatório de [1 .. 10) e seus 4 Você percorre a lista de pulos para encontrar o item que está no teto (4). Como 4 é menor que 10, você passa do nível superior para o segundo nível. Quatro é maior que 3, então agora estamos no 2 dos diamantes. 4 é menor que 3 + 7, então passamos para o nível inferior e 4 é menor que 3 + 3, então temos um 3 de diamantes.

Após remover os 3 diamantes da estrutura, a estrutura agora se parece com:

       7
top 3 ----------------> 4d
                         .
       3 4
    2 ---------> 2d -> 4d
                  . .
       1 2 4)
bot 1 -> Anúncio -> 2d -> 4d

Você observará que os nós ocupam uma quantidade de 'espaço' proporcional ao seu peso na estrutura. Isso permite a seleção ponderada.

Como isso se aproxima de uma árvore binária equilibrada, a pesquisa nela não precisa percorrer a camada inferior (que seria O (n)) e, em vez disso, sair do topo permite pular rapidamente a estrutura para descobrir o que você está procurando para.

Muito disso poderia ser feito com algum tipo de árvore equilibrada. O problema é que o reequilíbrio da estrutura quando um nó é removido fica confuso, já que essa não é uma estrutura de árvore clássica e a limpeza deve lembrar que os 4 diamantes agora são movidos das posições [6 7 8 9] para [3 4 5 6] pode custar mais do que os benefícios da estrutura da árvore.

No entanto, enquanto a lista de pulos se aproxima de uma árvore binária em sua capacidade de pular a lista em O (log n), ela tem a simplicidade de trabalhar com uma lista vinculada.

Isso não quer dizer que seja fácil fazer tudo isso (você ainda precisa manter o controle de todos os links que precisa modificar ao remover um elemento), mas isso significa apenas atualizar todos os níveis que você tem e os links deles. do que tudo à direita na estrutura apropriada da árvore.


fonte
Não tenho certeza como o que você está descrevendo partidas uma lista SKIP (mas, em seguida, eu tinha apenas olhar para cima salto listas). Pelo que entendi na Wikipedia, os pesos mais altos estariam mais à direita do que os pesos mais baixos. No entanto, você está descrevendo que a largura dos saltos deve ser o peso. Uma outra pergunta ... usando essa estrutura, como você escolhe um elemento aleatório?
Nathan Merrill
1
@MrTi, portanto, a modificação da ideia de uma lista de pulos indexáveis. A chave é poder acessar o elemento no local em que o peso dos elementos anteriores é somado a <23 no tempo O (log n) em vez do tempo O (n). Você ainda escolhe o elemento aleatório da maneira que estava descrevendo, seleciona um número aleatório entre [0, soma (pesos)] e, em seguida, obtém o elemento correspondente da lista. Não importa em que ordem os nós / cartões estão na lista de pulos - porque o maior espaço ocupado pelos itens mais pesados ​​é a chave.
Ah, eu entendi. Eu gosto disso.
Nathan Merrill