Эффективный выбор случайных чисел

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

Я не уверен, насколько быстры Java Random().nextInt на самом деле, но моя программа, похоже, не приносит такой пользы, как мне бы того хотелось.

При выборе случайных чисел я делаю следующее (в полупсевдокоде):

// Repeat this 300000 times
Set set = new Set();
while(set.length != 5)
    set.add(randomNumber(MIN,MAX));

Теперь очевидно, что это имеет плохое время выполнения в худшем случае, поскольку случайная функция теоретически может добавлять повторяющиеся числа на вечность, таким образом, навсегда оставаясь в цикле while. Однако числа выбираются из {0..45}, поэтому дублирование значений по большей части маловероятно.

Когда я использую вышеуказанный метод, он всего на 40% быстрее, чем мой другой метод, который не приближает, но дает правильный результат. Это выполняется примерно 1 миллион раз, поэтому я ожидал, что этот новый метод будет как минимум на 50% быстрее.

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

Чтобы уточнить, вот два метода:

// Run through all combinations (1 million). This takes 5 seconds
 for(int c1 = 0; c1 < deck.length; c1++){
    for(int c2 = c1+1; c2 < deck.length; c2++){
     for(int c3 = c2+1; c3 < deck.length; c3++){
        for(int c4 = c3+1; c4 < deck.length; c4++){
         for(int c5 = c4+1; c5 < deck.length; c5++){
             enumeration(hands, cards, deck, c1, c2, c3, c4, c5);
         }
            } 
      }     
   }
   }

