请记住,我在这里描述的算法基于列表 [1, 2, ... N-1] (长度为 N-1)。如果您坚持使用列表 [0, 1, ..., N](长度为 N+1),请进行所需的细微修改。此外,为了简洁起见,我使用 % 操作数的方式与大多数编程语言略有不同:a % b 取 1 和 b 之间的值,而不是 0 和 b - 1 之间的值,但背后的主要思想当然没有改变,因此 a % b 的值为 1 和 b 之间的整数,与 a 模 b 一致。
如果您仔细阅读本文,您会很明显地发现,生成的随机播放根本不是随机的。然而,如果参数选择得当,模式将不容易识别(我的模幂运算的基本思想来自密码学,其中具有不可识别的模式和不可恢复的函数非常重要)。
这更多的是与语言无关的算法描述,而不是实际的编程解决方案。我不会详细介绍有效实施和您可能遇到的陷阱。我希望它仍然有帮助。我还在 python 中编写了其中的某些部分,因此我可以提供进一步的帮助,甚至在需要时分享我的代码,但这之前需要一些完成和重构。
使用求幂代替乘法来消除模式
您对 f(x) = t * x % N(您选择为 911)的初步尝试显示了一些模式,因为乘法保持线性(在它的“模数”含义中)。
如果您使用幂而不是乘法,您可以给人更多随机的感觉。像 f(x) = t ^ x % N 之类的东西。但是,必须明智地选择 t(就像在乘法情况下一样,成为 N 的互质),并且此公式给出的输出不会提供不同的结果仅在 N 为素数的情况下,不同 x 值的数字。
大学数学即将到来,请耐心等待,我会尽量保持简单。
我们需要使用原根。相关的维基百科文章 http://en.wikipedia.org/wiki/Primitive_root_modulo_n可能会有很大帮助,但基本思想是精心选择的基数的幂的余数取 1 到 N-1 之间的所有值。例如
3^1 = 3
3^2 = 9 = 2 (mod 7)
3^3 = 27 = 6 (mod 7)
3^4 = 81 = 4 (mod 7)
3^5 = 243 = 5 (mod 7)
3^6 = 729 = 1 (mod 7)
都是不同的(从这一点开始,这些值从头开始重复:3^7 = 3^1 (mod 7)、3^8 = 3^2 (mod 7),依此类推)。
所以,如果你的 N 是 7,那么 3 就可以很好地成为 t。对于 1 到 6 之间的 x 值,可以使用 f(x) = (3 ^ x) % 7。
这会产生以下 f:
f(1) = 3
f(2) = 2
f(3) = 6
f(4) = 4
f(5) = 5
f(6) = 1
引入转变,提供一些额外的随机效果
如果你稍微玩一下这个,你会注意到,N-1 总是被打乱为 1。如果你想避免这种情况,我们可以更进一步,选择一个任意数字 k 在求幂后添加。也就是说,使用 g(x) = (f(x) + k) % (N-1) = ((t ^ x) % N + k) % (N-1),在我们的示例中令 k 为 2,产生排列:
g(1) = 5
g(2) = 4
g(3) = 2
g(4) = 6
g(5) = 1
g(6) = 3
如何选择底座
现在你有了基本的感觉。但当 N 不是 7 时,一般如何使用呢?
问题的关键是选择参数 t,在我们的示例中为 3。
不幸的是,这通常是一个难题(数学家称之为寻找原根 https://math.stackexchange.com/questions/124408/finding-a-primitive-root-of-a-prime-number),据我所知,没有任何易于解释的开箱即用的解决方案。
但这只是问题的一部分。更可悲的是,如果 N 是合数,原根甚至不起作用。例如,如果 N=6,则不存在任何数字 t 的表达式 t^x modulo 6 取 1 到 5 之间的所有值。
不过解决这部分并不是太难。
复合 N 可以做什么
如果 N 是合数,我们应该找到一个稍大一点的质数 P,并以基于该质数的算法为基础,通过将越界数字替换为洗牌后的值(并在需要时进行迭代) 。
例如,如果 N 为 6,我们可以选择 P 为 7,并使用之前构造的 g(x)。
g(1) = 5 ok (5<=N-1 holds) h(1) = 5
g(2) = 4 ok h(2) = 4
g(3) = 2 ok => h(3) = 2
g(4) = 6 too large, using g(g(4)) = g(6) = 3 h(4) = 3
g(5) = 1 ok h(5) = 1
为了安全起见,我给出了另一个 N=4 的例子,其中我们使用之前计算的 P=7 的解决方案。
g(1) = 5, g(5) = 1 h(1) = 1
g(2) = 4, g(4) = 6, g(6) = 3 => h(2) = 3
g(3) = 2 h(3) = 2
现在应该清楚了。选择接近 N 的 P 是明智的,这样 g 的越界值就不需要进行太多的重新计算。
回到寻找t
所以我们剩下的唯一问题就是找到可以用作幂运算底数的原根。
如果我之前链接的页面上的数学引起了一些本能的厌恶,我有一些好消息给你:t 的可能好的值在区间 [2, N-1] 中是密集的,所以即使是随机猜测也可能有所帮助。
有一些详细信息如何有效地验证随机选择的 t 是否真的对链接页面上的我们有利,但除非您正在处理非常大的数字,否则您可以只进行求幂并检查数字 1 是否出现早于 ( t 的 N-1) 次方(也许您还记得我注意到 t^x=1 (mod N) 在 x=N-1 的情况下始终成立,因此 1 的较早出现会破坏唯一性)。
我建议在 N/2 左右寻找合适的 t(意味着数量级 - 对于 P=91367,t=54949 效果很好)。如果您选择的 t 太低(例如 t=2),您可以轻松发现由某些相邻 x 值求幂引起的模式(2+k、4+k、8+k...将出现在彼此)。如果 t 太接近 N,则在相同奇偶校验的连续 x 值中查看 f(x) 时,可能会出现类似的现象。 t 的一个好的选择应该涵盖这些模式,并以足够随机的结果结束。
Summary
再次,这是算法的步骤
(N 给定)
找到一个比 N 稍大的素数 P
选择 1 和 P-1 之间的任意数字 k
找到 t,它是 P 的原根
(对于给定的 x,输出随机 h(x) 是)
计算
f(x) = (t ^ x) % P
计算
g(x) = (f(x) + k) % (P-1)
计算
h(x) = g(x) if g(x)<=N-1,
iterate the calculations with x = g(x) otherwise