Все мы хорошо знаем (а кто не знает, тот не мы или не все), как можно сделать случайную перестановку массива из N элементов:
arr = list(range(N)) # исходный массив
for i in range(0,N-1): # 0..N-2 - последняя итерация была бы вырожденной, поэтому исключим её.
j = random.randint(i,N-1) # равномерно распределённое случайное число из i..N-1
arr[i],arr[j] = arr[j],arr[i] # выполняем обмен очередного элемента со случайным
Несложно показать, что если ГСЧ равномерный, то все N! перестановок тоже распределены равномерно.
Правда, есть одна заморочка: если опорный ГСЧ выдаёт числа из фиксированного диапазона (обычно, в разрядной сетке — т.е., например, сишный rand() — 15-битный, 0..32767), — то результирующие 0..2, 0..4, 0..6 и т.д., не являющиеся делителями 2**14, будут чуточку неравномерно распределены.
Задачка 1. Разогревочная.
Если результирующий ГСЧ мы получаем из опорного по формуле вида
minvalue + rand()%(maxvalue+1-minvalue)
или
minvalue + rand()*(maxvalue+1-minvalue)/(RAND_MAX+1)
,
то во сколько раз чаще будем получать наиболее вероятную перестановку, по сравнению с наименее вероятной?
На какой длине массива этот разброс вероятностей достигнет 2? 10?
Будем считать, что опорный ГСЧ — сишный 15-битный.
Кстати, к сишному генератору есть вопросы по поводу автокорреляции (а это важно, когда мы берём не одно значение, а серию).
Поэтому есть лайфхак: сгенерить случайный текст без особых ограничений на корреляции, и зашифровать (или даже захешировать) его более-менее стойким алгоритмом. На выходе будет достаточно качественный белый шум.
И в качестве опорного ГСЧ взять байты этой шифрованной последовательности.
Будем считать, что опорный ГСЧ — вот такой хороший, но 8-битный.
На какой длине массива достигнем 2- и 10-кратного разброса вероятностей?
Задачка 2. Программистская.
По-прежнему, пусть у нас есть опорный ГСЧ — 15- или 8-битный.
Что нужно сделать, чтобы минимизировать разброс вероятностей перестановок? Желательно, детерминированным способом. (А то знаем мы: кидать кубик до тех пор, пока не выпадет число в искомом диапазоне).
Задачка 3. Внезапная.
Пусть мы разжились опорным равномерным ГСЧ 0..N-1 и решили этим воспользоваться.
for i in range(0,N): # 0..N-1
j = random.randint(0,N-1) # 0..N-1
arr[i],arr[j] = arr[j],arr[i]
Мысль здесь такая: выше мы прикладывали воздействие в пространстве из N! операций, а здесь — в пространстве N**N, что гораздо больше факториала. То есть, перемешиваем с некоторым избытком.
Насколько неравномерное распределение перестановок получилось?
Всё тот же вопрос: вероятность моды / вероятность раритета.
Задачка 4. С кучей звёздочек.
Для заданного N получить моду и раритет, то есть, сами перестановки, а не значения их вероятностей.
Как это сделать в случае с первым алгоритмом — факториальным — с дребезжащим ГСЧ — это очень просто. Мы выкрутим вероятности до крайностей и смоделируем.
Например, при взятии остатка число i наиболее вероятно, а число N-1 — наименее. При делении — обратная картина.
mode = list(range(N))
rare = list(range(N))
for i in range(N-2):
# обмен i,i для моды - вырожден
rare[i],rare[N-1] = rare[N-1],rare[i]
А вот со вторым случаем природа неравномерности уже не в дребезге ГСЧ, а в самом алгоритме.
И как сгенерировать моду и раритет, я не знаю. Есть ли у кого идеи?