【问题标题】:Ensuring that items that have been viewed are not seen again确保不再看到已查看的项目
【发布时间】:2009-05-31 04:19:12
【问题描述】:

对于我正在尝试解决的问题,我有一个可能的解决方案,但为了安全起见,我想在这里运行它。挑战在于确保在考试申请中完成了一些试题的用户在随后的考试中不会再次遇到这些试题。

我没有使用 SQL 数据库,它允许我使用左连接、子查询、临时表等。我正在使用 Google App Engine 的数据存储区,希望在单个 HTTP 请求和单个线程中获得我需要的信息,最好在一秒钟内获得。

假设有 100,000 个特定类型的词汇问题(例如,同义词)。该应用程序将从题库中为考试的给定部分选择 30 个问题。这是我想做的事情:

  • 当第一个问题被创建时,在池中为其分配一个随机整数位置。
  • 在个人第一次考试时,随机选择一个数字,然后选择前100题,按位置排序,位置大于该数字。跟踪数字作为问题窗口的下限以及整个池的起始位置。跟踪结果集中最后一个问题的(最大)位置作为新窗口的上限。
  • 从窗口中随机选择30个问题,然后将它们作为部分。存储剩余的 70 个问题,以供以后在考试中使用,也可能在后续考试中使用。
  • 当用户浏览其他部分(例如练习)并且当前窗口中的剩余问题列表已用完时,请从题库中选择位置大于之前存储的上限的接下来 100 个问题.将旧的上限设为新的下限,并找到新窗口的上限。
  • 当查询返回的问题少于 100 个时,绕回位置 0 并继续直到遇到原始起点(不太可能有人会遍历整个池,但务必要确定)。李>

随机分配职位的主要原因是为了抵消问题编写方式变化的影响,例如,较早编写的问题是在经验较少时编写的问题与后来的问题相比。

应用程序会为问题分配一个位置,而不检查该位置是否唯一。如果有足够多的问题,birthday paradox 表明重复职位将变得越来越普遍。我的想法是偶尔重复不会有什么坏处,而且这比确保给定位置是唯一的更简单,这可能需要重试以及随之而来的相关网络成本。没有重复的问题比确保向用户展示一系列问题中的每个问题更为重要。

有没有更好的方法来做到这一点?不担心重复位置可以吗?

【问题讨论】:

  • 回顾我四年前提出的这个问题,我认为不必担心在 ID 冲突的情况下必须重试,尽管有些响应者提供了解决此问题的方法。

标签: algorithm google-app-engine


【解决方案1】:

使用 0 到 1 之间的浮点数而不是整数。它有一个很好的域,它不会随着你拥有的实体数量而改变,并且双打有一个 52 位尾数,这给了我们大约 2^26 个对象,然后我们就可以预料到碰撞;远远超过你正在处理的。

