Что, если вообще что-то не так с этим алгоритмом тасования, и как я могу узнать? - PullRequest
74 голосов
/ 15 октября 2010

Как фон, я знаю о совершенном шаффле Фишера-Йейтса .Это отличный случай с его O (n) сложностью и гарантированной однородностью, и я был бы дураком не использовать его ... в среде, которая позволяет обновлять массивы на месте (так, в большинстве, если не во всех, императив среды программирования).

К сожалению, мир функционального программирования не дает вам доступа к изменяемому состоянию.

Однако из-за Фишера-Йейтса не так многолитературы я могу найти о том, как разработать алгоритм тасования.Те немногие места, где это решается, делают это кратко, прежде чем сказать, по сути, «так вот Фишер-Йейтс, это все те перетасовки, которые вам нужно знать».В конце концов мне пришлось придумать свое собственное решение.

Решение, которое я придумал, работает следующим образом, чтобы перетасовать любой список данных:

  • Если списокпусто, вернуть пустой набор.
  • Если в списке есть один элемент, вернуть этот единственный элемент.
  • Если список не пустой, разбить список на генератор случайных чисел и применитьалгоритм рекурсивно обращается к каждому разделу, собирая результаты.

В коде Erlang это выглядит примерно так:

shuffle([])  -> [];
shuffle([L]) -> [L];
shuffle(L)   ->
  {Left, Right} = lists:partition(fun(_) -> 
                                    random:uniform() < 0.5 
                                  end, L),
  shuffle(Left) ++ shuffle(Right).

(Если это выглядит как ненормальная быстрая сортировка для вас,в общем, вот что это такое.)

Итак, вот моя проблема: та же самая ситуация, которая затрудняет поиск алгоритмов тасования, которые не являются сложными по Фишеру-Йейтсу, затрудняет поиск инструментов для анализа тасованияАлгоритм одинаково сложен.Я могу найти много литературы по анализу PRNG на предмет однородности, периодичности и т. Д., Но не так много информации о том, как анализировать случайные числа.(Действительно, некоторая информация, которую я нашел при анализе тасовок, была просто неверной - ее легко обмануть с помощью простых методов.)

Поэтому мой вопрос заключается в следующем: как мне проанализировать мой алгоритм тасования (при условии, что random:uniform()вызов там есть до задачи генерации соответствующих случайных чисел с хорошими характеристиками)?Какие математические инструменты есть в моем распоряжении, чтобы судить, скажем, 100 000 прогонов тасовщика по списку целых чисел в диапазоне от 1 до 100 дали мне правдоподобно хорошие результаты тасования?Я провел несколько собственных тестов (например, сравнивая приращения с приращениями в случайном порядке), но я хотел бы знать еще несколько.

И если есть какое-то понимание самого этого алгоритма перемешиванияэто тоже будет оценено.

Ответы [ 4 ]

73 голосов
/ 17 октября 2010

Общее замечание

Мой личный подход к правильности алгоритмов, использующих вероятность: если вы знаете, как доказать , это правильно, то, вероятно, это правильно;если вы этого не сделаете, это, безусловно, неправильно.

Иными словами, как правило, безнадежно пытаться проанализировать каждый алгоритм, который вы можете придумать: вы должны продолжать искать алгоритм, пока не найдете тот, который вам может оказаться верным.

Анализ случайного алгоритма путем вычисления распределения

Я знаю один способ "автоматического" анализа случайного числа (или, в более общем случае, алгоритма случайного использования)это сильнее, чем просто «бросить много тестов и проверить на однородность».Вы можете механически вычислить распределение, связанное с каждым входом вашего алгоритма.

Общая идея состоит в том, что алгоритм случайного использования исследует часть мира возможностей.Каждый раз, когда ваш алгоритм запрашивает случайный элемент в наборе ({true, false} при подбрасывании монеты), у вашего алгоритма есть два возможных результата, и выбирается один из них.Вы можете изменить свой алгоритм так, чтобы вместо возврата одного из возможных результатов он параллельно анализировал всех решений и возвращал все возможные результаты с соответствующими распределениями.

В общем случае этотребуют переписать ваш алгоритм в глубину.Если ваш язык поддерживает продолжения с разделителями, это не обязательно;вы можете реализовать «исследование всех возможных результатов» внутри функции, запрашивающей случайный элемент (идея состоит в том, что генератор случайных чисел вместо возврата результата захватывает продолжение, связанное с вашей программой, и запускает его со всеми различными результатами).Пример такого подхода см. В статье HANSEI .

