Обеспечение того, чтобы просмотренные товары больше не видели - PullRequest
0 голосов
/ 31 мая 2009

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

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

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

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

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

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

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

Ответы [ 4 ]

1 голос
/ 08 июня 2009

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

1 голос
/ 09 июня 2009

Расставьте предметы по порядку и дайте каждому номер. Поддерживать счетчик для пользователя, который начинается с нуля. Создать случайный (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-битный блок, который дал бы отличную рандомизацию. Но не ясно, что вы могли бы адаптировать это к меньшим размерам.

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

РЕДАКТИРОВАТЬ: Хорошо, здесь мы идем! Я получил ключевые идеи из нити, которую я начал с sci.crypt , и в частности от одного Пола Рубина, который является универсальным героем Usenet.

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

Кстати, вычислить потолок по основанию 2 логарифму числа просто:

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

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

Чтобы сделать шифр Фейстеля, вам нужно:

  • Числа 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 с одного конца (неважно, какой) в качестве нашего вывода.

Шифр ​​Фейстеля работает так:

  1. Взять B-битное входное слово
  2. Для i от 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. Ну, я могу сделать это в сотне строк Java - я не знаю о вас! :)

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

1 голос
/ 31 мая 2009

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

0 голосов
/ 06 октября 2013

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...