【问题标题】:Algorithm to print out a shuffled list, in-place and with O(1) memory打印出一个随机列表的算法,就地和 O(1) 内存
【发布时间】:2009-12-08 12:37:28
【问题描述】:

看了this question之后,我开始怀疑:有没有可能有一个不修改或复制原始列表的洗牌算法?

说清楚:

假设你得到了一个对象列表。列表大小可以是任意的,但假设它非常大(例如,10,000,000 个项目)。您需要以随机顺序打印出列表中的项目,并且需要尽可能快地完成。但是,您不应该:

  • 复制原始列表,因为它非常大,复制会浪费大量内存(可能会达到可用 RAM 的限制);
  • 修改原始列表,因为它以某种方式排序,而稍后的其他部分取决于它的排序。
  • 创建一个索引列表,因为列表非常大,复制需要太多时间和内存。 (澄清:这是指任何其他列表,其元素数量与原始列表相同)。

这可能吗?

添加:更多说明。

  1. 我希望列表以真正随机的方式随机排列,所有排列的可能性都相同(当然,假设我们有一个合适的 Rand() 函数开始)。
  2. 关于我创建一个指针列表、索引列表或任何其他与原始列表具有相同数量元素的列表的建议,被原始问题明确认为是低效的。您可以根据需要创建其他列表,但它们应该比原始列表小几个数量级。
  3. 原始列表就像一个数组,您可以通过它在 O(1) 中的索引从中检索任何项目。 (所以没有双向链表的东西,你必须遍历列表才能找到你想要的项目。)

添加 2:好的,让我们这样说吧:你有一个 1TB 的硬盘,里面装满了数据项,每个 512 字节大(一个扇区)。您想在洗牌所有项目时将所有这些数据复制到另一个 1TB 硬盘。您希望尽可能快地执行此操作(单次传递数据等)。你有 512MB 的可用 RAM,不要指望交换。 (这是一个理论上的场景,我在实践中没有这样的东西。我只是想找到完美的algorithm.item。)

【问题讨论】:

  • 我认为这个问题的答案可能取决于列表的内部实现。需要明确的是,这很可能是一个双向链表,对吧?
  • paxdiablo 在这篇文章中讨论了 Knuth 的洗牌 stackoverflow.com/questions/1858610/… 我想这就是你所追求的。
  • 再想一想,虽然它以 O(n) 运行,但您需要创建一个该长度的 int 数组 - 但我认为这是您可以达到的最佳效果 - 您应该以某种方式跟踪选定的项目。
  • 答案是“不”,但这个空白处没有足够的空间来解释原因:-)
  • 如果你破产了,你还有比洗牌更重要的事情要担心。去找工作吧:-)

标签: algorithm theory shuffle


【解决方案1】:

嗯,这有点取决于您除了洗牌之外的随机性,即所有洗牌是否应该尽可能地发生,或者分布是否可以倾斜。

有一些数学方法可以产生 N 个整数的“随机”排列,所以如果 P 是从 0..N-1 到 0..N-1 的排列,您只需将 x 从 0 迭代到 N -1 并输出列表项 L(P(x)) 而不是 L(x) 并且您已经获得了改组。可以得到这样的排列,例如使用模运算。例如,如果 N 是素数,则 P(x)=(x * k) mod N 是任何 0

应该注意,模幂运算是许多加密算法(例如 RSA、Diffie-Hellman)的基础,并且被该领域的专家认为是一种强伪随机运算。

另一种简单的方法(不需要素数)是首先扩展域,以便您考虑 M 而不是 N,其中 M 是 N 之上 2 的最小幂。例如如果 N=12,则设置 M=16。然后你使用双射位操作,例如

P(x) = ((x ^ 0xf) ^ (x << 2) + 3) & 0xf

然后当你输出你的列表时,你将 x 从 0 迭代到 M-1 并且仅当 P(x) 实际上是

一个“真正的、无偏的随机”解决方案可以通过修复一个加密强的分组密码(例如 AES)和一个随机密钥 (k),然后迭代序列来构建

