Arbitrary Randomness (ฉบับความเร็ว)


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, 16GB RAM, Intel i7-8750H 6 คอร์, 4.1GHz, GTX 1060 ในกรณีที่คุณต้องการละเมิด GPU) ดังนั้นโปรดให้คำแนะนำโดยละเอียดเพื่อเรียกใช้รหัสของคุณ เครื่องของฉัน
ตามคำขอรายการสามารถเรียกใช้อย่างใดอย่างหนึ่งผ่าน Debian บน WSL หรือบน Xubuntu Virtual Machine (ทั้งในเครื่องเดียวกันข้างต้น)

ผลงานที่ส่งเข้ามาจะถูกดำเนินการ 50 ครั้งติดต่อกันคะแนนสุดท้ายจะเป็นค่าเฉลี่ยของผลลัพธ์ทั้งหมด 50 รายการ



การเข้ารหัสฮาร์ดบิตทำได้น้อยกว่า 4000 ไบต์หรือไม่
Quintec

@Quintec ไม่ hardcoding เป็นช่องโหว่มาตรฐานดังนั้นถูกห้ามโดยค่าเริ่มต้น สิ่งที่ยุ่งยากคือการเข้ารหัสก็ถือว่าเป็นเกณฑ์ที่ไม่สามารถสังเกตเห็นได้ดังนั้นฉันจึงไม่สามารถพูดอย่างเป็นทางการว่า "ไม่มีการเข้ารหัส" เกินกว่าสิ่งที่ช่องโหว่ไม่สามารถทำได้ ดังนั้นขีด จำกัด ไบต์ ในคำอื่น ๆ : โปรดอย่า hardcode
Skidsdev

1
การส่งส่วนใหญ่จะใช้วิธีการปฏิเสธดังนั้นเวลาทำงานจะสุ่มและมีความแปรปรวนมาก นั่นทำให้เวลายากขึ้น
หลุยส์เมนโด

2
โอ้ฉันลืมไปแล้ว - เนื่องจากวิธีแก้ปัญหาบางอย่างอาจตัดสินใจใช้ RNG ที่มีคุณภาพต่ำเพื่อความรวดเร็วจึงจำเป็นต้องจัดเตรียมกล่องดำที่ใช้ n และสร้างตัวเลขสุ่มใน (1..n) และบังคับให้ทั้งหมด โซลูชั่นที่จะใช้
user202729

คำตอบ:


6

สนิม , n ≈ 1400

วิธีการวิ่ง

รูปร่างด้วยและวิ่งด้วยcargo build --releasetarget/release/arbitrary-randomness n

โปรแกรมนี้ทำงานได้เร็วที่สุดพร้อมหน่วยความจำมากมาย (ตราบใดที่มันยังไม่ได้แลกเปลี่ยนแน่นอน) คุณสามารถปรับการใช้หน่วยความจำได้โดยแก้ไขMAX_BYTESค่าคงที่ซึ่งปัจจุบันตั้งไว้ที่ 8 GiB

มันทำงานอย่างไร

ชุดถูกสร้างขึ้นโดยลำดับของการตัดสินใจแบบไบนารี (แต่ละหมายเลขมีทั้งภายในหรือภายนอกชุด) ซึ่งแต่ละความน่าจะเป็นคำนวณได้แบบ combinatorially โดยการนับจำนวนชุดที่เป็นไปได้หลังจากแต่ละตัวเลือกโดยใช้การเขียนโปรแกรมแบบไดนามิก

ใช้หน่วยความจำขนาดใหญ่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 GiB ประการที่สองเนื่องจาก TIO ไม่อนุญาตให้คุณเขียน a Cargo.tomlและขึ้นอยู่กับลังภายนอกเช่นrandฉันจึงดึงเข้าdrand48มาจากไลบรารี C โดยใช้ FFI ฉันไม่ได้สนใจที่จะเพาะมันดังนั้นรุ่น TIO จะให้ผลลัพธ์เหมือนกันทุกครั้งที่ใช้อย่าใช้รุ่น TIO สำหรับการเปรียบเทียบอย่างเป็นทางการ)


