将物品按某种顺序排列,并给每个物品一个编号。为用户维护一个从零开始的计数器。为每个用户生成一个随机(ish)密钥。选择一个函数,该函数将 0...N 范围内的整数和一个键的组合映射到 0...N 范围内的另一个整数,这样对于键外的任何值,整数之间的映射都是双射的。当您需要一个项目时,获取计数器的值,将它与键一起放入函数中,然后使用该数字索引到项目列表中。增加计数器。
您需要为每个用户存储的只是密钥和计数器,并且您可以保证不会重复任何项目。它基本上是一种即时构建巨大排列的方式。
问题当然是选择功能!
一个简单的例子是f(counter, key) = (counter + key) mod N,虽然这可行,但它根本不会随机化项目,所以每个人都会以相同的顺序获得项目,只是从不同的地方开始。如果 N + 1 是素数,您可以使用 F(counter, key) = (((counter + 1) * (key + 1)) mod (N + 1)) - 1,这会很好。如果范围是 0...2^64,您可以使用任何具有 64 位块的块密码,这将提供出色的随机化。但尚不清楚您是否可以将其调整为更小的尺寸。
我会稍微挖掘一下,看看我是否能想出一个通用的函数。这是我自己遇到的一个问题,如果最终能得到一个好的答案,那就太好了。如果我找到任何东西,我会编辑这个答案。
编辑:好的,我们开始吧!我从a thread i started on sci.crypt 那里得到了关键的想法,尤其是从一位全能的用户网英雄 Paul Rubin 那里得到的。
策略略有改变。将您的项目放在一个列表中,以便可以通过索引访问它们。选择一个数字 B,这样 2^B >= N - 任何值都可以,但你真的想要最小的一个(即 N-1 的以 2 为底的对数的上限)。然后我们将 2^B 称为 M。设置一个从 0 运行到 M-1 的计数器,并为每个用户设置一个密钥,该密钥可以是任意字节字符串 - 随机整数可能是最简单的。在整数 0 ... M-1 的集合上设置魔法函数,它是一个双射,也就是一个排列(见下文!)。当你想要一个item的时候,取counter的值,把counter加1,然后通过Magic Function把原来的值放进去,得到一个index;如果索引大于N-1,则将其丢弃,并重复该过程。重复直到你得到一个小于 N 的索引。有时,你需要经过一个或多个无用的索引才能得到一个好的索引,但平均而言,它需要 M/N 次尝试,这还不错(如果您选择尽可能小的 B,则小于 2)。
顺便说一句,计算以 2 为底的对数的上限很容易:
int lb(int x) {
int lb = 0;
while (x > 0) {
++lb;
x >>= 1;
}
return lb;
}
好的,魔术函数,它将一个从 0 ... M-1 的数字映射到另一个这样的数字。我在上面提到了分组密码,这就是我们要使用的。除了我们的块是 B 位长,它是可变的并且比正常的 64 位或 128 位要小。因此,我们需要一种适用于可变大小的小块的密码。所以我们要自己写——一个可变大小的轻度不平衡Feistel cipher。简单!
要进行 Feistel 密码,您需要:
- 数字 B_L 和 B_R 使得 B_L + B_R = B。在平衡的 Feistel 密码中,B_L = B_R,因此 B 必须是偶数;我们将使用 B_L = B/2 和 B_R = B - B_L,即类似于平衡网络,但当 B 为奇数时,B_R 稍大。
- 若干轮,称为C;我们将用 i 计算轮数,从 0 到 C-1。有人告诉我 4 和 10 是绝对最小值,所以为了安全起见,让我们选择 12。
- 一个密钥时间表,它只是每一轮的一个密钥,每个都称为K_i,都来自上面提到的主密钥i。您可以在每一轮中使用相同的密钥;正确的密码可以通过某种方式从主密钥生成子密钥。我建议将整数连接到主密钥。
- 一个叫F的轮函数,它接受一个B_R位子块和一个轮密钥,并产生一个B_L位子块;在这些限制范围内,它绝对可以是任何东西。这是 Feistel 密码的核心。对我们来说,最好的选择就是使用已经存在的加密哈希函数,因为这既简单又可靠。 SHA-1 是当前的最爱。我们将向其提供轮密钥,然后是输入子块,计算哈希值,然后从一端(不管是哪一个)获取 B_L 位作为我们的输出。
Feistel 密码的工作原理如下:
- 取一个 B 位输入字
- 对于从 0 到 C-1 的 i:
- 将单词拆分为左右子块 L 和 R,分别具有 B_L 和 B_R 位
- 将 R 和 K_i 通过轮函数 F 生成加密值 X
- 将 X 添加到 L,丢弃任何从高位溢出的进位
- 以错误的方式重新组合 L 和 R,将 R 在左侧,L 在右侧,以形成 B 的新值
B 的最终值是加密值,是密码的输出。解密它非常简单 - 反向执行上述操作 - 但由于我们不需要解密,所以不用担心。
所以你去。维护一个计数器(和一个密钥,以及 M 的值),用一个小密码加密它的值,并将结果用作索引。鉴于密码是一种排列,很容易证明这将永远不会重复,这应该会让您的客户满意。更好的是,鉴于密码的加密特性,您还可以声称学生将无法预测接下来会出现哪个问题(这可能并不重要,但听起来很酷)。
所有这一切都比仅仅增加一个计数器并按顺序提供项目要复杂一些,但这并不难。你可以用一百行 java 来完成。好吧,i can do it in a hundred lines of java - 我不了解你! :)
顺便说一句,这将适用于不断增长的项目池,前提是您始终在最后添加项目,尽管它永远不会选择编号高于 M 的项目。不过,在许多情况下,这会给您一些增长空间.