AES(k, 0), AES(k, 1), ...

并从序列 iff AES(k,i)

【讨论】:

  • 好吧,我想要真正的无偏随机。我知道主要的解决方案,但这并不是随机的。
  • 强 PRNG 的问题是你会得到重复。这需要一个位图来防止多次“挑选”原始列表中的某些元素。
  • 好吧,正如我上面指出的,有一些非重复序列作为排列被认为是强随机的,例如模幂运算或现代分组密码。
  • 我认为这个答案是你能得到的最接近的答案。任何真正随机的算法都需要跟踪已选择的项目,因为根据定义,它是不可预测的。这意味着要么修改原始列表,要么创建一个单独的数据结构。
  • 喜欢这个答案。我不会将这些用于任何加密,但对于游戏来说,这个想法将是一种享受。
【解决方案2】:

您不能复制、修改或跟踪您访问过的元素?我会说这是不可能的。除非我误解了你的第三个标准。

我的意思是你不能说,制作一个包含 10,000,000 个相应布尔值的数组,当你打印相应的元素时设置为 true。并且您不能列出 10,000,000 个索引、打乱列表并按该顺序打印出元素。

【讨论】:

  • 是的,您对我的理解是正确的。好吧,如果您想办法做到这一点,而无需制作另一个与他输入的大小相同的列表,您就可以跟踪您访问过的项目。如果你列出一个类似 log(N) 的大小列表,那么我会很满意。
【解决方案3】:

这是一个非常简单的证明,证明任何 PRNG 方案都不起作用:

PRNG 的想法有两个阶段:首先,选择一个 PRNG 及其初始状态;其次,使用 PRNG 对输出进行洗牌。嗯,有 N! 个可能的排列,所以你至少需要 N! 个不同的可能开始状态,才能进入阶段 2。这意味着在阶段 2 开始时你必须至少有 log2 N! 位状态,这是不允许的。

但是,这并不排除算法在运行时从环境中接收新随机位的方案。比如说,可能有一个 PRNG,它懒惰地 读取它的初始状态,但保证不会重复。我们能证明不存在吗?

假设我们确实有一个完美的洗牌算法。想象一下,我们开始运行它,当它运行到一半时,我们让计算机进入睡眠状态。现在程序的完整状态已经保存在某个地方。令 S 为程序在此中途可能处于的所有可能状态的集合。

由于算法是正确的并且保证会终止,所以有一个函数 f,给定保存的程序状态加上任何足够长的位串,产生一个有效的磁盘读取和写入序列完成洗牌。计算机本身就实现了这个功能。但将其视为一个数学函数:

f : (S × bits) → 读写序列

然后,简单地说,存在一个函数g,它给定保存的程序状态,产生一组尚未读取和写入的磁盘位置。 (只需将一些任意位字符串传递给 f,然后查看结果。)

g : S读取和写入的位置集

证明的剩余部分是证明g的域至少包含NCN/2 无论选择何种算法,不同的集合。如果这是真的,那么S的元素必须至少有那么多,因此程序的状态必须至少包含log2NCN/2个位在中间标记,违反要求。

不过,我不确定如何证明最后一点,因为 要么 要读取的位置集 位置集-to-write 可以是低熵,具体取决于算法。我怀疑有一些明显的信息论原则可以解决问题。标记这个社区维基,希望有人会提供它。

