任意随机(速度版)


10

给定integer n,计算n范围1..n^2(含)范围内的一组随机唯一整数,以使该集合的总和等于n^2

在这种情况下,随机是指有效输出之间的一致随机。给定的每个有效输出n必须有统一的机会被生成。

例如,n=3应该有三分之一的机会的各个输出6, 1, 23, 5, 14, 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虚拟机上运行(两者都与上述相同)

提交将连续运行50次,最终得分将是全部50个结果的平均值。



如果少于4000个字节,是否可以进行硬编码?
Quintec '18

@Quintec否,硬编码是一个标准漏洞,因此默认情况下是禁止的。棘手的是,硬编码也被认为是不可观察的标准,因此我不能在漏洞允许的范围之外正式说“无硬编码”。因此,字节限制。换句话说:不要硬编码
Skidsdev,

1
大多数提交将使用拒绝方法,因此运行时间将是随机的并且具有很大的可变性。这使时间安排变得困难
Luis Mendo

2
哦,我忘了-因为某些解决方案可能会决定使用低质量的RNG来提高速度,所以可能有必要提供一个黑盒例程,该例程需要n并在(1..n)中产生一个随机数,并强制所有解决方案。
user202729

Answers:


6

ñ ≈1400

怎么跑

使用构建cargo build --release并运行target/release/arbitrary-randomness n

该程序在有大量内存的情况下运行速度最快(当然,只要不进行交换)。您可以通过编辑MAX_BYTES当前设置为8 GiB 的常数来调整其内存使用量。

怎么运行的

该集合是由一系列二进制决策(每个数字位于集合内部或外部)构成的,通过使用动态规划计算每次选择后可构造的可能集合的数目,可以组合计算每个概率。

使用此二项式分区策略的版本可以减少大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,我改用drand48C从库中获取FFI。我并没有为它播种,所以TIO版本将在每次运行中产生相同的结果。请勿将TIO版本用于官方基准测试。)


由于浮点格式是有限的,因此可以ln_add_exp通过检查绝对差是否大于〜15左右来进行优化,如果有很多这样的加法,则可能会更快。
user202729

@ user202729不,几乎所有ln_add_exp呼叫都涉及可比较的输入。
安德斯·卡塞格

3

Java 7+,在TIO上约30秒内n = 50

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. 然后,它会检查所有值是否都是唯一的,并且都不是零;如果任何一个为false,它将再次尝试步骤1,漂洗并重复直到得到结果。

步骤1包含以下子步骤:

1)生成一个n-1范围为的随机整数数组[0, n squared)。并添加0n squared到此列表中。这是在O(n+1)性能上完成的。
2)然后它将使用内置数组对数组进行排序java.util.Arrays.sort(int[]),这是在O(n*log(n))性能上完成的,如docs中所述:

将指定的int数组按升序排序。排序算法是一种经过调整的快速排序,它是根据乔恩·本特利(Jon L. Bentley)和道格拉斯·麦克罗伊(M. Douglas McIlroy)的“工程设计排序功能”(软件实践和经验,第1卷,第1期)改编而来的。23(11)P.1249-1265(1993年11月)。该算法在许多数据集上提供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)差异列表已保存在中java.util.Set。它将检查此Set的大小是否等于n。如果是,则表示我们生成的所有随机值都是唯一的。
2)并且还将检查其是否包含0在Set中,因为质询要求范围为的随机值[1, X],其中Xn squared减去总和[1, ..., n-1],如@Skidsdev在下面的注释中所述。

如果以上两个选项中的任何一个(并非所有值都是唯一的或存在零),它将生成一个新数组,并通过重置为步骤1再次进行设置。这将继续直到获得结果。因此,时间可能相差很大。我看到它在TIO上3秒就能完成n=50,但在55秒内就能完成n=50

均匀性证明:

我不太确定如何证明这一点是完全诚实的。该java.util.Random#nextInt可以肯定的是统一的,在该文档中描述:

int从该随机数生成器的序列返回下一个伪随机,均匀分布的值。的一般约定nextInt是,一个int值是伪随机生成并返回的。产生(近似)相等概率的所有2 32个可能int值。

这些(排序的)随机值之间的差异当然不是统一的,但总体而言,这些集合是统一的。同样,我不确定如何用数学方法证明这一点,但是这里有一个脚本,它将10,000生成的(for n=10)集放在带有counter的Map中,其中大多数集都是唯一的;一些重复两次;最大重复发生通常在范围内[4,8]

安装说明:

由于Java是一种非常知名的语言,其中包含大量有关如何创建和运行Java代码的信息,因此我将在此简短介绍。
我的代码中使用的所有工具都可以在Java 7中使用(也许甚至在Java 5或6中也可以使用,但以防万一,让我们使用7)。我很确定Java 7已经被归档了,所以我建议下载Java 8来运行我的代码。

关于改进的想法:

我想找到一种改进,以检查零,并检查所有值是否唯一。我可以0通过确保没有添加到数组中的随机值来进行检查,但这将意味着两件事:数组应该是一个,ArrayList因此我们可以使用内建方法.contains;应该添加一个while循环,直到我们发现List中还没有的随机值为止。由于检查零与现在做.contains(0)的设置(这是只检查一次),它是最有可能更好的性能在该点检查,相较于与加入环.contains上的名单,这将至少被检查n倍,但很有可能更多。

至于唯一性检查,我们只有n随机整数的数量n squared在程序的第1步之后求和,因此只有这样才能检查所有整数是否唯一。可能可以保留一个可排序的List而不是array,并检查它们之间的差异,但是我严重怀疑,与仅将它们放入a Set并检查Set的大小是否为n一次相比,它会提高性能。


1
如果它有助于提高速度,则集合中的任何数字都不能大于n^2 - sum(1..n-1)例如n=5最大有效数字,例如5^2 - sum(1, 2, 3, 4) == 25 - 10 == 15
Skidsdev '18

@Skidsdev谢谢,还没想到。尽管使用当前方法无法使用它,因为我得到了随机对之间的差异,而不是直接获得随机值。但这可能对其他答案很有用。
凯文·克鲁伊森

1
结果集的大小不能超过n,可以吗?在这种情况下,您可以添加0到集合中,然后检查大小(现在)是否大于n。仅当差异均为非零且不同时,才会发生这种情况。
尼尔

@Neil哦,这很聪明,我一定会在我的代码高尔夫球答案中使用它,以节省几个字节。不过,我不确定是否会提高此处的性能。HashSet.contains在大多数情况下都接近O(1),在最坏的情况下是O(n)在Java 7和O(log n)Java 8+中(它们被冲突检测替换为链接后得到了改进)。如果允许我返回带有添加项的Set 0进行检查,那么它的性能确实确实要好一些,但是如果我必须set.remove(0);在if中调用,我可以肯定性能是相同的。
凯文·克鲁伊森

哦,我忘记了您也需要退还套装...没关系。
尼尔

1

Mathematica n = 11

(While[Tr@(a=RandomSample[Range[#^2-#(#-1)/2],#])!=#^2];a)&     
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.