Рекурсия мемоизации C++

Я реализовывал рекурсивную функцию с запоминанием для ускорения. Суть программы в следующем:

Я перемешиваю колоду карт (с равным количеством красных и черных карт) и начинаю сдавать их лицевой стороной вверх. После любой карты вы можете сказать «стоп», после чего я плачу вам 1 доллар за каждую сданную красную карту, а вы платите мне 1 доллар за каждую сданную черную карту. Какова ваша оптимальная стратегия и сколько вы готовы заплатить за эту игру?

Моя рекурсивная функция выглядит следующим образом:

double Game::Value_of_game(double number_of_red_cards, double number_of_black_cards)
{
    double value, key;


    if(number_of_red_cards == 0)
    {
    Card_values.insert(Card_values.begin(), pair<double, double> (Key_hash_table(number_of_red_cards, number_of_black_cards), number_of_black_cards));
    return number_of_black_cards;
    }
    else if(number_of_black_cards == 0)
    {
    Card_values.insert(Card_values.begin(), pair<double, double> (Key_hash_table(number_of_red_cards, number_of_black_cards), 0));
    return 0;
    }

    card_iter = Card_values.find(Key_hash_table(number_of_red_cards, number_of_black_cards));
    if(card_iter != Card_values.end())
    {
        cout << endl << "Debug: [" << number_of_red_cards << ", " << number_of_black_cards << "] and value = " << card_iter->second << endl;
    return card_iter->second; 
    }

    else 
    {
    number_of_total_cards = number_of_red_cards + number_of_black_cards;
        prob_red_card = number_of_red_cards/number_of_total_cards;
        prob_black_card = number_of_black_cards/number_of_total_cards;

    value = max(((prob_red_card*Value_of_game(number_of_red_cards - 1, number_of_black_cards)) + 
             (prob_black_card*Value_of_game(number_of_red_cards, number_of_black_cards - 1))), 
             (number_of_black_cards - number_of_red_cards));
    cout << "Check: value = " << value << endl;

    Card_values.insert(Card_values.begin(), pair<double, double> (Key_hash_table(number_of_red_cards, number_of_black_cards), value));

        card_iter = Card_values.find(Key_hash_table(number_of_red_cards , number_of_black_cards ));
        if(card_iter != Card_values.end());
        return card_iter->second;
     } 
}

double Game::Key_hash_table(double number_of_red_cards, double number_of_black_cards)
{
    double key = number_of_red_cards + (number_of_black_cards*91);
    return key; 
}

Третий оператор if — это «запоминающая» часть кода, в нем хранятся все необходимые значения. Значения, которые хранятся на карте, можно рассматривать как матрицу, эти значения будут соответствовать определенным #красным картам и #черным картам. Что действительно странно, так это то, что когда я выполняю код всего для 8 карт (4 черных и 4 красных), я получаю неверный ответ. Но когда я выполняю код для 10 карт, мой ответ неверен, но теперь мой ответ для 4 черных и 4 красных правилен (8 карт)! То же самое можно сказать и о 12 карточках, где я получаю неправильный ответ на 12 карточек, но правильный ответ на 10 карточек, и так далее, и тому подобное. В коде какая-то ошибка, но я не могу понять.