【讨论】:

  • 哇,酷。其实下半场我并没有跟着你,但看起来很严重,我不怀疑你。
  • 确实,如果所有排列都需要尽可能地可能,您就不能使用任何标准的、现成的 PRNG,因为当 N 变大时,N! >> 2**K 其中 K = PRNG 内部状态的位大小。但是,我认为以这种严格的方式从原始帖子中解释“真正的随机”是错误的,因为它甚至会使可能使用任意数量空间的解决方案变得非常困难。我认为最初的问题更多是关于空间使用,“真正的随机”意味着“真正的伪随机”,这在计算机科学中是标准的。
  • 好吧,提问者说了好几遍他或她所说的“真正的随机”是什么意思。他或她勾选了这个答案。所以。
  • @antti.huima:还要注意,这个答案的第二部分试图争辩说指定的任务是不可能的即使我们认为一些完美的随机性来源是理所当然的 (即“算法在运行时从环境中接收新的随机位”)。
  • 从理论上讲,我认为一个合理的问题可能是:假设给定一个函数,该函数假定时间为常数,并且对于任意一对整数 X 和 Y 将产生一个真正的随机整数平均分布在 0 和 (X-1) 之间。如果最大整数幅度是纸牌数量的恒定幂,则产生 N 张牌的完美洗牌的最佳时间/空间折衷曲线是什么?它可以在 O(1) 空间和 O(N^2) 时间内完成,通过从 0 到 N-1,然后从 0 到 N-2 等选择数字,但将每个数字增加...
【解决方案4】:

这 10,000,000 个项目只是对实际项目的引用(或指针),因此您的列表不会那么大。对于所有引用 + 该列表的内部变量的大小,在 32 位架构上只有 ~40MB。如果您的商品小于参考尺寸,您只需复制整个列表。

