Довільна випадковість (швидкість видання)


10

Задавши ціле число n, обчисліть набір nвипадкових унікальних цілих чисел у діапазоні 1..n^2(включно) таким чином, що сума множини дорівнюєn^2

Випадково в цьому випадку означає рівномірно випадкові між дійсними результатами. Кожен дійсний вихід для даної nповинен мати рівномірний шанс генеруватися.

Наприклад, n=3повинні мати 1/3 шансу кожен з виведення 6, 1, 2, 3, 5, 1або 4, 3, 2. Оскільки це набір, порядок не має значення, 4, 3, 2ідентичний3, 2, 4

Оцінка балів

Переможець - програма, яка може обчислити найвищу nза менше 60 секунд.
Примітка. Щоб запобігти можливому частковому жорсткому кодуванню, всі записи повинні бути менше 4000 байт

Тестування

Весь код буде працювати на моїй локальній машині Windows 10 (Razer Blade 15, 16 Гб оперативної пам’яті, Intel i7-8750H 6 ядер, 4,1 ГГц, GTX 1060 у випадку, якщо ви хочете зловживати графічним процесором), тому, будь ласка, надайте детальну інструкцію для запуску коду на моя машина.
За запитом записи можна запускати або через Debian на WSL, або на віртуальній машині Xubuntu (обидва на тій же машині, що і вище)

Подання будуть проводитися 50 разів підряд, підсумковий бал становитиме в середньому всі 50 результатів.



Чи дозволено жорстке кодування, якщо воно становить менше 4000 байт?
Квінтек

@Quintec Ні, жорстке кодування є стандартною лазівкою, тому заборонено за замовчуванням. Хитра річ у жорсткому кодуванні також вважається критерієм, що не спостерігається, тому я офіційно не можу сказати "Без жорсткого кодування", поза тим, що лазівка ​​не дозволяє. Звідси межа байта. Іншими словами: Будь ласка , не
жорсткий код

1
Більшість публікацій використовуватиме метод відхилення, і, таким чином, час роботи буде випадковим і мати велику варіативність. Це ускладнює терміни
Луїс Мендо

2
О, я забув - оскільки деякі рішення можуть вирішити використовувати низькоякісний RNG для швидкої роботи, можливо, буде потрібно передбачити чорну скриньку, яка приймає n і видає випадкове число в (1..n), і змушує всіх рішення для його використання.
користувач202729

Відповіді:


6

Іржа , n ≈ 1400

Як бігати

Створіть cargo build --releaseі працюйте з target/release/arbitrary-randomness n.

Ця програма працює найшвидше з великою кількістю пам’яті (доки, звичайно, її не замінювати). Ви можете налаштувати його використання пам'яті, відредагувавши MAX_BYTESконстанту, встановлену на даний момент у 8 Гб.

Як це працює

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

Використання пам'яті для великих n зменшується за допомогою версії цієї біноміальної стратегії розподілу .

Cargo.toml

[package]
name = "arbitrary-randomness"
version = "0.1.0"
authors = ["Anders Kaseorg <andersk@mit.edu>"]

[dependencies]
rand = "0.6"

src/main.rs

extern crate rand;

use rand::prelude::*;
use std::env;
use std::f64;
use std::mem;

const MAX_BYTES: usize = 8 << 30; // 8 gibibytes

fn ln_add_exp(a: f64, b: f64) -> f64 {
    if a > b {
        (b - a).exp().ln_1p() + a
    } else {
        (a - b).exp().ln_1p() + b
    }
}

fn split(steps: usize, memory: usize) -> usize {
    if steps == 1 {
        return 0;
    }
    let mut u0 = 0;
    let mut n0 = f64::INFINITY;
    let mut u1 = steps;
    let mut n1 = -f64::INFINITY;
    while u1 - u0 > 1 {
        let u = (u0 + u1) / 2;
        let k = (memory * steps) as f64 / u as f64;
        let n = (0..memory)
            .map(|i| (k - i as f64) / (i as f64 + 1.))
            .product();
        if n > steps as f64 {
            u0 = u;
            n0 = n;
        } else {
            u1 = u;
            n1 = n;
        }
    }
    if n0 - (steps as f64) <= steps as f64 - n1 {
        u0
    } else {
        u1
    }
}

