Двоичный поиск/деление пополам для чисел с плавающей запятой

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

Установив сцену, мой вопрос аналогичен: как мы можем угадать число с плавающей запятой IEEE 754? Предположим, что это не NaN, но все остальное — честная игра. Для каждого предположения вашей программе будет сказано, является ли рассматриваемое число большим, равным или меньшим. Минимизируйте количество предположений, необходимых в худшем случае.

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

Редактировать: если бы я лучше искал, я мог бы найти ответ --- но это работает, только если вы уже знайте, что реинтерпретация как int работает (с некоторыми оговорками). Так что оставим это. Спасибо Гарольду за отличный ответ!


person Matthias    schedule 08.07.2017    source источник
comment
Можно ли переинтерпретировать битовый шаблон чисел с плавающей запятой как целые числа?   -  person harold    schedule 09.07.2017
comment
Конечно, как хочешь. Но оракул даст вам результат сравнения при интерпретации как float.   -  person Matthias    schedule 09.07.2017
comment
Зачем сначала угадывать порядок величины? Какое это имеет отношение к бинарному поиску?   -  person Henk Holterman    schedule 09.07.2017
comment
@HenkHolterman Смотрите новую ссылку, которую я добавил, надеюсь, это прояснит вопрос? Резюме: чтобы использовать классический бинарный поиск, вам сначала нужны конечные границы. Целое число в математическом смысле не имеет никаких конечных границ априори, оно может быть произвольно большим или малым. Поэтому вам нужно угадать их, прежде чем вы сможете начать делить интервал.   -  person Matthias    schedule 09.07.2017
comment
@Matthias: так что это скорее бинарное предположение, чем бинарный поиск. Двоичный поиск ищет в (отсортированном) списке (или массиве, или другом контейнере) элементов. Там интервал задается наибольшим и наименьшим значениями.   -  person Rudy Velthuis    schedule 10.07.2017
comment
@Rudy, я вырос в академических кругах, где мы обычно используем термин бинарный поиск в этом более широком значении. См., например, stackoverflow.com/a/27354580/590534, чтобы узнать, кто делает то же самое. Но не стесняйтесь предлагать редактирование вопроса. Однако термин «бинарная догадка» не встречается в литературе.   -  person Matthias    schedule 10.07.2017
comment
@Matthias: см. здесь: в информатике используется бинарный поиск, также известный как полуинтервальный поиск. , логарифмический поиск или двоичный поиск — это алгоритм поиска, который находит положение целевого значения в отсортированном массиве.   -  person Rudy Velthuis    schedule 10.07.2017
comment
@ Руди, я тоже это видел. И для вводной статьи Википедия достаточно хороша для непрофессионала, но вы можете увидеть, можете ли вы / хотите ли вы улучшить статью, чтобы она больше соответствовала фактическому использованию в CS. В разделе Обсуждения есть много других идей по улучшению статьи. См. также, как метод разделения пополам упоминает более широкое значение бинарного поиска.   -  person Matthias    schedule 10.07.2017
comment
@Matthias: Я тоже непрофессионал (я стоматолог-программист). ‹g› То, о чем вы спрашиваете, обычно называется игрой в угадывание чисел, а бинарная стратегия — просто самая эффективная.   -  person Rudy Velthuis    schedule 10.07.2017
comment
@Rudy, я думаю, я был слишком вежлив с Википедией: я имею в виду, что в статье есть серьезные проблемы, и ее не следует использовать в качестве ссылки самостоятельно. В любом случае, пожалуйста, не стесняйтесь предлагать изменение формулировки вопроса. (По образованию я математик, но занялся вычислениями только потому, что это так просто.)   -  person Matthias    schedule 10.07.2017
comment
Давайте продолжим обсуждение в чате.   -  person Matthias    schedule 10.07.2017
comment
@Matthias: Нет необходимости. Это не слишком важно. Я просто имел в виду, что не стал бы называть это поиском, хотя принцип тот же: многократно рубить пополам.   -  person Rudy Velthuis    schedule 10.07.2017


Ответы (2)


64-битные числа с плавающей запятой IEEE-754 на самом деле являются 64-битными представлениями. Более того, за исключением значений NaN, нет никакой разницы между сравнением с плавающей запятой и сравнением целых чисел с положительными значениями. (То есть два битовых шаблона с неустановленным знаковым битом дадут один и тот же результат сравнения независимо от того, сравниваете ли вы их как int64_t или double, если только один из битовых шаблонов не является числом с плавающей запятой NaN-.)

Это означает, что вы можете найти число за 64 попытки, угадывая по одному биту за раз, даже если это число . Начните со сравнения числа с 0; если цель «меньше», то произведите предположения так же, как показано ниже, но отмените их перед предположением. (Поскольку числа с плавающей запятой IEEE-754 представляют собой знак/величину, вы можете инвертировать число, установив бит знака в 1. Или вы можете выполнить переинтерпретацию положительного битового шаблона, а затем отрицать результат с плавающей запятой.)

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