【讨论】:

    【解决方案5】:

    真正随机数生成器无法做到这一点,因为您必须:

    • 记住已经选择了哪些数字并跳过它们(这需要一个 O(n) 的布尔值列表,并且随着您跳过越来越多的数字,运行时间会逐渐恶化);或
    • 在每次选择后减少池(这需要修改原始列表或单独的 O(n) 列表来修改)。

    这些都不是你的问题的可能性,所以我不得不说“不,你不能这样做”。

    在这种情况下,我倾向于使用已使用值的位掩码,但不会跳过,因为如前所述,随着使用值的累积,运行时间会变得更糟。

    位掩码将大大优于 39Gb 的原始列表(1000 万位仅约 1.2M),即使它仍然是 O(n),也比您要求的要少很多数量级。

    为了解决运行时问题,每次只生成一个随机数,如果相关的“已使用”位已经设置,则通过位掩码向前扫描,直到找到一个 not 设置。

    这意味着您不会四处闲逛,急需随机数生成器为您提供尚未使用的数字。运行时间只会变得与扫描 1.2M 数据所花费的时间一样糟糕。

    当然,这意味着在任何时候选择的特定数字都是基于已经选择的数字而倾斜的,但是,由于这些数字无论如何都是随机的,所以倾斜是随机的(如果数字 不是 一开始真的是随机的,那么倾斜就无关紧要了)。

    如果您想要更多种类,您甚至可以交替搜索方向(向上或向下扫描)。

    底线:我不相信您所要求的是可行的,但请记住,我以前犯过错误,因为我的妻子会迅速而频繁地证明:-) 但是,与所有事情一样,有通常是解决此类问题的方法。

    【讨论】:

    • 好吧,我同意你关于真正随机数生成器的观点。我在想一个奇怪的 PRNG 函数,它会生成一个不重复的整数列表,可以作为原始列表的索引。好吧,不重复只与数组的大小一样大。之后,它们自然而然地开始重复,甚至可能以相同的顺序重复。
    • 当然,每个 PRNG 都需要一个种子,一些原始熵作为基础。如果我有 1,000,000,000 个项目,则有 1,000,000,000 个!可能的排列很多。我什至不知道有多少,但 PRNG 种子至少需要有这么多的变化,否则分布会严重偏斜。这种大小的二进制数会小于 32 位整数索引列表吗?
    • 如果是,那么这个种子可以由真正的随机数生成器生成,然后 PRNG 会处理剩下的事情。
    【解决方案6】:

    听起来不可能。

    但是 10,000,000 个 64 位指针只有大约 76MB。

    【讨论】:

    • 好吧,我提高了一点赌注。 :)
    • @Vilx 你只说项目重 1TB,而不是指针有 1T。这完全取决于您的项目数量,而不是所有这些项目的大小。
    • 在我上面的例子中,有 1G 的指针,对于 512MB 的 RAM 来说太多了。
    【解决方案7】:

    线性反馈移位寄存器几乎可以做你想做的事——生成一个数字列表,直到某个限制,但以(合理的)随机顺序。它产生的模式在统计上与您对尝试随机性的期望相似,它甚至不接近加密安全。 Berlekamp-Massey 算法允许您根据输出序列对等效 LFSR 进行逆向工程。

    鉴于您需要约 10,000,000 个项目的列表,您需要一个 24 位最大长度的 LFSR,并简单地丢弃大于列表大小的输出。

    就其价值而言,与同期典型的线性同余 PRNG 相比,LFSR 通常相当快。在硬件中,LFSR 非常简单,由一个 N 位寄存器和 M 个 2 输入 XOR(其中 M 是抽头数——有时只有几个,很少超过一个半打左右)。

    【讨论】:

    • 这也是一个非常好的主意——一些游戏程序员会感谢你指出这一点。
    【解决方案8】:

    如果有足够的空间,您可以将节点的指针存储在数组中,创建位图并获取指向下一个选定项的随机整数。如果已经选择(您将其存储在位图中),则获取最接近的一个(左侧或右侧,您可以随机化),直到没有剩余项目。

    如果没有足够的空间,那么你可以在不存储节点指针的情况下做同样的事情,但是时间会受到影响(这是时空权衡☺)。

    【讨论】:

    • OP 已经说过他们不想创建一个相同大小的索引数组,指针数组相当于。如果他们确实允许这样做,我不明白为什么你需要这个模糊的“随机整数位图”,如果你被允许拥有第二个容器,你可以简单地使用std::shuffle然后迭代它,这会更简单并且(我认为)更快。
    【解决方案9】:

    您可以使用分组密码创建伪随机的“安全”排列 - 请参阅 here。他们的关键见解是,给定一个 n 位长度的分组密码,您可以使用“折叠”将其缩短为 m

    【讨论】:

      【解决方案10】:

      本质上,您需要的是一个随机数生成器,它每个生成数字 0..n-1 一次。

      这是一个半生不熟的想法:你可以通过选择一个略大于 n 的素数 p,然后选择一个介于 1 和 p-1 之间的随机 x,它在乘法组 mod p 中的顺序是 p-1(选择随机 xs 并测试哪些满足 x^i != 1 for i = n 的那些,这会为您提供一系列要打印的索引。

      这不是非常随机的,但是您可以多次使用相同的技术,将上面的索引 (+1) 用作另一个生成器 x2 模另一个素数 p2 的指数(您需要 n

      【讨论】:

        【解决方案11】:

        我的解决方案取决于一些巧妙计算的数字的数学属性

        range = array size
        prime = closestPrimeAfter(range)
        root = closestPrimitiveRootTo(range/2)
        state = root
        

        通过这种设置,我们可以重复计算以下内容,它会以看似随机的顺序对数组的所有元素进行一次精确的迭代,之后它将再次以相同的顺序循环遍历数组。

        state = (state * root) % prime
        

        我在 Java 中实现并测试了它,所以我决定将我的代码粘贴在这里以供将来参考。

        import java.math.BigInteger;
        import java.util.ArrayList;
        import java.util.Arrays;
        import java.util.Random;
        
        public class PseudoRandomSequence {
        
            private long            state;
            private final long  range;
            private final long  root;
            private final long  prime;
            //Debugging counter
            private int             dropped = 0;
        
            public PseudoRandomSequence(int r) {
                range = r;
                prime = closestPrimeAfter(range);
                root = modPow(generator(prime), closestPrimeTo(prime / 2), prime);
                reset();
                System.out.println("-- r:" + range);
                System.out.println("   p:" + prime);
                System.out.println("   k:" + root);
                System.out.println("   s:" + state);
            }
        
            // https://en.wikipedia.org/wiki/Primitive_root_modulo_n
            private static long modPow(long base, long exp, long mod) {
                return BigInteger.valueOf(base).modPow(BigInteger.valueOf(exp), BigInteger.valueOf(mod)).intValue();
            }
        
            //http://e-maxx-eng.github.io/algebra/primitive-root.html
            private static long generator(long p) {
                ArrayList<Long> fact = new ArrayList<Long>();
                long phi = p - 1, n = phi;
                for (long i = 2; i * i <= n; ++i) {
                    if (n % i == 0) {
                        fact.add(i);
                        while (n % i == 0) {
                            n /= i;
                        }
                    }
                }
                if (n > 1) fact.add(n);
                for (long res = 2; res <= p; ++res) {
                    boolean ok = true;
                    for (long i = 0; i < fact.size() && ok; ++i) {
                        ok &= modPow(res, phi / fact.get((int) i), p) != 1;
                    }
                    if (ok) {
                        return res;
                    }
                }
                return -1;
            }
        
            public long get() {
                return state - 1;
            }
        
            public void advance() {
                //This loop simply skips all results that overshoot the range, which should never happen if range is a prime number.
                dropped--;
                do {
                    state = (state * root) % prime;
                    dropped++;
                } while (state > range);
            }
        
            public void reset() {
                state = root;
                dropped = 0;
            }
        
            private static boolean isPrime(long num) {
                if (num == 2) return true;
                if (num % 2 == 0) return false;
                for (int i = 3; i * i <= num; i += 2) {
                    if (num % i == 0) return false;
                }
                return true;
            }
        
            private static long closestPrimeAfter(long n) {
                long up;
                for (up = n + 1; !isPrime(up); ++up)
                    ;
                return up;
            }
        
            private static long closestPrimeBefore(long n) {
                long dn;
                for (dn = n - 1; !isPrime(dn); --dn)
                    ;
                return dn;
            }
        
            private static long closestPrimeTo(long n) {
                final long dn = closestPrimeBefore(n);
                final long up = closestPrimeAfter(n);
                return (n - dn) > (up - n) ? up : dn;
            }
        
            private static boolean test(int r, int loops) {
                final int array[] = new int[r];
                Arrays.fill(array, 0);
                System.out.println("TESTING: array size: " + r + ", loops: " + loops + "\n");
                PseudoRandomSequence prs = new PseudoRandomSequence(r);
                final long ct = loops * r;
                //Iterate the array 'loops' times, incrementing the value for each cell for every visit. 
                for (int i = 0; i < ct; ++i) {
                    prs.advance();
                    final long index = prs.get();
                    array[(int) index]++;
                }
                //Verify that each cell was visited exactly 'loops' times, confirming the validity of the sequence
                for (int i = 0; i < r; ++i) {
                    final int c = array[i];
                    if (loops != c) {
                        System.err.println("ERROR: array element @" + i + " was " + c + " instead of " + loops + " as expected\n");
                        return false;
                    }
                }
                //TODO: Verify the "randomness" of the sequence
                System.out.println("OK:  Sequence checked out with " + prs.dropped + " drops (" + prs.dropped / loops + " per loop vs. diff " + (prs.prime - r) + ") \n");
                return true;
            }
        
            //Run lots of random tests
            public static void main(String[] args) {
                Random r = new Random();
                r.setSeed(1337);
                for (int i = 0; i < 100; ++i) {
                    PseudoRandomSequence.test(r.nextInt(1000000) + 1, r.nextInt(9) + 1);
                }
            }
        
        }
        

        这受到了 2D 图形“溶解”效果的小型 C 实现的启发,如 Graphics Gems vol. 中所述。 1 这反过来又是对 2D 的适应,对称为“LFSR”的机制进行了一些优化(维基百科文章 here,原始溶解.c 源代码 here)。

        【讨论】:

        • 我认为这在“我希望列表以真正随机的方式随机排列,所有排列可能性相同”标准上失败了。但这对于许多实际用途来说可能已经足够了。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-11-22
        • 2012-04-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多