fn gen(n: usize, rng: &mut impl Rng) -> Vec<usize> {
    let s = n * n.wrapping_sub(1) / 2;
    let width = n.min(MAX_BYTES / ((s + 1) * mem::size_of::<f64>()));
    let ix = |m: usize, k: usize| m + k * (s + 1);
    let mut ln_count = vec![-f64::INFINITY; ix(0, width)];
    let mut checkpoints = Vec::with_capacity(width);
    let mut a = Vec::with_capacity(n);
    let mut m = s;
    let mut x = 1;

    for k in (1..=n).rev() {
        let i = loop {
            let i = checkpoints.len();
            let k0 = *checkpoints.last().unwrap_or(&0);
            if k0 == k {
                checkpoints.pop();
                break i - 1;
            }
            if i == 0 {
                ln_count[ix(0, i)] = 0.;
                for m in 1..=s {
                    ln_count[ix(m, i)] = -f64::INFINITY;
                }
            } else {
                for m in 0..=s {
                    ln_count[ix(m, i)] = ln_count[ix(m, i - 1)];
                }
            }
            let k1 = k - split(k - k0, width - 1 - i);
            for step in k0 + 1..=k1 {
                for m in step..=s {
                    ln_count[ix(m, i)] = ln_add_exp(ln_count[ix(m - step, i)], ln_count[ix(m, i)]);
                }
            }
            if k1 == k {
                break i;
            }
            checkpoints.push(k1);
        };

        while m >= k && rng.gen_bool((ln_count[ix(m - k, i)] - ln_count[ix(m, i)]).exp()) {
            m -= k;
            x += 1;
        }
        a.push(x);
        x += 1;
    }
    a
}

fn main() {
    if let [_, n] = &env::args().collect::<Vec<_>>()[..] {
        let n = n.parse().unwrap();
        let mut rng = StdRng::from_entropy();
        println!("{:?}", gen(n, &mut rng));
    } else {
        panic!("expected one argument");
    }
}

Спробуйте в Інтернеті!

(Примітка: версія TIO має декілька модифікацій. По-перше, обмеження пам’яті зменшено до 1 Гб. По-друге, оскільки TIO не дає вам написати a Cargo.tomlі залежить від зовнішніх ящиків, наприклад rand, я замість цього витягнув drand48із бібліотеки С за допомогою FFI. Я не намагався його викласти, тому версія TIO дасть однаковий результат на кожному запуску. Не використовуйте версію TIO для офіційного бенчмаркінгу.)


Оскільки формат з плаваючою комою є кінцевим, його можна оптимізувати ln_add_exp, перевіривши, чи абсолютна різниця більша за ~ 15 або більше, що може бути швидше, якщо таких доповнень багато.
користувач202729

@ user202729 Ні, майже всі ln_add_expдзвінки передбачають порівнянні входи.
Андерс Касеорг

3

Java 7+, n = 50 дюймів ~ 30 сек на TIO

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Random;
class Main{
  public static void main(String[] a){

    int n=50;

    Random randomGenerator = new Random();
    int i = n+1;
    int squaredN = n*n;
    int[]randomIntegers = new int[i];
    randomIntegers[n] = squaredN;
    while(true){
      for(i=n; i-->1; ){
        randomIntegers[i] = randomGenerator.nextInt(squaredN);
      }
      Set<Integer> result = new HashSet<>();
      Arrays.sort(randomIntegers);
      for(i=n; i-->0; ){
        result.add(randomIntegers[i+1] - randomIntegers[i]);
      }
      if(!result.contains(0) && result.size()==n){
        System.out.println(result);
        return;
      }
    }
  }
}

Невикористана версія моєї відповіді на версію коду-гольфу цього виклику наразі лише з однією незначною зміною: java.util.Random#nextInt(limit)використовується замість (int)(Math.random()*limit)цілого числа в діапазоні [0, n), оскільки це приблизно вдвічі швидше .

Спробуйте в Інтернеті.

Пояснення:

Використовуваний підхід:

Код розділений на дві частини:

  1. Створіть список nкількості випадкових цілих чисел, до яких дорівнює n squared.
  2. Потім він перевіряє, чи всі значення унікальні і жодне не дорівнює нулю, і якщо будь-яке є фальси, воно спробує зробити крок 1 ще раз, промивши та повторюючи, поки ми не отримаємо результат.

Крок 1 виконується за допомогою наступних кроків:

1) Створення масиву n-1кількості випадкових цілих чисел у діапазоні [0, n squared). І додайте 0і n squaredдо цього списку. Це робиться у O(n+1)виконанні.
2) Потім буде сортувати масив із вбудованим. java.util.Arrays.sort(int[])Це робиться у O(n*log(n))виконанні, як зазначено в документах:

Сортує вказаний масив вкладень у порядку зростання. Алгоритм сортування - це налаштований квакспорт, адаптований з Джона Л. Бентлі та М. Дугласа Макілроя «Інженерія функції сортування», Програмне забезпечення та практика, Vol. 23 (11) С. 1249-1265 (листопад 1993 р.). Цей алгоритм пропонує продуктивність n * log (n) для багатьох наборів даних, які призводять до погіршення інших кварцкортів до квадратичної продуктивності.

3) Обчисліть різницю між кожною парою. Цей результуючий список різниць буде містити nцілі числа, які підсумовують n squared. Це робиться у O(n)виконанні.

Ось приклад:

// n = 4, nSquared = 16

// n-1 amount of random integers in the range [0, nSquared):
[11, 2, 5]

// Add 0 and nSquared to it, and sort:
[0, 2, 5, 11, 16]

// Calculate differences:
[2, 3, 6, 5]

// The sum of these differences will always be equal to nSquared
sum([2, 3, 6, 5]) = 16

Таким чином, ці три етапи вище досить хороші для виконання, на відміну від кроку 2 та циклу навколо всієї справи, що є базовою грубою силою. Крок 2 розділений на наступні кроки:

1) Список відмінностей уже збережений у a java.util.Set. Він перевірить, чи розмір цього набору дорівнює n. Якщо це так, це означає, що всі генеровані нами випадкові значення є унікальними.
2) І він буде також перевірити , що вона не містить 0в наборі, так як виклик просить випадкові величини в діапазоні [1, X], де Xє n squaredмінус суми [1, ..., n-1], як заявлено @Skidsdev в коментарях нижче.

Якщо будь-який з двох вищезазначених варіантів (не всі значення є унікальними, або нуль присутній), він генерує новий масив і встановить знову, скинувшись на етап 1. Це продовжується, поки ми не отримаємо результат. Через це час може дещо відрізнятися. Я бачив, як це закінчилося за 3 секунди один раз на TIO для n=50, але також і за 55 секунд один раз за n=50.

Доведіть рівномірність:

Я не зовсім впевнений, як довести це цілком чесно. java.util.Random#nextIntРівномірна точно, як описано в документації:

Повертає наступне псевдовипадкове, рівномірно розподілене intзначення з цієї послідовності генератора випадкових чисел. Загальний контракт Росії nextIntполягає в тому, що одне intзначення псевдовипадково генерується і повертається. Усі 2 32 можливих intзначення виробляються з (приблизно) однаковою ймовірністю.

Різниці між цими (відсортованими) випадковими значеннями самі по собі, звичайно, не є рівномірними, але множини в цілому є рівномірними. Знову ж таки, я не впевнений, як це довести математично, але ось скрипт, який буде розміщувати 10,000згенеровані набори (для n=10) на карті з лічильником , де більшість наборів унікальні; деякі повторюються двічі; і максимальне повторне виникнення зазвичай знаходиться в діапазоні [4,8].

Інструкції з Інсталяції:

Оскільки Java є досить відомою мовою з великою кількістю інформації про те, як створити та запустити код Java, я буду тримати це коротко.
Усі інструменти, які використовуються в моєму коді, доступні в Java 7 (можливо, навіть вже в Java 5 або 6, але давайте використовувати 7 на всякий випадок). Я впевнений, що Java 7 вже заархівований, тому я б запропонував завантажити Java 8 для запуску коду.

Думки про вдосконалення:

Я хотів би знайти покращення для перевірки нулів і перевірити, чи всі значення унікальні. Я міг би перевірити 0раніше, переконавшись, що випадкове значення, яке ми додаємо до масиву, вже не в ньому, але це означатиме пару речей: масив повинен бути ArrayListтаким, щоб ми могли використовувати метод вбудованого .contains; цикл у той час повинен бути доданий до тих пір, поки ми не знайдемо випадкове значення, якого ще немає у списку. Оскільки перевірка на нуль зараз робиться .contains(0)за допомогою набору (що перевіряється лише один раз), то, швидше за все, для продуктивності краще перевірити його в той момент, порівняно з додаванням циклу .containsв списку, який перевірятиметься хоча б nраз , але, швидше за все, більше.

Що стосується перевірки унікальності, то у нас є лише наша nкількість випадкових цілих чисел, яка дорівнює n squaredпісля кроку 1 програми, тому лише тоді ми можемо перевірити, чи всі вони унікальні чи ні. Можливо, можливо, зберегти сортований Список замість масиву та перевірити відмінності між ними, але я серйозно сумніваюся, що це покращить ефективність, ніж просто поставити їх у Setта перевірити, чи розмір цього набору є nодин раз.


1
якщо це допомагає швидкості, жодне число в наборі не може бути більше, ніж, n^2 - sum(1..n-1)наприклад, для n=5найбільшого дійсного числа5^2 - sum(1, 2, 3, 4) == 25 - 10 == 15
Skidsdev

@Skidsdev Спасибі, не думав про це. Хоча при моєму теперішньому підході я не можу його використовувати, оскільки я отримую відмінності між випадковими парами замість випадкових значень безпосередньо. Але це може бути корисно для інших відповідей.
Kevin Cruijssen

1
Розмір отриманого набору ніколи не може бути більшим, чи не nможе? У цьому випадку ви можете додати 0до набору, а потім перевірити, чи розмір (зараз) більше n. Це може статися лише в тому випадку, якщо всі відмінності не є нульовими та різними.
Ніл

@Neil О, це досить розумно, і я обов'язково використаю це у своєму кодовому гольф-відповіді на гольф на кілька байтів. Я не впевнений, чи поліпшить це показники тут, однак. HashSet.containsу більшості випадків близький O(1), а в гіршому - O(n)у Java 7 та O(log n)у Java 8+ (це було покращено після заміни ланцюга на виявлення зіткнень). Якщо мені дозволено повернути набір із доданим 0для перевірки, то він справді трохи кращий для продуктивності, але якщо мені доведеться зателефонувати set.remove(0);всередину if, я впевнений, що продуктивність дещо однакова.
Кевін Кройсейсен

О, я забув, вам також потрібно повернути набір ... неважливо.
Ніл

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