, разработанной Олегом. Посредник, и, возможно, менее загадочное решение, состоит в том, чтобы представить этот «мир возможных результатов» в виде монады и использовать язык.например, Haskell со средствами для монадического программирования.Вот пример реализации варианта¹ вашего алгоритма в Haskell с использованием монады вероятности пакета вероятность :

import Numeric.Probability.Distribution

shuffleM :: (Num prob, Fractional prob) => [a] -> T prob [a]
shuffleM [] = return []
shuffleM [x] = return [x]
shuffleM (pivot:li) = do
        (left, right) <- partition li
        sleft <- shuffleM left
        sright <- shuffleM right
        return (sleft ++ [pivot] ++ sright)
  where partition [] = return ([], [])
        partition (x:xs) = do
                  (left, right) <- partition xs
                  uniform [(x:left, right), (left, x:right)]

Вы можете запустить его для заданного ввода и получитьраспределение выхода:

*Main> shuffleM [1,2]
fromFreqs [([1,2],0.5),([2,1],0.5)]
*Main> shuffleM [1,2,3]
fromFreqs
  [([2,1,3],0.25),([3,1,2],0.25),([1,2,3],0.125),
   ([1,3,2],0.125),([2,3,1],0.125),([3,2,1],0.125)]

Вы можете видеть, что этот алгоритм одинаков с входами размера 2, но неоднороден на входах размера 3.

Разница с тестом на основеПодход заключается в том, что мы можем получить абсолютную уверенность за конечное число шагов: он может быть довольно большим, поскольку он представляет собой исчерпывающее исследование мира возможных (но обычно меньше 2 ^ N, поскольку существуют факторизации схожих результатов), но если он возвращает неравномерное распределение, мы точно знаем, что алгоритм неверен.Конечно, если он возвращает равномерное распределение для [1..N] и 1 <= N <= 100, вы знаете только, что ваш алгоритм одинаков вплоть до списков размером 100;это все еще может быть неправильно.

¹: этот алгоритм является вариантом реализации вашего Erlang из-за специфической обработки сводных данных.Если я не использую pivot, как в вашем случае, размер ввода больше не уменьшается на каждом шаге: алгоритм также учитывает случай, когда все входы находятся в левом списке (или правом списке), и теряются в бесконечном цикле,Это слабость реализации монады вероятностей (если у алгоритма есть вероятность не прекращения 0, вычисление распределения может все еще расходиться), которое я пока не знаю, как исправить.

На основе сортировкиshuffles

Вот простой алгоритм, который, я уверен, мог бы оказаться правильным:

  1. Выберите случайный ключ для каждого элемента в вашей коллекции.
  2. Если ключине все различны, перезапустите с шага 1.
  3. Сортируйте коллекцию по этим случайным ключам.

Шаг 2 можно опустить, если вы знаете вероятность столкновения (два случайных числа)выбраны равны) достаточно низок, но без него перемешивание не идеально равномерно.

Если вы выберете ключи в [1..N], где N - длина вашей коллекции, у вас будет много коллизий ( День рождения ). Если вы выберете свой ключ как 32-разрядное целое число, вероятность конфликта на практике будет низкой, но все равно будет возникать проблема с днем ​​рождения.

Если вы используете бесконечные (лениво оцениваемые) строки битов в качестве ключей, а не ключи конечной длины, вероятность коллизии становится равной 0, и проверка отличимости больше не требуется.

Вот случайная реализация в OCaml, использующая ленивые действительные числа в качестве бесконечных цепочек битов:

type 'a stream = Cons of 'a * 'a stream lazy_t

let rec real_number () =
  Cons (Random.bool (), lazy (real_number ()))

let rec compare_real a b = match a, b with
| Cons (true, _), Cons (false, _) -> 1
| Cons (false, _), Cons (true, _) -> -1
| Cons (_, lazy a'), Cons (_, lazy b') ->
    compare_real a' b'

let shuffle list =
  List.map snd
    (List.sort (fun (ra, _) (rb, _) -> compare_real ra rb)
       (List.map (fun x -> real_number (), x) list))

Есть и другие подходы к "чистой перетасовке". Хорошим примером является решение на основе слияния от apfelmus .

Алгоритмические соображения: сложность предыдущего алгоритма зависит от вероятности того, что все ключи различны. Если вы выберете их как 32-разрядные целые числа, у вас есть вероятность в ~ 4 миллиарда, что определенный ключ сталкивается с другим ключом. Сортировка по этим ключам O (n log n), при условии выбора случайного числа O (1).

Если у вас бесконечные цепочки битов, вам никогда не придется перезапускать сбор, но сложность тогда связана с тем, «сколько элементов потоков оценивается в среднем». Я предполагаю, что это O (log n) в среднем (следовательно, O (n log n) в целом), но не имеет доказательств.

