Як реалізувати зважене перетасування


22

Нещодавно я написав якийсь код, який вважав дуже неефективним, але оскільки він містив лише кілька значень, я його прийняв. Однак мене все ще цікавить кращий алгоритм для наступного:

  1. Список X-об’єктів, кожному з яких присвоюється "вага"
  2. Підсумуйте ваги
  3. Утворіть випадкове число від 0 до суми
  4. Ітерайте через об’єкти, віднімаючи їх вагу від суми, поки сума не буде додатною
  5. Видаліть об’єкт зі списку, а потім додайте його до кінця нового списку

Пункти 2,4 і 5 потребують nчасу, і це O(n^2)алгоритм.

Чи можна це покращити?

Як приклад зваженого переміщення, елемент має більший шанс опинитися на фронті з більшою вагою.

Приклад (я генерую випадкові числа, щоб це стало реальним):

6 предметів з вагою 6,5,4,3,2,1; Сума - 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 переходить на третю позицію, вага зараз 5,4,1; Сума - 10

Я вибрав 8:, 8-5-4 = -1таким чином, 4 виходить на четверту позицію, вага зараз 5,1; Сума - 6

Я вибрав 5:, 5-5=0таким чином, 5 йде на п'яту позицію, ваги зараз 1; Сума - 1

Я вибрав 1:, 1-1=0таким чином, 1 йде в останню позицію, у мене більше немає ваг, я закінчую


6
Що саме таке зважене переміщення? Чи означає це, що чим більша вага, тим більша ймовірність того, що предмет буде у верхній частині колоди?
Довал

З цікавості, яка мета кроку (5). Є способи покращити це, якщо список статичний.
Gort the Robot

Так, Довал. Я вилучаю елемент зі списку, щоб він не з’являвся у перетасованому списку більше одного разу.
Натан Меррілл

Чи постійна вага елемента в списку?

Один предмет матиме більшу вагу, ніж інший, але предмет X завжди матиме однакову вагу. (Очевидно, якщо ви виймете предмети, більша вага зростатиме пропорційно)
Натан Меррілл,

Відповіді:


14

Це можна реалізувати у O(n log(n))використанні дерева.

Спочатку створіть дерево, зберігаючи в кожному вузлі сукупну суму всіх низхідних вузлів праворуч і ліворуч від кожного вузла.

Для вибірки елемента рекурсивно вибирайте з кореневого вузла, використовуючи сукупні суми, щоб вирішити, повернути чи поточний вузол, вузол зліва або вузол справа. Кожен раз, коли ви відбираєте зразок вузла, встановлюйте його вагу до нуля, а також оновлюйте батьківські вузли.

Це моя реалізація в 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))

Використання:

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]

Останнє оновлення здається моторошним подібним до неправильного рішення з однолінійним каналом . Ви впевнені, що це правильно?
Джакомо Альзетта

19

EDIT: Ця відповідь не тлумачить ваги так, як можна було б очікувати. Тобто предмет із вагою 2 не є вдвічі більшим, ніж перший із вагою 1.

Один із способів перетасувати список - присвоїти випадкові числа кожному елементу у списку та сортувати за цими числами. Ми можемо розширити цю ідею, нам просто потрібно вибрати зважені випадкові числа. Наприклад, ви можете використовувати random() * weight. Різний вибір призведе до різних розподілів.

У чомусь на зразок Python це має бути таким же простим, як:

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

Будьте обережні, щоб не оцінювати ключі більше одного разу, оскільки вони виявляться з різними значеннями.


2
Це чесно геніально завдяки своїй простоті. Якщо припустити, що ви використовуєте алгоритм сортування nlogn, це має працювати добре.
Натан Меррілл

Яка вага ваг? Якщо вони високі, предмети просто сортують за вагою. Якщо вони низькі, об'єкти майже випадкові з лише невеликими збуреннями відповідно до ваги. Так чи інакше, цей метод я завжди використовував, але для розрахунку позиції сортування, ймовірно, знадобиться певна настройка.
david.pfx