// Approximate (300000 combinations). This takes 3 seconds
Random rand = new Random();
HashSet<Integer> set = new HashSet<Integer>();
int[] numbers = new int[5];
while(enumerations < 300000){
set.clear();
while(set.size() != 5){
    set.add(rand.nextInt(deck.length));
}
Iterator<Integer> i = set.iterator();
int n = 0;
while(i.hasNext()){
    numbers[n] = i.next();
    n++;
}

После некоторого тестирования и профилирования я нашел этот метод наиболее эффективным:

Random rand = new Random();
int[] numbers = new int[5];
ArrayList<Integer> list = new ArrayList<Integer>();
while(enumerations < 300000){
 while(list.size() != 5) {
     int i = rand.nextInt(deck.length);
        if(!list.contains(i)) list.add(i);
 }
 int index = 0;
 for(int i : list){ numbers[index] = i; index++; }
 enumeration(hands, cards, deck,numbers);
}

person Community    schedule 26.03.2010    source источник
comment
Можете ли вы еще раз сформулировать, чего вы пытаетесь достичь? Вы пытаетесь сгенерировать набор из N различных чисел при каждом вызове метода? Вы говорите о сравнении этого метода с другим, который не является аппроксимирующим, а другой метод более быстрым - является ли реальная проблема генерацией случайных чисел или вашим методом для выполнения каких-либо других вычислений (приближение или неприближение)?   -  person matt b    schedule 26.03.2010
comment
Проблема в генерации случайных чисел. Другие расчеты не имеют значения, поэтому я не упомянул их в своем вопросе.   -  person Frederik Wordenskjold    schedule 26.03.2010


Ответы (8)


Вы можете попробовать использовать существующую реализацию Java (или этот) для Вихрь Мерсенна.

Имейте в виду, что большинство MT не криптографически защищены.

person Yuval Adam    schedule 26.03.2010
comment
Можете ли вы уточнить, что вы подразумеваете под не криптографически безопасным? - person Frederik Wordenskjold; 26.03.2010
comment
Это означает, что вы не должны использовать их для криптографии, потому что все же можно предсказать следующее число, учитывая определенный объем предварительной информации. - person Tesserex; 26.03.2010

Похоже, вы хотите выбрать комбинацию k- из набора S без замены, где S имеет n различных значений, k = 5 и n > = 52. Вы можете shuffle() весь набор и выбрать элементы k (как предлагает @Tesserex) или pick() элементов k, избегая при этом дубликатов (как вы показали). Вы захотите профилировать как в вашей конкретной среде, так и для выбранного вами генератора. Я часто, но не всегда, вижу скромное преимущество pick().

private static final Random rnd = new Random();
private static final int N = 52;
private static final int K = 5;
private static final List<Integer> S = new ArrayList<Integer>(N);
static {
    for (int i = 0; i < N; i++) {
        S.add(i + 1);
    }
}
private final List<Integer> combination = new ArrayList<Integer>(K);

...

private void shuffle() {
    Collections.shuffle(S, rnd);
    combination.addAll(S.subList(0, K));
}

private void pick() {
    for (int i = 0; i < K; i++) {
        int v = 0;
        do {
            v = rnd.nextInt(N) + 1;
        } while (combination.contains(v));
        combination.add(v);
    }
}
person trashgod    schedule 26.03.2010

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

person Tesserex    schedule 26.03.2010
comment
Если приложение выглядит так, как оно выглядит (калькулятор шансов в покере), то существует 52C5 == 2598960 возможных входных данных, поэтому будет использовано менее 1/6 входных данных. Это очень неэффективное использование памяти, поскольку входные выборки (в типичном калькуляторе шансов в покере) не нужно сохранять в памяти после оценки. Это будет еще хуже в вероятном случае, если функция оценки будет расширена до 7-карточных рук (52C7 == 133784560 комбинаций). - person finnw; 26.03.2010
comment
да, вы правы - к сожалению, дополнительной информации с фактическими методами не было в вопросе, когда я писал свой ответ. - person Tesserex; 26.03.2010

Вы можете использовать линейную конгруэнтность в качестве генератора случайных чисел: /a> [но учтите их статистические недостатки]

Вам нужен только расчет (x + c)% m для каждого числа. Тем не менее, по моему опыту, создание объектов (как вы могли бы делать с каждым вызовом new Set и add, в зависимости от используемой реализации) может стоить вам больше скорости, чем вызов nextInt(). Возможно, вам стоит попробовать профилировщик, например, например. этот: http://www.eclipse.org/tptp/

person Searles    schedule 26.03.2010
comment
Я использую OS X, поэтому я не могу использовать профилировщик eclipse tptp! Я очень скучаю по профайлеру! - person Frederik Wordenskjold; 26.03.2010
comment
Я когда-то использовал JProfiler в Mac OS X. У них есть бесплатный пробный период 14 дней. - person Searles; 26.03.2010

У меня нет никакого вклада в вашу настоящую проблему, и я не слишком много знаю Java (просто ковыряюсь). Однако мне кажется, что вы пытаетесь создать оценщик рук для покера, и эта тема http://pokerai.org/pf3/viewtopic.php?f=3&t=16 содержит несколько очень быстрых оценщиков рук Java. Надеюсь, что часть этого кода может помочь.

person RD.    schedule 26.03.2010
comment
На самом деле меня вдохновили некоторые алгоритмы в этой теме. Однако я реализую omaha-evaluator, поэтому многие вещи в этом потоке, такие как таблицы поиска один на один, я не могу использовать. - person Frederik Wordenskjold; 26.03.2010

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

// Assuming we're just numbering all the cards 0 to 51. This could be more sophisticated, of course.
ArrayList cards=new ArrayList(52);
for (int x=0;x<52;++x)
  cards=new Integer(x);

Integer[] hand=new Integer[5];
for (int h=0;h<5;++h)
{
  // Pick a card from those remaining
  int n=random.nextInt(cards.size());
  hand[h]=cards.get(n);
  // Remove the picked card from the list
  cards.remove(n);
}

Для первого тиражаcards.get(n) вернет n, независимо от того, что такое n. Но с этого момента значения будут удалены, так чтоcards.get(3) может возвращать 7 и т.д.

Создание списка и удаление из него добавляет кучу накладных расходов. Я предполагаю, что если вы выбираете только 5 карт за раз, вероятность столкновений достаточно мала, поэтому устранение дубликатов после их обнаружения будет быстрее, чем их предотвращение. Даже при последнем розыгрыше вероятность дублирования составляет всего 4/52 = 1/13, поэтому вы вообще редко попадете в дубликат, а вероятность того, что 2 розыгрыша подряд будут дубликатами, будет ничтожно мала. Все зависит от того, сколько времени требуется для генерации случайного числа по сравнению с тем, сколько времени требуется для настройки массива и удаления. Самый простой способ сказать это – провести несколько экспериментов и измерить. (Или профиль!)

person Jay    schedule 26.03.2010
comment
Именно то, о чем я думал, - вероятность дубликатов настолько мала, что время, необходимое для их предотвращения, займет больше времени, чем просто проверка на них. Я обновил ОП своим результатом. - person Frederik Wordenskjold; 27.03.2010

Никогда не угадывайте, всегда измеряйте.

 long time = System.getCurrentMilliseconds();
 Random().nextInt()
 System.out.println(System.getCurrentMilliseconds() - time);

Кроме того, вы никогда не должны полагаться на то, насколько редко будет происходить известная ошибка, просто защитите код, чтобы этого не произошло. Обнаружьте дубликат, и если это дубликат, не добавляйте его и пропустите итерацию с оператором continue.

Что касается самых быстрых методов и случайных чисел... Вы не можете получить случайные числа в Math.random() Java. Вы можете получить только псевдослучайные числа. То, насколько быстро вы хотите, чтобы это было, приходится жертвовать тем, насколько случайными они должны вам казаться. Самый быстрый способ генерировать псевдослучайное число будет включать сдвиг битов и добавление на основе начального значения, такого как System.getCurrentMilliSeconds(). достаточно счастлив, как только вы увидите, сколько миллисекунд требуется, чтобы сгенерировать один с Math.random().

person Zombies    schedule 26.03.2010
comment
@Yuval: Если вы не измеряете, вы не знаете, когда это будет достаточно быстро. Профилирование часто является инвазивным. Вы должны измерять профиль и... хотя, конечно, не должны измерять одиночный вызов, подобный этому. - person Jon Skeet; 26.03.2010
comment
Насколько быстро работает Math.random() по сравнению с Random().nextInt()? В данный момент я использую класс Random. - person Frederik Wordenskjold; 26.03.2010
comment
@Frederik: Math.random() использует Random.nextDouble() внутри, поэтому он работает медленнее, если что. - person Michael Borgwardt; 26.03.2010

Не пытайтесь разработать свой известный генератор случайных чисел. Вместо этого используйте известный, например SecureRandom:

http://www.owasp.org/index.php/Using_the_Java_Cryptographic_Extensions

person Community    schedule 26.03.2010