Эффективная версия / альтернатива методу псевдонима, который выбирает без замены

Я пишу викторину / обучающую игру на HTML5/JS, в которой игроку предлагается набор из 10 вопросов из более широкого набора мастерства. Игра отслеживает оценки игроков с течением времени и с большей вероятностью выбирает вопросы из списка вопросов, с которыми у игрока возникают проблемы.

Чтобы построить список распределения вероятностей, я использую метод псевдонимов, как показано ниже, который выбирает элемент за O(1) время, полностью придерживаясь распределения:

function generate_random_question_selector() {
    // Generates a random selector function using the Alias Method
    // for discrete probability distributions (see
    // https://en.wikipedia.org/wiki/Alias_method for an explanation)
    var i = 0;
    var probabilities = [], aliases = [];
    var probSum = 0;

    /* ... Business logic to fill probabilities array ... */

    // Normalize all probabilities to average to 1
    // and categorize each probability as to where it fits
    // in that scale
    var probMultiplier = probabilities.length / probSum;
    var overFull = [], underFull = [];
    probabilities = probabilities.map(function(p, i) {
        var newP = p * probMultiplier;
        if (newP > 1) overFull.push(i);
        else if (newP < 1) underFull.push(i);
        else if (newP !== 1) {
            throw "Non-numerical value got into scores";
        }
        return newP;
    });
    overFull.sort();
    underFull.sort();

    // Process both queues by having each under-full entry
    // have the rest of its space occupied by the fullest
    // over-full entry, re-categorizing the over-full entry
    // as needed
    while (overFull.length > 0 || underFull.length > 0) {
        if (!(overFull.length > 0 && underFull.length > 0)) {
            // only reached due to rounding errors.
            // Just assign all the remaining probabilities to 1
            var notEmptyArray = overFull.length > 0 ? overFull : underFull;
            notEmptyArray.forEach(function(index) {
                probabilities[index] = 1;
            });
            break; // get out of the while loop
        }

        aliases[underFull[0]] = overFull[0];
        probabilities[overFull[0]] += probabilities[underFull[0]] - 1;
        underFull.shift();
        if (probabilities[overFull[0]] > 1) overFull.push(overFull.shift());
        else if (probabilities[overFull[0]] < 1) underFull.push(overFull.shift());
        else overFull.shift();
    }

    return function() {
        var index = Math.floor(Math.random() * probabilities.length);
        return Math.random() < probabilities[index] ? index : aliases[index];
    }
}

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

var selectQuestion = generate_random_question_selector();   
var questionSet = [];
for (var i = 0; i < num_questions; i++) {
    var question_num;
    do {
        question_num = selectQuestion();
    } while (questionSet.indexOf(question_num) >= 0)
    questionSet.push(question_num);
}

Что можно сделать с этим методом или с этим методом, который позволил бы мне эффективно отбирать вопросы без замены?

1 ответ

Метод псевдонимов плохо подходит для выборки без замены, потому что каждое значение выбирается с использованием различного распределения вероятностей, и при вычислении (или обновлении) таблицы псевдонимов O(n).

Вам нужна структура данных, которая может быть обновлена ​​более эффективно. Например, вы могли бы построить дерево поиска всех значений (где каждый узел хранит общий вес своего поддерева), что позволило бы осуществлять выборку и обновление распределения вероятностей в O(log n).

Если мы удалим записи, установив их вероятность в 0, это дерево никогда не будет структурно изменено и может быть закодировано в массив.

Вот некоторый код:

function prepare() {
    // index i is the parent of indices 2*i and 2*i+1
    // therefore, index 0 is unused, and index 1 the root of the tree
    var i;
    for (i = weights.length - 1; i > 1; i--) {
        weights[i >> 1] += weights[i];
    }
}

function sample() {
    var index = 1;
    var key = Math.random() * weights[index];

    for (;;) {
        var left = index << 1;
        var right = left + 1;
        leftWeight = weights[left] || 0;
        rightWeight = weights[right] || 0;

        if (key < leftWeight) {
            index = left;
        } else {
            key -= leftWeight;
            if (key < rightWeight) {
                index = right;
            } else {
                return index;
            }
        }
    }
}

function remove(index) {
    var left = index << 1;
    var right = left + 1;
    leftWeight = weights[left] || 0;
    rightWeight = weights[right] || 0;

    var w = weights[index] - leftWeight - rightWeight;
    while (index > 0) {
        weights[index] -= w;
        index = index >> 1;
    }
}

Тестовый код:

function retrieve() {
    var index = sample();
    remove(index);
    console.log(index);
    console.log(weights);
}

weights = [0,1,2,3,4];
prepare();
console.log(weights);
retrieve();
retrieve();
retrieve();
retrieve();
Другие вопросы по тегам