【讨论】:

    【解决方案2】:

    我认为您不需要从 100 个问题库中“随机选择 30 个”。开始时随机选择 100 个 - 如果您选择前 30 个,这些将已经是随机的。你的代码会更简单,随机性也不会减少。

    【讨论】:

    • 非常好。那里的想法是,对于较小的池,例如 300 个问题,我不希望人们有一种感觉,即他们在完成后会经历大致相同的问题集。所以在这种情况下这是一个无关紧要的细节,它(有点)违背了所提出的问题,即,什么是防止人们再次看到相同问题的有效方法?但我不想因为考虑太多而使最初的问题变得过于复杂。
    • 啊。最终,这取决于您希望用户体验的随机性类型。使用“从 100 个子池中挑选 30 个”的方法,每次他们绕过主池时,他们可能会在看到问题 Y 之前看到问题 X 4 或 5 次,并且会有一些他们根本看不到的问题。如果您以随机顺序浏览整个 300 个问题,他们将在看到任何重复项之前查看每个问题 1 次。为了让用户感兴趣,您可能会创建多个整体排序,以便可能有 5 种方法来遍历 300 个项目。
    【解决方案3】:

    将物品按某种顺序排列,并给每个物品一个编号。为用户维护一个从零开始的计数器。为每个用户生成一个随机(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 密码的工作原理如下:

    1. 取一个 B 位输入字
    2. 对于从 0 到 C-1 的 i:
      1. 将单词拆分为左右子块 L 和 R,分别具有 B_L 和 B_R 位
      2. 将 R 和 K_i 通过轮函数 F 生成加密值 X
      3. 将 X 添加到 L,丢弃任何从高位溢出的进位
      4. 以错误的方式重新组合 L 和 R,将 R 在左侧,L 在右侧,以形成 B 的新值

    B 的最终值是加密值,是密码的输出。解密它非常简单 - 反向执行上述操作 - 但由于我们不需要解密,所以不用担心。

    所以你去。维护一个计数器(和一个密钥,以及 M 的值),用一个小密码加密它的值,并将结果用作索引。鉴于密码是一种排列,很容易证明这将永远不会重复,这应该会让您的客户满意。更好的是,鉴于密码的加密特性,您还可以声称学生将无法预测接下来会出现哪个问题(这可能并不重要,但听起来很酷)。

    所有这一切都比仅仅增加一个计数器并按顺序提供项目要复杂一些,但这并不难。你可以用一百行 java 来完成。好吧,i can do it in a hundred lines of java - 我不了解你! :)

    顺便说一句,这将适用于不断增长的项目池,前提是您始终在最后添加项目,尽管它永远不会选择编号高于 M 的项目。不过,在许多情况下,这会给您一些增长空间.

    【讨论】:

    • 非常有趣的方法。我绝对有兴趣看看你想出什么。就我而言,我需要能够向教育组织保证,在进行考试时,参与者不会遇到他或她以前遇到的问题,所以这是非常重要的一点。我想我可以将题库分为两个题库,练习题和测试题,但我更愿意从同一个题库中抽取。
    • 我遇到的一个问题是,是否可以避免为这种方法假设一个固定的池大小。当项目插入池中间或添加到池尾时,它可以工作吗?
    • 好的,自定义小块 Feistel 密码,以散列作为轮函数,加上修改后的 Hasty Pudding 技巧。如果仅在最后添加项目,则可以使其适用于不断增长的池。明天详细解释!
    【解决方案4】:

    这是一种需要考虑的方法。没有大量的时间来编写和编辑它,因此对于后续的任何问题,请提前道歉。使用您的 100K 问题创建一个模型,以便您可以使用键名(例如 question_000001、question_0000002 等)访问它们。当学生注册时,使用三个 TextProperties 创建他/她的记录。创建记录后,生成一组 1-100K 的随机数字并将其序列化为分隔文本字符串。第二个字符串是记录尚未被任务队列处理的已回答问题。第三个字符串是TQ处理后的回答问题。

    当用户登录时,发送待回答字符串的前 N ​​个字段(N = 足以为任何类型的会话提供服务的问题),以及整个已回答问题字符串。在客户端,将它们分成一系列要问的问题,以及一系列回答的问题。如果问题在哈希中,请跳过它。当用户处理这些问题时,每个问题都通过对您的在线处理程序的简单 get_by_id 调用得到服务。回答每个问题后,客户端将其发送到 GAE,您将问题编号附加到最近回答问题的文本字段中,并设置一个布尔值以标记以供以后 TQ 处理。大约一天一次,运行一个任务队列进程,通过拆分要问的问题文本字段并删除自上次更新以来回答的所有问题来更新记录。将这些移至作为已完成问题提交的第三个文本。

    请注意,您可以仅使用两个文本字段跳过很多内容,并在发布答案时进行更新。但是,我倾向于始终尽可能少地使用在线处理程序,并将事情推送到 TQ。

    没有计数器(通过获取拆分 GAE 文本字段的长度始终可用),没有查询(TQ 布尔标志查询除外)。没有复杂性。有很多方法可以提高效率:传输的数据量。等等。

    希望我有更多的时间来确保这 100% 符合您的需求,但必须离开。所以我给你留下一个“HTH”-stevep

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-24
      • 1970-01-01
      • 2020-08-05
      • 1970-01-01
      • 2013-03-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多