person Chen Li    schedule 07.10.2012    source источник
comment
Почему вы используете double для хранения целых чисел?   -  person nneonneo    schedule 07.10.2012
comment
Просто так, я думаю, это не будет иметь никакого значения?   -  person Chen Li    schedule 07.10.2012
comment
Нет, это не было бы проблемой (иначе я бы опубликовал это как ответ), но обычно использование значения с плавающей запятой для целочисленных данных является плохой формой.   -  person nneonneo    schedule 07.10.2012
comment
Я поменяю, знаю, что так делать не следует, но я хотел сначала найти проблему.   -  person Chen Li    schedule 07.10.2012
comment
Где объявлены prob_red_card и prob_black_card? Они глобальны?   -  person nneonneo    schedule 07.10.2012
comment
Вы не можете использовать doubles в качестве ключа хеш-таблицы, потому что два значения типа double могут быть фактически равны, но не хешировать одно и то же. Я не думаю, что это ваша проблема, но это проблема, которая сделает ваш мемоизатор хеш-таблиц гораздо менее полезным, чем вам хотелось бы. И сравнение их с «0», чтобы решить, должна ли рекурсия закончиться, возможно, ваша проблема. Никогда не сравнивайте двойники на равенство.   -  person Omnifarious    schedule 07.10.2012
comment
@nneonneo: Держу пари, вы нашли проблему. Как бы они ни были объявлены, это явно неправильно для этого рекурсивного алгоритма. Они должны быть локальными переменными, чтобы все работало нормально.   -  person Omnifarious    schedule 07.10.2012
comment
@Omnifarious: Да, именно то, о чем я думал, относительно глобальных переменных. Кроме того, обратите внимание, что doubles может точно содержать целые числа размером до 50 или около того бит и не терять точность (или не сравнивать точно равные). Таким образом, безопасно помещать 32-битные целые значения в double и выполнять над ними строго целочисленные операции (но, конечно, это не то, что вам следует делать).   -  person nneonneo    schedule 07.10.2012
comment
Итак, я должен изменить свои двойные #ofredcards на int и то же самое для черных?   -  person Chen Li    schedule 07.10.2012
comment
@nneonneo - doubles может точно удерживать, его следует заменить обычно double в большинстве систем может точно удерживать. :-)   -  person Omnifarious    schedule 07.10.2012
comment
@nneonneo: даже тогда это небезопасно, поскольку деление вводит части, которые не могут быть выполнены, и в зависимости от того, какие операции вы выполняете, они все равно могут не сравниваться равными. 8.0000000 != 7.999999999   -  person Mooing Duck    schedule 07.10.2012
comment
Если разделить два дубля, то да. Вещи ломаются. Но, по крайней мере, с числами с плавающей запятой IEEE, сложение, умножение и вычитание двойных чисел всегда должны сохранять целочисленное свойство. Очевидно, что на это никогда не следует полагаться.   -  person nneonneo    schedule 07.10.2012


Ответы (1)


Никто на самом деле не ответил на этот вопрос ответом. Так что я попробую, хотя nneonneo на самом деле указывает на вероятный источник вашей проблемы.

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

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

И это подводит меня к тому, что, вероятно, является вашей проблемой. У вас есть эти два утверждения в вашем коде:

number_of_total_cards = number_of_red_cards + number_of_black_cards;
prob_red_card = number_of_red_cards/number_of_total_cards;
prob_black_card = number_of_black_cards/number_of_total_cards;

который, разумеется, должен гласить:

number_of_total_cards = number_of_red_cards + number_of_black_cards;
prob_red_card = number_of_red_cards/double(number_of_total_cards);
prob_black_card = number_of_black_cards/double(number_of_total_cards);

потому что вы были хорошим программистом и объявили эти переменные как целые числа.

Предположительно prob_red_card и prob_black_card являются переменными типа double. Но они нигде не объявлены в коде, который вы нам показываете. Это означает, что независимо от того, где они объявлены или каковы их типы, они должны эффективно использоваться всеми подвызовами в рекурсивном дереве вызовов для Game::Value_of_game.

Это почти наверняка не то, что вы хотите. Чрезвычайно сложно рассуждать о том, какие значения имеют эти переменные и что эти значения представляют во время любого данного вызова в рекурсивном дереве вызовов для вашей функции. Они действительно должны быть локальными переменными, чтобы алгоритм можно было анализировать. К счастью, похоже, что они используются только в предложении else определенного оператора if. Таким образом, они могут быть объявлены, когда им изначально присваиваются значения. Вот, вероятно, что должен читать этот код:

unsigned const int number_of_total_cards = number_of_red_cards + number_of_black_cards;
const double prob_red_card = number_of_red_cards/double(number_of_total_cards);
const double prob_black_card = number_of_black_cards/double(number_of_total_cards);

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

person Omnifarious    schedule 07.10.2012
comment
Большое спасибо!! Ты прав. Я не понимаю, почему я ДОЛЖЕН был объявить 3 типа данных в своем операторе else, даже если я никогда не использовал их где-либо еще? - person Chen Li; 07.10.2012
comment
@ChenLi: вам не нужно было объявлять их в своем операторе else. Вам просто нужно было объявить их как локальные переменные для функции. Это связано с тем, что для каждого рекурсивного вызова функции требуется собственная копия этих переменных. Я объявил их в операторе else, потому что еще одна действительно хорошая практика программирования — всегда объявлять переменные с наименьшей возможной областью действия. Хорошо иметь меньше переменных, о которых вам приходится беспокоиться в любой точке программы, когда вы пытаетесь ее прочитать и понять. - person Omnifarious; 07.10.2012