2009-05-31 3 views
0

У меня есть возможное решение проблемы, которую я пытаюсь решить, но я хотел запустить ее здесь, чтобы быть в безопасности. Задача состоит в том, чтобы убедиться, что пользователь, который прошел некоторые тестовые вопросы в приложении для экзамена, не сталкивается с ними снова во время последующего теста.Убедитесь, что элементы, которые были просмотрены, снова не видны

Я не использую базу данных SQL, которая позволила бы мне использовать левые соединения, подзапросы, временные таблицы и т. Д. Я использую хранилище данных Google App Engine и надеюсь получить нужную мне информацию в рамках одного HTTP-запроса и одного потока, предпочтительно в течение секунды.

Предположим, существует пул из 100 000 терминов по тематике определенного типа (например, синонимы). Приложение выберет 30 вопросов из пула для данного раздела экзамена. Вот что я собирался делать:

  • Когда возникает вопрос в кулак, назначьте ему случайное целое положение в пуле.
  • Во время первого экзамена человека выберите случайное число, затем выберите первые 100 вопросов, упорядоченных по положению, чьи позиции больше этого числа. Следите за номером как нижнюю границу окна вопросов, а также как начальную позицию для пула в целом. Следите за (максимальным) положением последнего вопроса в результирующем наборе как верхняя граница нового окна.
  • Выберите 30 вопросов в случайном порядке из окна, а затем укажите их как раздел. Сохраните оставшиеся 70 вопросов для использования позже на экзамене и, возможно, на последующем экзамене.
  • Поскольку пользователь просматривает дополнительные разделы (например, для практики), а список оставшихся вопросов в текущем окне исчерпан, выберите следующие 100 вопросов из пула, позиции которых больше верхней границы, которая была сохранена ранее , Сделайте прежнюю верхнюю границу новой нижней границей и найдите верхнюю границу нового окна.
  • Когда запрос возвращает менее 100 вопросов, оберните их в положение 0 и продолжайте до тех пор, пока не встретится исходная точка начала (маловероятно, чтобы кто-либо прошел весь пул, но важно быть уверенным).

Основная причина, по которой позиции назначаются случайным образом, заключается в том, чтобы отменить эффекты изменений в стиле написания вопросов, например, вопросы, которые были написаны ранее, когда было меньше опыта по сравнению с более поздними.

Приложение назначит позицию для вопроса, не проверяя, что позиция уникальна. Если имеется достаточно большое количество вопросов, то birthday paradox предполагает, что дублирующиеся позиции станут все более распространенными. Я думал, что это не повредит случайным дубликатам, и что это упростит ситуацию, чем обеспечить уникальность данной позиции, что может повлечь за собой повторную попытку и связанные с ней сетевые затраты. Более важно, чтобы повторных вопросов не возникало, чем для того, чтобы каждый вопрос в пределах диапазона вопросов показывался пользователю.

Есть ли лучший способ сделать это? Можно ли не беспокоиться о дублирующих позициях?

+0

Оглядываясь назад на этот вопрос, который я задал четыре года назад, я не думаю, что нужно было беспокоиться о необходимости повторить попытку в случае столкновения в ID, хотя некоторые из респондентов предложили способы обойти это. –

ответ

1

Используйте число с плавающей запятой между 0 и 1 вместо целого. У этого есть хороший домен, который не изменяется с количеством сущностей, которые у вас есть, и удваивает 52-битную мантиссу, что дает нам приблизительно 2^26 объектов, прежде чем мы сможем ожидать столкновения; существенно больше, чем вы имеете дело.

1

Я не думаю, что вам нужно «выбрать 30 в случайном порядке» из вашего пула вопросов 100.Для начала 100 были выбраны случайным образом - если вы возьмете первые 30, они будут рандомизированы. Ваш код будет проще, с не меньшей случайностью.

+0

Очень хорошая точка. Идея заключалась в том, что для небольших пулов 300 проблем, скажем, я не хотел, чтобы люди чувствовали, что они проходят через те же самые проблемы, как только они обернутся. Таким образом, в этом случае это была посторонняя деталь, и это (вроде) противоречит заданному вопросу, то есть эффективному способу предотвращения повторения людей теми же проблемами? Но я не хотел задавать слишком сложный вопрос, задавая слишком много соображений. –

+0