เนื่องจากรูปแบบจุดลอยตัวมีขอบเขต จำกัด จึงเป็นไปได้ที่จะปรับให้เหมาะสมln_add_expโดยการตรวจสอบว่าผลต่างสัมบูรณ์มีค่ามากกว่า ~ 15 หรือมากกว่านั้นซึ่งอาจเร็วกว่าหากมีการเพิ่มจำนวนมากดังกล่าว
user202729

@ user202729 ไม่เลยการln_add_expโทรเกือบทั้งหมดจะเกี่ยวข้องกับอินพุตที่เทียบเท่า
Anders Kaseorg

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;
      }
    }
  }
}

คำตอบของฉันที่ไม่ได้อัปโหลดสำหรับเวอร์ชันcode-golf ของความท้าทายนี้ในตอนนี้มีการเปลี่ยนแปลงเพียงเล็กน้อยเท่านั้น: java.util.Random#nextInt(limit)ใช้แทน(int)(Math.random()*limit)จำนวนเต็มในช่วง[0, n)เนื่องจากมันเร็วกว่าสองเท่า

ลองออนไลน์

คำอธิบาย:

วิธีการที่ใช้:

รหัสจะแบ่งออกเป็นสองส่วน:

  1. สร้างรายการของจำนวนของจำนวนเต็มแบบสุ่มจำนวนเงินนั้นไปnn 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))ประสิทธิภาพตามที่ระบุไว้ในเอกสาร:

เรียงลำดับอาร์เรย์ที่ระบุไว้ในลำดับตัวเลข อัลกอริธึมการเรียงลำดับนั้นเป็น quicksort ที่ปรับจูนดัดแปลงมาจาก Jon L. Bentley และ M. Douglas McIlroy ของ "ฟังก์ชั่นการจัดเรียงวิศวกรรม", การปฏิบัติซอฟต์แวร์และประสบการณ์ฉบับที่ 23 (11) หน้า 1249-1265 (พฤศจิกายน 2536) อัลกอริธึมนี้นำเสนอประสิทธิภาพของ n * log (n) ในชุดข้อมูลจำนวนมากที่ทำให้ Quicksorts อื่น ๆ ลดประสิทธิภาพการทำงานเป็นกำลังสอง

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) 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เป็นชุดเพื่อตรวจสอบว่าเป็นที่อธิบายไว้ในเอกสาร:

ส่งคืน pseudorandom ถัดไปซึ่งมีการกระจายintค่าอย่างสม่ำเสมอจากลำดับตัวสร้างตัวเลขสุ่มนี้ สัญญาทั่วไปของnextIntคือว่าหนึ่งintค่าจะถูกสร้างและส่งคืนแบบหลอกเทียม ค่าที่เป็นไปได้2 32intทั้งหมดนั้นสร้างขึ้นโดยมีความน่าจะเป็นเท่ากัน (โดยประมาณ)

ความแตกต่างระหว่างค่าสุ่มเหล่านี้ (เรียงลำดับ) ตัวเองแน่นอนว่าไม่เหมือนกัน แต่ชุดโดยรวมมีความเหมือนกัน อีกครั้งฉันไม่แน่ใจว่าจะพิสูจน์ทางคณิตศาสตร์ได้อย่างไร แต่นี่คือสคริปต์ที่จะนำ10,000ชุดที่สร้างขึ้น (สำหรับn=10) ลงในแผนที่ที่มีตัวนับซึ่งชุดส่วนใหญ่จะไม่ซ้ำกัน บางครั้งซ้ำสองครั้ง; [4,8]และเกิดขึ้นซ้ำแล้วซ้ำอีกสูงสุดมักจะอยู่ในช่วง

คำแนะนำในการติดตั้ง:

เนื่องจาก Java เป็นภาษาที่รู้จักกันดีและมีข้อมูลมากมายเกี่ยวกับวิธีการสร้างและเรียกใช้รหัส Java ฉันจะเก็บย่อ
เครื่องมือทั้งหมดที่ใช้ในรหัสของฉันมีอยู่ใน Java 7 (อาจจะมีอยู่แล้วใน Java 5 หรือ 6 แต่ลองใช้ 7 ในกรณี) ฉันค่อนข้างมั่นใจว่า Java 7 นั้นถูกเก็บถาวรแล้วดังนั้นฉันขอแนะนำให้ดาวน์โหลด Java 8 เพื่อเรียกใช้รหัสของฉัน

ความคิดเกี่ยวกับการปรับปรุง:

ฉันต้องการค้นหาการปรับปรุงสำหรับการตรวจสอบสำหรับศูนย์และตรวจสอบค่าทั้งหมดจะไม่ซ้ำกัน ฉันสามารถตรวจสอบ0ก่อนโดยตรวจสอบให้แน่ใจว่าค่าสุ่มที่เราเพิ่มไปยังอาร์เรย์ไม่ได้อยู่ในนั้น แต่มันหมายถึงสองสิ่ง: อาร์เรย์ควรเป็นArrayListเพื่อให้เราสามารถใช้วิธีการ builtin .contains; ควรเพิ่ม while-loop จนกว่าเราจะพบค่าสุ่มที่ยังไม่ได้อยู่ในรายการ เนื่องจากการตรวจสอบศูนย์เสร็จสิ้นด้วย.contains(0)การตั้งค่า (ซึ่งถูกตรวจสอบเพียงครั้งเดียว) จึงเป็นการดีกว่าสำหรับการตรวจสอบที่จุดนั้นเมื่อเทียบกับการเพิ่มการวนซ้ำกับ.containsในรายการซึ่งจะตรวจสอบอย่างน้อยnครั้ง แต่มีแนวโน้มมากขึ้น

สำหรับการตรวจสอบความเป็นเอกลักษณ์เรามีเพียงnจำนวนเต็มแบบสุ่มที่รวมไปถึงn squaredหลังขั้นตอนที่ 1 ของโปรแกรมดังนั้นจากนั้นเราสามารถตรวจสอบว่าทั้งหมดนั้นไม่ซ้ำกันหรือไม่ อาจเป็นไปได้ที่จะเก็บ Listable เรียงแทนที่จะเป็น Array และตรวจสอบความแตกต่างระหว่างกัน แต่ฉันสงสัยอย่างจริงจังว่ามันจะปรับปรุงประสิทธิภาพมากกว่าเพียงแค่ใส่ไว้ในSetและตรวจสอบว่าขนาดของ 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

@ Neil โอ้มันเป็นคนฉลาดและฉันจะใช้แน่นอนในการตอบรหัสกอล์ฟเพื่อเล่นกอล์ฟไม่กี่ไบต์ ฉันไม่แน่ใจว่ามันจะปรับปรุงประสิทธิภาพที่นี่หรือไม่ HashSet.containsเป็นกรณีส่วนใหญ่ที่ใกล้เคียงกับO(1)และในกรณีที่เลวร้ายที่สุดคือO(n)ใน Java 7 และO(log n)ใน Java 8+ (ได้รับการปรับปรุงหลังจากที่พวกเขาได้แทนที่การโยงด้วยการตรวจจับการชนกันของข้อมูล) ถ้าฉันอนุญาตให้ส่งคืนชุดพร้อมส่วน0ตรวจสอบเพิ่มเติมได้จริง ๆ แล้วมันก็ดีกว่าสำหรับการแสดง แต่ถ้าฉันต้องโทรset.remove(0);เข้าไปข้างในถ้าฉันค่อนข้างมั่นใจว่าประสิทธิภาพค่อนข้างเหมือนกัน
Kevin Cruijssen

โอ้ฉันลืมไปว่าคุณต้องคืนฉากด้วย ... ไม่เป็นไร
Neil

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.