Всем привет! Может быть, эта тема уже обсуждалась, но по запросу "random" ничего похожего на Codeforces не нашел.
Предыстория. Хотели с Сашей (demon1999) изучить декартово дерево. Для инициализации приоритетов рекомендуется использовать случайные числа, чтобы высота дерева не была очень большой. Соответственно, надо эти числа как-то получить. Я совсем не разбираюсь в структурах данных, поэтому не задумывался, насколько плохо будет дереву (и будет ли), если приоритеты нескольких вершин будут одинаковыми. Поэтому на всякий случай хотелось сделать их попарно различными (чего не гарантирует простое использование rand() при создании очередной вершины). Предложил следующий, "надежный" и "проверенный" метод — создать массив из N чисел, инициализировать его числами от 0 до (N-1) соответственно, применить к нему random_shuffle — и мы получим N различных ключей в случайном порядке.
История. Саша стала сдавать задачи и на практике оказалось, что в нескольких задачах такой подход достаточно стабильно приводит к вердикту Time Limit Exceeded, в то время как простейшая инициализация pr=rand() получала Accepted. Стало очень интересно, почему так происходит, да и вообще STL не склонен быть причиной каких-либо ошибок. После несложных исследований оказалось, что (возможно, не это причина TLE, но тем не менее) random_shuffle перемешивает массив не совсем случайным образом.
Если попытаться найти реализацию алгоритма random_shuffle, то Eclipse во всплывающей подсказке предлагает следующее:
for (_RandomAccessIterator __i = __first + 1; __i != __last; ++__i) std::iter_swap(__i, __first + (std::rand() % ((__i — __first) + 1)));
То есть, каждый элемент меняется местом с другим элементом, находящимся на rand() правее него. Проблема в том, что в Windows, например, rand() возвращает число от 0 до RAND_MAX, т.е. до 32767. Получается, что при большом массиве (скажем, больше 200000, что при нынешних мощностях компьютеров обычно и бывает в задачах на деревья) элемент меняется с другим элементом, стоящим достаточно близко к нему. Далее будем считать, что у нас есть массив a[] и изначально a[i]=i. Тогда после такого random_shuffle в среднем значение элемента a[i] изменится не сильно (порядка RAND_MAX). Более того, теоретически на позиции i не будет элемента со значением больше, чем i+RAND_MAX (кроме некоторых эффектов на границах массива). Посмотрим, что получается на практике.
Пусть n=500000. Давайте посмотрим, какие числа будут в середине массива (от n/2 до n/2+10000). Числа, понятное дело, большие и различные. Для наглядности рассмотрим распределение первой цифры этих чисел. Хотелось бы, конечно, чтобы после перемешивания это распределение было близко к равномерному от 0 до 4. Например, shuffle из testlib.h работает вполне корректно:
Если использовать random_shuffle, то, ожидаемо, получим совсем другое распределение:
То есть у подавляющего большинства чисел, действительно, первая цифра не изменилась (осталась 2).
Как мы видим, с серединой массива уже большие проблемы. Давайте пройдемся по всему массиву и посмотрим на значение каждого тысячного элемента (все 500000 элементов тяжело на графике расположить). Опять-таки, нам хочется, чтобы значения элементов были случайно распределены от 0 до n. Вот, что дает shuffle из testlib:
Вполне достойная случайная величина. Теперь посмотрим на аналог, созданный через random_shuffle:
Даже при всем желании "качественным" перемешиванием это не назовешь. Ну и давайте еще найдем усредненные значения элементов (т.е мат. ожидание) на интервалах с a[i] по a[i]+10000 для всех i, делящихся на 10000. Конечно, хочется, чтобы эти значения были примерно равны n/2 (т.е. 250000) и примерно равны между собой. Вот результат для shuffle:
Грубо говоря, все мат. ожидания лежат в интервале 25000+-300. График для random_shuffle:
Практически линейная зависимость мат. ожидания от индекса. Соответственно, давайте, наконец, сделаем более математическое сравнение. Пусть у нас будут две случайные величины — индекс элемента массива (i) и значение элемента массива (a[i]). При случайном перемешивании эти величины должны быть независимы. Давайте посчитаем линейный коэффициент корреляции этих величин по нескольким выборкам для различных значений n (от 100000 до 2000000 с шагом 100000) — в идеале значение должно быть очень близко к 0. Вот табличка результатов для shuffle:
100000: -0.00157 0.00092 -0.00082 0.00244 -0.00089 200000: -0.00168 -0.00105 0.00036 0.00303 -0.00205 300000: 0.00082 0.00176 0.00111 0.00082 -0.00324 400000: 0.00045 0.00195 -0.00293 0.00027 -0.00001 500000: 0.00115 0.00122 -0.00129 0.00098 0.00089 600000: 0.00055 0.00100 0.00025 0.00067 0.00033 700000: -0.00009 0.00094 0.00069 0.00003 0.00061 800000: 0.00122 0.00160 0.00048 -0.00077 -0.00061 900000: -0.00052 -0.00040 0.00003 0.00027 -0.00023 1000000: -0.00049 0.00049 0.00001 0.00050 0.00126 1100000: 0.00100 -0.00129 0.00051 0.00114 0.00071 1200000: 0.00081 0.00147 -0.00026 0.00086 -0.00116 1300000: 0.00008 0.00122 -0.00045 0.00077 -0.00052 1400000: 0.00040 0.00132 -0.00048 0.00022 -0.00030 1500000: -0.00051 -0.00009 -0.00021 -0.00054 -0.00119 1600000: 0.00010 -0.00004 -0.00012 0.00028 0.00023 1700000: -0.00046 0.00024 -0.00109 0.00017 -0.00090 1800000: 0.00019 0.00052 0.00007 0.00082 -0.00039 1900000: -0.00018 -0.00122 0.00081 -0.00030 0.00008 2000000: 0.00099 0.00051 -0.00040 -0.00076 -0.00043
А вот — для random_shuffle:
100000: -0.13653 -0.13753 -0.13943 -0.13137 -0.13647 200000: 0.21258 0.21242 0.21115 0.21414 0.21200 300000: 0.42703 0.42686 0.42717 0.42612 0.42577 400000: 0.55270 0.55252 0.55290 0.55336 0.55311 500000: 0.63480 0.63479 0.63484 0.63503 0.63458 600000: 0.69161 0.69160 0.69173 0.69143 0.69172 700000: 0.73298 0.73307 0.73306 0.73300 0.73322 800000: 0.76480 0.76489 0.76484 0.76493 0.76477 900000: 0.78988 0.78995 0.78985 0.78993 0.78980 1000000: 0.81022 0.81009 0.81013 0.81010 0.81005 1100000: 0.82676 0.82678 0.82685 0.82680 0.82676 1200000: 0.84076 0.84077 0.84080 0.84076 0.84072 1300000: 0.85269 0.85272 0.85280 0.85268 0.85267 1400000: 0.86300 0.86300 0.86295 0.86293 0.86297 1500000: 0.87188 0.87186 0.87191 0.87189 0.87186 1600000: 0.87969 0.87970 0.87972 0.87971 0.87971 1700000: 0.88662 0.88664 0.88664 0.88662 0.88665 1800000: 0.89279 0.89282 0.89278 0.89282 0.89282 1900000: 0.89834 0.89835 0.89835 0.89834 0.89835 2000000: 0.90338 0.90335 0.90334 0.90335 0.90333
Как и ожидалось, у random_shuffle на большИх значениях n коэффициент корреляции приближается к 1, т.е. наблюдается практически линейная зависимость (что согласуется с графиками выше). При этом shuffle из testlib работает практически идеально, коэффициент по абсолютному значению почти всегда меньше 0.001.
Итог: при большИх значениях размера массива (или какой-либо коллекции) стандартный std::random_shuffle работает не совсем корректно. Перемешивание, близкое к "случайному", гарантируется на маленьких размерах (не больше RAND_MAX), при увеличении числа элементов, перемешивание носит локальный характер и меняет только близкие числа. Будьте внимательны и не получайте ненужных TLE!
UPD: Как предложили в комментариях ilyakor, slycelote и CountZero, в c++11 можно использовать, например, mt19937 и random_device для перемешивания функцией shuffle. На всякий случай проверил эту реализацию — "проходит" все тесты, предложенные выше, никаких проблем не наблюдается.
Большое спасибо, очень информативно! А можно указать ещё версию компилятора на всякий случай?
Пробовал на довольно новой версии g++ (4.8.1) и на старой (аж 3.4.2, 2006 года) — эффект такой же. Аналогично, тот же результат получается на 32 и 64-битном компиляторе, с использованием и без использования c++11.
Но, насколько я знаю, под Linux таких проблем нет, ибо там rand() возвращает 32-битное число.
После слов красного: "Я совсем не разбираюсь в структурах данных..." мой мир перевернулся)
Посмотрел учебные планы ЛКШ 2011 года... Из 15 тем параллели A знаю 2. Если бы был сейчас школьником, подал бы заявку в параллель B. Надеюсь, Ваш мир не сильно пострадал :)
Ну так в B самые полезные для спортивного программирования вещи проходят.
А можно ссылку на 15 тем? Я, как человек не занимающийся спортивным программированием на регулярной основе, трудно ориентируюсь в этих ресурсах.
Вообще, ответ на Ваш вопрос гуглится по запросу "ЛКШ учебный план".
На сайте ЛКШ ссылка на учебные планы 2011 года: http://lksh.ru/sis/2011/materials/plans-11.pdf
Может быть, сейчас планы изменились, но тут тоже тем достаточно.
Wow, какой неочевидный способ выстрелить себе в ногу... Стоит заметить, что (вроде бы) на всех распространённых unix-системах в GCC RAND_MAX достаточно большой, и прикол видимо специфичен для винды (т.е. на контестах вроде ACM Finals, Opencup, Topcoder волноваться не стоит — проблемы только у windows-based систем (т.е. codeforces и видимо pcms2)).
Стоит заметить, что в современном C++ существует нормальный shuffle, использующий нормальный рандом:
в unix-like системах RAND_MAX = 231 - 1
а еще
mt19937
работает в 2 раза быстрее обычногоrand()
, позволяет генерировать 64-битные числа (в версии mt19937_64), и вообще это хорошая практика — использовать C++, когда пишешь на C++ :)О, спасибо! Чуть-чуть только до этого смотрел, что добавили в c++11, теперь буду знать, как можно шафлить. На всякий случай тоже эту реализацию проверил, все работает, обновил пост :)
На счет windows-based — мне тоже казалось, что для unix-like все хорошо (теперь CountZero подтвердил). В посте вскользь упоминал, что это под windows беда такая, теперь выделил жирным.
Good finding! As we see here, this overload of
std::random_shuffle
is deprecated and this one (renamed tostd::shuffle
since C++11) should be used instead:where
RandomFunc
can be, for example, anstd::random_device
. Have you tried it?upd: ninja'd by ilyakor
В testlib.h, кстати, из java.util.Random скопипащено все, даже константы те же)
Это же обычный конгруэтный ГСЧ, для которого константы должны удовлетворять определённым свойствам, и зачем искать новые, когда уже есть известные. Думаю, если поискать, то точно такие же константы можно найти ещё много где.
Мне как-то пару лет назад -XraY- рассказал про это) Если не под C++11, то можно делать так:
Вообще, декартово дерево не ломается, если есть элементы с одинаковыми приоритетами. Чтобы не вносить хаос можно, к примеру, ориентировать все ребра между равноприоритетными элементами направо. Т.е. вот код merge:
Правда, если много элементов с одинаковыми приоритетами, дерево разбалансируется, но при адекватном rand() это невероятно.
Спасибо за открытие очередной подставы С++, теперь только shuffle() =)
можна просто юзать. rand()|(rand()<<16). получаем диапазон до 2^32,я не отрицаю что при большом количестве вершин эти значения будут совпадать но как вариант
Что мешает брать рандом от n, делённый на 10000 плюс рандомный остаток? Если сумма больше чем n, генерим новое число. И написать свой рандом шафл, это несколько строк.