@ david.pfx Діапазон ваг повинен бути діапазоном випадкових чисел. Таким чином max*min = min*max, і тому будь-яка перестановка можлива, але деякі є набагато більш імовірними (особливо якщо ваги не рівномірно розподілені)
Натан Меррілл,

2
Власне, такий підхід неправильний! Уявіть ваги 75 та 25. У випадку 75, у 2/3 часу він вибере число> 25. За решту 1/3 часу він "биє" 25 50% часу. 75 буде першим 2/3 + (1/3 * 1/2) часу: 83%. Ще не відпрацьовано виправлення.
Адам Рабунг

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 рівень
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> o 2 рівень
   1 1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o Нижній рівень

Голова 1-го 2-го 3-го 4-го 5-го 6-го 7-го 8-го 9-го 10-го НІЛ
      Node Node Node Node Node Node Node Node Node Node Node

Однак у цьому випадку кожен вузол також займає стільки місця, скільки і його вага.

Тепер, шукаючи карту у цьому списку, ви можете отримати доступ до її позиції у списку за O (log n) час та вилучити її із асоційованих списків за O (1) час. Гаразд, це може бути не O (1), це може бути час O (журнал журналу n) (я повинен би подумати над цим набагато більше). Видалення 6-го вузла у наведеному вище прикладі передбачає оновлення всіх чотирьох рівнів - і ці чотири рівні не залежать від кількості елементів у списку (залежно від того, як ви реалізуєте рівні).

Оскільки вага елемента є постійною, можна просто обійтися 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 -----------------------> 4д
                                .
       3 7.
    2 ---------> 2d ---------> 4d
                  . .
       1 2. 3 4.
бот 1 -> Оголошення -> 2d -> 3d -> 4d

Це дійсно невелика колода з лише 4 картками. Слід легко зрозуміти, як це можна продовжити. Із 52 карт ідеальною структурою було б 6 рівнів (журнал 2 (52) ~ = 6), хоча якщо ви зануритесь у пропускні списки, навіть це може бути зменшено до меншої кількості.

Сума всіх ваг дорівнює 10. Отже, ви отримуєте випадкове число з [1 .. 10) та його 4. Ви переходите до списку пропусків, щоб знайти предмет, який знаходиться на стелі (4). Оскільки 4 менше 10, ви переходите від верхнього рівня до другого рівня. Чотири більше, ніж 3, тому зараз ми на 2 алмазах. 4 менше 3 + 7, тому ми переходимо до нижнього рівня, а 4 - менше 3 + 3, тому у нас є 3 алмази.

Після вилучення 3-х алмазів зі структури тепер структура виглядає так:

       7
верхній 3 ----------------> 4д
                         .
       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) час, він має простоту роботи із пов'язаним списком.

Це не означає, що це легко зробити (все-таки потрібно тримати вкладки на всіх посиланнях, які потрібно змінити, коли ви видаляєте елемент), але це означає лише оновлення незалежно від багатьох рівнів та їх посилань. ніж усе праворуч на правильній структурі дерева.


Я не знаю , як то , що ви описуєте матчі список Пропустити (але потім, я ж просто подивитися пропуском списки). З того, що я розумію у Вікіпедії, більш зважений буде більше праворуч, ніж нижчі ваги. Однак ви описуєте, що ширина пропусків повинна бути вагою. Ще одне питання ... використовуючи цю структуру, як ви вибираєте випадковий елемент?
Натан Меррілл

1
@MrTi, таким чином, модифікація ідеї про індексаційний пропускний список. Ключ полягає в тому, щоб мати змогу отримати доступ до елемента там, де вага попередніх елементів підсумовується до <23 за час O (log n), а не час O (n). Ви все одно вибираєте випадковий елемент так, як ви описували, вибираєте випадкове число з [0, сума (ваги)] і потім отримуєте відповідний елемент зі списку. Не має значення, в якому порядку розміщуються вузли / картки у списку пропусків - тому що більший "простір", який займають більш важкі зважені елементи, є ключовим.

Ах, я розумію. Мені це подобається.
Натан Меррілл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.