把项目按一定的顺序,并给每一个数字。为从零开始的用户维护一个计数器。为每个用户生成一个随机(ish)密钥。选择一个函数,它将0 ... N范围内的整数与0 ... N范围内的另一个整数进行映射,使得对于任何关键值,整数之间的映射都是双射的。当你需要一个物品时,取出计数器的值,将其与键一起放入函数中,并使用该数字索引到物品列表中。增加计数器。
您需要为每个用户存储的所有内容都是密钥和计数器,并且您有保证不会重复任何项目。这基本上是一种即时构建巨大排列的方式。
问题当然是选择功能!
一个简单的将是f(counter, key) = (counter + key) mod N
,虽然这将起作用,但它不会随机化所有项目,所以每个人都会按照相同的顺序获得项目,只是从不同的地方开始。如果N + 1是素数,则可以使用F(计数器,键)=(((计数器+ 1)*(键+ 1))模(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(见下文!)。当你想要一个项目时,取计数器的值,增加计数器,然后通过魔术函数将原始值得到一个索引;如果索引大于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比特输入字
- 对于i从0到C-1:
- 拆分字分成左右子块L和R,分别用B_L和B_R位
- 将R和K_i放入循环函数F以产生加密值X
- 添加X为L,不考虑任何进位,它从顶部比特
- 重新组装大号溢出和圆R是错误的方法,在左侧和L在右侧R,以形成用于B中的新的值
对于B的最终值是加密的值,并且是从密码的输出。解密这个过程非常简单 - 上面的步骤相反 - 但由于我们不需要解密,所以不用担心。
所以你去。维护一个计数器(以及一个密钥和M的值),用一个小密码对其值进行加密,并将结果用作索引。考虑到密码是一个排列组合,很容易证明这不会重复,这会让您的客户满意。更好的是,考虑到密码的密码属性,你也可以声称学生将无法预测下一个问题(这可能不重要,但听起来很酷)。
这一切不仅仅是增加一个计数器,并给他们以项目更复杂一些,但它并不难。你可以在一百行的java中做到这一点。那么,i can do it in a hundred lines of java - 我不知道你! :)
顺便说一句,这将与项目的成长池的工作,只要你总是在结尾处添加项目,但它绝不会挑项目编号比M高在许多情况下,虽然,会给你一些成长空间。
回想起四年前我问过的这个问题,我不认为有必要担心在发生冲突时必须重试ID,尽管一些响应者提供了解决方法。 – 2013-10-09 03:14:29