ahhh. В конечном счете это зависит от типа случайности, которую вы хотите испытать пользователи. С помощью метода «выбрать 30 из подпулов 100» каждый раз, когда они обертываются вокруг основного пула, они, скорее всего, будут видеть вопрос X 4 или 5 раз, прежде чем они увидят вопрос Y, и будут какие-то вопросы, которые они вообще не видят. Если вы пройдете весь 300 в случайном порядке, они будут видеть каждый вопрос 1 раз, прежде чем увидеть какие-либо дубликаты. Чтобы это было интересно для пользователей, вы могли бы создать несколько заказов всего пула, так что может быть 5 способов пройти через 300 элементов. –

1

Положите товары в определенном порядке и дайте каждому номер. Ведите счетчик для пользователя, который начинается с нуля. Создайте случайный (ish) ключ для каждого пользователя. Выберите функцию, которая сопоставляет комбинацию целого числа в диапазоне 0 ... N и клавишу с другим целым числом в диапазоне 0 ... N, так что для любого значения от ключа сопоставление целых чисел является биективным. Когда вам нужен элемент, возьмите значение счетчика, поместите его через функцию вместе с ключом и используйте это число для индексации в список элементов. Увеличьте счетчик.

Все, что вам нужно для хранения для каждого пользователя - это ключ и счетчик, и у вас есть гарантия, что ни один элемент никогда не будет повторен. Это в основном способ построения огромной перестановки «на лету».

Проблема, конечно, выбор функции!

Простым будет f(counter, key) = (counter + key) mod N, и пока это будет работать, оно не будет рандомизировать элементы вообще, поэтому каждый получит предметы в том же порядке, только начиная с разных мест. Если N + 1 является простым, вы можете использовать F (счетчик, ключ) = (((счетчик + 1) * (ключ + 1)) mod (N + 1)) - 1, который будет работать очень хорошо. Если диапазон был 0 ... 2^64, вы могли бы использовать любой блочный шифр, который имеет 64-битный блок, что даст отличную рандомизацию. Но неясно, что вы могли бы адаптировать это к меньшим размерам.

У меня будет кое-что, чтобы посмотреть, могу ли я придумать универсальную функцию. Это проблема, с которой я столкнулся сам, и было бы здорово наконец получить хороший ответ. Я отредактирую этот ответ, если найду что-нибудь.

EDIT: Хорошо, мы идем! Я получил ключевые идеи от a thread i started on sci.crypt, и, в частности, от одного Пола Рубина, который является универсальным героем Usenet.

Незначительное изменение стратегии. Поместите свои элементы в список, чтобы к ним можно было получить доступ по индексу. Выберем число B, такое, что 2^B> = N - любое значение будет делать, но вы действительно хотите наименьший (то есть потолок логарифма базы-2 N-1). Затем мы ссылаемся на 2^B как M. Установите счетчик, который будет работать от 0 до M-1, и ключ для каждого пользователя, который может быть произвольной байтовой строкой - случайное целое, вероятно, является самым простым. Настройте Волшебную функцию, которая представляет собой биекцию, а также перестановку, на набор целых чисел 0 ... M-1 (см. Ниже!). Когда вы хотите элемент, возьмите значение счетчика, увеличьте счетчик, затем поместите исходное значение через функцию Magic, чтобы получить индекс; если индекс больше N-1, затем выбросьте его и повторите процесс. Повторяйте, пока не получите индекс, который меньше N. Иногда вам нужно пройти один или несколько бесполезных индексов, прежде чем вы получите хороший, но в среднем, это потребует попыток M/N, что не так уж плохо (это меньше 2, если вы выбрали наименьшее возможное значение B).

Кстати, вычисление потолка базы 2 логарифма числа легко:

int lb(int x) { 
    int lb = 0; 
    while (x > 0) { 
     ++lb; 
     x >>= 1; 
    } 
    return lb; 
} 

Итак, Волшебная функция, которая отображает число от 0 ... М-1 к другому такому количеству , Я упомянул блок-шифры выше, и это то, что мы будем использовать. За исключением того, что наш блок имеет длину B бит, который является переменным и меньше, чем обычные 64 или 128 бит. Поэтому нам нужен шифр, который работает на небольших блоках с переменным размером. Таким образом, мы собираемся написать наш собственный - переменный размер слегка несбалансированный Feistel cipher. Легко!