... и я думаю, что ваш алгоритм работает

После дополнительного размышления, я думаю (как douplep), что ваша реализация верна. Вот неофициальное объяснение.

Каждый элемент в вашем списке проверен несколькими random:uniform() < 0.5 тестами. Элементу можно связать список результатов этих тестов в виде списка логических значений или {0, 1}. В начале алгоритма вы не знаете список, связанный с каким-либо из этих чисел. После первого вызова partition вы знаете первый элемент каждого списка и т. Д. Когда ваш алгоритм возвращается, список тестов полностью известен, и элементы отсортированы в соответствии с этими списками (отсортированы в лексикографическом порядок или рассматривается как двоичные представления действительных чисел).

Итак, ваш алгоритм эквивалентен сортировке по бесконечным цепочкам ключей. Действие разделения списка, напоминающее разделение быстрой сортировки по элементу pivot, фактически является способом отделения для данной позиции в цепочке битов элементов с оценкой 0 от элементов с оценкой 1.

Сортировка однородна, потому что все цепочки битов разные. Действительно, два элемента с действительными числами, равными n -ому биту, находятся на одной стороне раздела, происходящего во время рекурсивного shuffle вызова глубины n. Алгоритм завершается только тогда, когда все списки, полученные из разделов, пусты или синглетны: все элементы были разделены по крайней мере одним тестом и, следовательно, имеют одно отличное двоичное десятичное число.

Вероятностное завершение

Тонкое замечание относительно вашего алгоритма (или моего эквивалентного метода на основе сортировки) состоит в том, что условие завершения вероятностное . Фишер-Йейтс всегда заканчивается после известного количества шагов (количество элементов в массиве). С вашим алгоритмом завершение зависит от выхода генератора случайных чисел.

Существуют возможные выходные данные, которые заставят ваш алгоритм расходиться , а не завершаться. Например, если генератор случайных чисел всегда выводит 0, каждый вызов partition будет возвращать список ввода без изменений, для которого вы рекурсивно вызываете случайное перемешивание: цикл будет бесконечным.

Однако, это не проблема, если вы уверены, что ваш генератор случайных чисел справедлив: он не обманывает и всегда возвращает независимые равномерно распределенные результаты. В этом случае вероятность того, что тест random:uniform() < 0.5 всегда вернет true (или false), равна 0:

  • вероятность того, что первые N вызовов вернут true, равна 2 ^ {- N}
  • вероятность того, что все вызовы вернутся true - это вероятность бесконечного пересечения, для всех N, события, когда первые N вызовов вернутся 0; это нижний предел¹ 2 ^ {- N}, который равен 0

¹: математические детали см. http://en.wikipedia.org/wiki/Measure_(mathematics)#Measures_of_infinite_intersections_of_measurable_sets

В более общем смысле алгоритм не завершается тогда и только тогда, когда некоторые элементы связываются с одним и тем же логическим потоком. Это означает, что как минимум два элемента имеют одинаковый логический поток. Но вероятность того, что два случайных логических потока равны, снова равна 0: вероятность того, что цифры в позиции K равны, равна 1/2, поэтому вероятность того, что N первых цифр равны, равна 2 ^ {- N}, и то же самое анализ применим.

Следовательно, вы знаете, что ваш алгоритм завершается с вероятностью 1 . Это немного более слабая гарантия того, что алгоритм Фишера-Йейтса, который всегда завершается . В частности, вы уязвимы для атаки злого противника, который будет контролировать ваш генератор случайных чисел.

Имея больше теории вероятностей, вы также можете вычислить распределение времени выполнения вашего алгоритма для заданной входной длины. Это выходит за рамки моих технических возможностей, но я предполагаю, что это хорошо: я полагаю, что вам нужно только посмотреть на O (log N) первые цифры в среднем, чтобы проверить, что все N lazy потоков отличаются, и что вероятность намного более высокого времени выполнения экспоненциально.

21 голосов
/ 15 октября 2010

Ваш алгоритм представляет собой перемешивание на основе сортировки, как описано в статье в Википедии.

Вообще говоря, вычислительная сложность перемешивания на основе сортировки такая же, как и в базовом алгоритме сортировки (например, O (* 1003).* n log n ) среднее значение, O ( n ²) наихудший случай для перемешивания на основе быстрой сортировки), и в то время как распределение не идеально равномерный, он должен приближаться к равномерному достаточно близко для большинства практических целей.

Олег Киселев приводит следующую статью / обсуждение:

, который более подробно охватывает ограничения на основе сортировки по принципу сортировки, а также предлагает две модификации стратегии Фишера – Йейтса: наивный O ( n ²) одини O ( n log n ) на основе двоичного дерева.