Есть два предостережения:

  1. Вы не можете отличить 0 от сравнительных тестов. Это означает, что если ваш оппонент хочет, чтобы вы различали их, он должен будет предоставить вам способ спросить о равенстве с 0, и вам придется использовать этот механизм после того, как вы, очевидно, установили, что число равно 0 ( что произойдет на 64-м предположении). Это добавит одно предположение, всего 65.

  2. Если вы уверены, что цель не NaN, то других проблем нет. Если это может быть NaN, вам нужно быть осторожным при сравнении: все будет хорошо, если вы всегда будете спрашивать «X меньше этого предположения?», потому что сравнение NaN всегда будет возвращать false . Это означает, что после 11 последовательных ответов «нет» (не считая ответа на установление знака) вы обнаружите, что угадываете , при условии, что если число не меньше , то оно должно быть равно. Однако только в этом случае вам также необходимо явно проверить равенство, потому что это также будет ложным, если целью является NaN. Это не добавляет дополнительного предположения к счету, потому что это всегда происходит задолго до того, как будут израсходованы 64 предположения.

person rici    schedule 09.07.2017
comment
О вашем первом предостережении: учитывая, что каждый раз мы получаем трехсторонний результат (выше/ниже/равно) и что первое сравнение проводится с 0,0, не будет ли установление того, что число равно +/- 0, произойдет при первом предположении , чтобы при втором приближении мог получиться результат либо +0,0, либо -0,0? - person Mark Dickinson; 09.07.2017
comment
Потрясающий! Я даже очень легко вижу, как этот алгоритм не только завершает работу, но и имеет оптимальное время выполнения в худшем случае. Не могли бы вы добавить ссылку на фон для сохранения сравнения при переосмыслении как int64_t, пожалуйста? - person Matthias; 09.07.2017
comment
Я реализовал вашу идею на Python. Работает очарование. Я могу угадать каждый поплавок за 64 шага. (И есть несколько примеров, которые терпят неудачу за 63 шага.) - person Matthias; 10.07.2017

Тот же подход можно применить к числу с плавающей запятой. В худшем случае время выполнения составляет O (log n).

public class GuessComparer
{
    private float random;
    public GuessComparer() // generate a random float and keep it private
    {
        Random rnd = new Random();
        var buffer = new byte[4];
        rnd.NextBytes(buffer);
        random = BitConverter.ToSingle(buffer, 0);
    }
    public int CheckGuess(float quess) // answer whether number is high, lower or the same.
    {
        return random.CompareTo(quess);
    }
}
public class FloatFinder
{

    public static int Find(GuessComparer checker)
    {
        float guess = 0;
        int result = checker.CheckGuess(guess);
        int guesscount = 1;
        var high = float.MaxValue;
        var low = float.MinValue;
        while (result != 0)
        {
            if (result > 0) //random is higher than guess
                low = guess;
            else// random is lower than guess

                high = guess;

            guess = (high + low) / 2;
            guesscount++;
            result = checker.CheckGuess(guess);
        }
        Console.WriteLine("Found answer in {0}", guesscount);
        return guesscount;
    }

    public static void Find()
    {
        var checker = new GuessComparer();
        int guesses = Find(checker);
    }
}
person Alexander Higgins    schedule 08.07.2017
comment
Спасибо. Я удивлен, что это может справиться со всеми неприятностями, связанными с переполнением, недостатком и т. Д.? Я не думаю, что это обрабатывает бесконечность, верно? Я сомневаюсь, что это оптимально, так как он ничего не знает о плотности поплавков (т. е. он не принимает во внимание, что между 0 и 1 больше поплавков, чем между 1 000 000 и 1 000 001. - person Matthias; 09.07.2017
comment
Это не нужно. Пока начальный высокий уровень является максимальным значением для типа данных (3,40282347E+38 для одного), а начальный низкий уровень является минимальным значением (-3,40282347E+38), взяв среднее значение между высоким и низким в каждом цикле, алгоритм сходятся так же, как и с целыми числами. Невозможно столкнуться с положительной бесконечностью (1,0F / 0,0F), отрицательной бесконечностью (-1,0F / 0,0F) или NaN (0,0F / 0,0F). - person Alexander Higgins; 09.07.2017
comment
Что, если нужно угадать число равно бесконечности? Или что, если рассматриваемое число находится чуть ниже float.MaxValue --- не будет ли ваш расчет (высокий + низкий)/2 переполняться ближе к концу, когда «низкий» также очень большой? - person Matthias; 09.07.2017
comment
Максимальное значение единичного числа равно положительной бесконечности, а не 3,40282347E+38. - person Patricia Shanahan; 09.07.2017