Для шифра Фейстеля, вам необходимо:

  • Числа B_l и b_r таким образом, что B_l + b_r = B.В сбалансированном шифре Файстеля B_L = B_R, поэтому B должен быть четным; мы будем использовать B_L = B/2 и B_R = B - B_L, то есть аналогично сбалансированной сети, но с немного большим B_R, когда B нечетно.
  • Число раундов, называемых C; мы будем считать раунды с i, который будет идти от 0 до C-1. Мне сказали, что 4 и 10 являются абсолютными минимумами, так что давайте отправимся с 12, чтобы быть в безопасности.
  • Ключевое расписание, которое является всего лишь ключом для каждого раунда, каждый из которых называется K_i, все производные от основного ключа, упомянутого выше. Вы могли бы использовать один и тот же ключ для каждого раунда; у правильных шифров есть способ генерации дочерних ключей из главного ключа. Я бы предложил просто конкатенировать круглый номер с помощью главного ключа.
  • Круглая функция, называемая F, которая принимает бит-бит B_R-бит и круглый ключ и создает B-L-бит-подблок; в рамках этих ограничений это может быть абсолютно все. Это основа шифрования Фейстеля. Для нас лучшим вариантом является использование уже существующей криптографической хэш-функции, так как это легко и надежно. SHA-1 является фаворитом. Мы будем снабжать его круглым ключом, а затем входным подблоком, вычислять хэш, а затем принимать B_L бит с одного конца (неважно, какой) в качестве нашего вывода.

Feistel шифра работает следующим образом:

  1. Возьмите B-битную входное слово
  2. Ибо я от 0 до C-1:
    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. Во многих случаях, однако, это даст вам немного место для роста.

+0

Очень интересный подход. Мне определенно интересно узнать, что вы придумали. В моем случае я должен буду иметь возможность гарантировать образовательные организации, что при вступлении в экзамен участник не будет сталкиваться с проблемами, с которыми он или она сталкивался раньше, поэтому это довольно важный момент. Полагаю, я могу разделить пул на два пула, практиковать вопросы и тестовые вопросы, но я бы предпочел рисовать из одного пула. –

+0

Один из вопросов, который у меня был, заключается в том, можно ли избежать принятия фиксированного размера пула для этого подхода. Можно ли заставить работать, когда предметы вставлены в середину пула или добавлены в конец? –

+0

Хорошо, пользовательский мелкоблочный шифр Feistel с хешем в качестве круглой функции, а также модифицированный трюк Hasty Pudding. Можно сделать работу для растущего пула, если предметы добавляются только в конце. Объясним подробно завтра! –

0

Вот подход к рассмотрению. У вас нет времени писать и редактировать, поэтому заранее приносите свои извинения за любые проблемы. Создайте модель с вашими 100K вопросами, чтобы вы могли получить к ним доступ с использованием имени ключа (например, question_000001, question_0000002 и т. Д.). Когда студент регистрирует свою запись с тремя TextProperties.Когда запись создается, сгенерируйте рандомизированный набор чисел от 1-100K и сериализуйте его как разделительную текстовую строку. Вторая строка предназначена для записи ответных вопросов, но для обработки заданий. Третья строка для ответов на вопросы после обработки TQ.

Когда пользователь входит в систему, отправьте первые N полей строки, подлежащей прослушиванию (N = более чем достаточно вопросов для обслуживания любого типа сеанса), а также целую строку ответов на вопросы. На стороне клиента разделите их на множество вопросов, которые нужно задать, и ответ на хэш вопросов. Если вопрос находится в хеше, пропустите его. По мере того, как пользователь работает с вопросами, каждый обслуживается простым вызовом get_by_id для вашего on-line-обработчика. По мере ответа на каждый вопрос клиент отправляет его в GAE, где вы добавляете номер вопроса в текстовое поле, которое недавно ответило на вопросы, и установите флаг boolean для последующей обработки TQ. Один раз в день или около того, запустите процесс заданий, чтобы пройти и обновить запись, разделив вопросы, которые необходимо задать в текстовом поле, и удалите все вопросы, которые были заданы после последнего обновления. Переместите их в третий текст, заполненный как заполненные вопросы.

Обратите внимание, что вы можете пропустить много всего, используя только два текстовых поля и обновляя при отправке ответов. Тем не менее, моя склонность - всегда делать как можно меньше с помощью on-line-обработчика и подталкивать вещи к TQ.

Нет счетчиков (всегда доступны путем получения длины разделенных текстовых полей GAE), без запросов (кроме запроса булевского флага TQ). Нет сложности. Существует множество способов сделать это более эффективным: количество переданных данных. и т. д.

Желание у меня было немного больше времени, чтобы убедиться, что это на 100% соответствует вашим потребностям, но нужно идти. Поэтому я оставляю вас с «HTH» -stevep

Смежные вопросы