К сожалению, в мире функционального программирования нет доступа к изменяемымштат.

Это не так: хотя чисто функциональное программирование позволяет избежать побочных эффектов , оно поддерживает доступ к изменяемому состоянию с первоклассными эффектами, не требуя побочных эффектов.

В этом случае вы можете использоватьИзменчивые массивы Haskell для реализации мутирующего алгоритма Фишера-Йейтса, как описано в этом руководстве:

Приложение

Конкретная основа вашей случайной сортировки на самом деле - бесконечный ключ radix sort : как указывает Гаш, каждый раздел соответствует группировке цифр.

Основным недостатком этогоаналогичен любой другой случайной сортировке: гарантия завершения не предоставляется.Хотя вероятность завершения увеличивается по мере продолжения сравнения, верхней границы никогда не бывает: сложность наихудшего случая равна O (∞).

3 голосов
/ 08 марта 2011

Некоторое время назад я занимался чем-то похожим на это, и, в частности, вас могут заинтересовать векторы Clojure, которые являются функциональными и неизменными, но при этом имеют O (1) характеристики произвольного доступа / обновления. Эти два гистограммы имеют несколько реализаций «произвольно взять N элементов из этого списка размером M»; хотя бы один из них превращается в функциональную реализацию Фишера-Йейтса, если вы допустите N = M.

https://gist.github.com/805546

https://gist.github.com/805747

1 голос
/ 30 июля 2015

На основании Как проверить случайность (в данном случае - Перемешивание) , я предлагаю:

Перестановочные (средние) массивы, состоящие из равного числа нулей и единиц. Повторите и объедините, пока не надоест. Используйте их в качестве входных данных для несгибаемых испытаний. Если у вас хорошее перемешивание, то вы должны генерировать случайные последовательности нулей и единиц (с оговоркой, что совокупный избыток нулей (или единиц) равен нулю на границах массивов среднего размера, которые, как вы надеетесь, обнаружат тесты). , но чем больше «средний», тем меньше вероятность, что они это сделают).

Обратите внимание, что тест может отклонить ваш случай по трем причинам:

  • алгоритм тасования плох,
  • генератор случайных чисел, используемый shuffler или во время инициализации, неверен, или
  • Плохая реализация теста.

Вам придется решить, что в случае отклонения любого теста.

Различные адаптации жестких тестов (для разрешения определенных чисел я использовал source со страницы diehard ). Основной механизм адаптации состоит в том, чтобы заставить алгоритм случайного выбора действовать как источник равномерно распределенных случайных битов.

  • Интервалы дня рождения: в массиве n нулей вставьте лог n единиц. Перемешать. Повторите, пока не надоест. Построить распределение расстояний между ними, сравнить с экспоненциальным распределением. Вы должны выполнить этот эксперимент с разными стратегиями инициализации - те, что на передней панели, и те, что в конце, и те, и другие в середине, и те, которые разбросаны случайным образом. (Последний имеет наибольший риск плохой рандомизации инициализации (относительно случайной случайности перемешивания), приводящей к отклонению перемешивания.) Это может фактически быть сделано с блоками с одинаковыми значениями, но есть проблема, которая вводит корреляцию в распределениях ( один и два не могут быть в одном и том же месте в одном случайном порядке.
  • Перекрывающиеся перестановки: перемешайте пять значений несколько раз. Убедитесь, что 120 результатов примерно одинаково вероятны. (Критерий хи-квадрат, 119 степеней свободы - критерий несгибаемости (cdoperm5.c) использует 99 степеней свободы, но это (в основном) артефакт последовательной корреляции, вызванный использованием перекрывающихся подпоследовательностей входной последовательности.)
  • Ранги матриц: из 2 * (6 * 8) ^ 2 = 4608 бит из перетасовывания равного количества нулей и единиц выберите 6 непересекающихся 8-битных подстрок. Рассматривайте их как двоичную матрицу 6 на 8 и вычисляйте ее ранг. Повторите для 100 000 матриц. (Объедините ранги 0-4. Ранги тогда будут либо 6, 5, либо 0-4.) Ожидаемая доля рангов составляет 0,773118, 0,217439, 0,009443. Хи-квадрат сравнивают с наблюдаемыми фракциями с двумя степенями свободы. Тесты 31 на 31 и 32 на 32 похожи. Ранги 0-28 и 0-29 объединяются соответственно. Ожидаемые фракции: 0,2887880952, 0,5775761902, 0,1283502644, 0,0052854502. Тест хи-квадрат имеет три степени свободы.

и так далее ...

Вы также можете использовать dieharder и / или ent для проведения аналогичных адаптированных тестов.

...