Быстрое целочисленное решение x(x-1)/2 = c

Учитывая неотрицательное целое число c, мне нужен эффективный алгоритм для нахождения наибольшего целого числа x такого, что

x*(x-1)/2 <= c

Соответственно, мне нужен эффективный и надежно точный алгоритм для вычисления:

x = floor((1 + sqrt(1 + 8*c))/2)        (1)

Для определенности я пометил этот вопрос как C++, поэтому ответом должна быть функция, написанная на этом языке. Вы можете предположить, что c является 32-битным целым числом без знака.

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


person becko    schedule 01.10.2014    source источник
comment
Каков ожидаемый диапазон c?   -  person phs    schedule 02.10.2014
comment
@phs Вы можете предположить, что c - это 32-битное целое число без знака.   -  person becko    schedule 02.10.2014
comment
Спасибо. Еще один вопрос: насколько эффективно эффективно? Вы уже нашли закрытую форму O(1) (полагаю, я еще не прогонял ваши коэффициенты по формуле quad.) Вы ищете хитрости?   -  person phs    schedule 02.10.2014
comment
@phs настолько эффективен, насколько это возможно, если он не является полностью нечитаемым. Если под O(1) закрытой формой вы подразумеваете выражение floor((1 + sqrt(1 + 8*c))/2), обратите внимание, что оно включает арифметику с плавающей запятой, которая может быть неточной. Я не проверял, всегда ли в этом случае он дает правильный результат.   -  person becko    schedule 02.10.2014
comment
Ну, для первого переставь отношение так, чтобы у тебя в левой части было x, т.е. реши его математически. Затем подумайте, как написать соответствующий код. Во всяком случае, что вы пробовали сами до сих пор?   -  person Ulrich Eckhardt    schedule 02.10.2014
comment
@UlrichEckhardt Я знаю, что решение x = floor((1 + sqrt(1 + 8*c))/2) (я написал его в вопросе), и я знаю, как это запрограммировать на C++. Проблема в том, что это включает арифметику с плавающей запятой, и я не уверен, всегда ли это будет точно для всех возможных значений c   -  person becko    schedule 02.10.2014
comment
@EvilTeach До сих пор я пытался использовать формулу x = floor((1 + sqrt(1 + 8*c))/2). Кажется, он дает точные результаты для значений c, которые я пробовал. Но я хотел бы увидеть доказательство/гарантию того, что эта формула (или ее эквивалентная версия) всегда дает точные результаты. Я также могу придумать метод разделения пополам для поиска x, но я не стал его программировать, так как подозреваю, что есть решения получше.   -  person becko    schedule 02.10.2014
comment
Это дает правильный результат, потому что было доказано, что квадратичная формула дает правильный результат.   -  person AndyG    schedule 02.10.2014
comment
@AndyG Вы говорите, что цифры перед запятой всегда верны для этой формулы для всех целых чисел c? У вас есть ссылка, где я могу найти доказательство?   -  person becko    schedule 02.10.2014
comment
Можете ли вы выделить память (и циклы процессора во время запуска) для построения таблицы поиска? Если это так, обратите внимание, что x(x-1)/2 — это просто сумма первых x-1 натуральных чисел. Вы можете создать таблицу поиска со всеми записями, если c никогда не бывает слишком большим, но если c может занимать весь 32-битный диапазон, вам может быть лучше с приличной реализацией целочисленного квадратного корня или просто иметь дело с плавающей запятой (что уже не так медленно, как раньше).   -  person twalberg    schedule 02.10.2014
comment
@twalberg Производительность с плавающей запятой в этом случае для меня достаточно высока. Меня волнует только точность. Есть ли у меня гарантия, что выражение (1) всегда будет давать правильный результат в арифметике с плавающей запятой?   -  person becko    schedule 02.10.2014
comment
@becko: Да, вы всегда получите правильный результат ПЛЮС некоторую ошибку из-за усечения и накопления вычислений sqrt.   -  person AndyG    schedule 02.10.2014
comment
Если вас беспокоит точность sqrt, этого будет более чем достаточно для 32-битных целых чисел. См. stackoverflow.com/a/4319151/5987.   -  person Mark Ransom    schedule 02.10.2014
comment
похоже на математический вопрос, а не на вопрос C++, но конечно.   -  person Ahmed Masud    schedule 02.10.2014
comment
@AhmedMasud Если бы это был математический вопрос, меня бы не волновала арифметика с плавающей запятой в C ++, которая здесь является основной проблемой.   -  person becko    schedule 02.10.2014


Ответы (3)


Если вы готовы принять удвоение IEEE с правильным округлением для всех операций, включая квадратный корень, то написанное вами выражение (плюс приведение к удвоению) дает правильный ответ для всех входных данных.

Вот неофициальное доказательство. Поскольку c — это 32-битное целое число без знака, которое преобразуется в тип с плавающей запятой с 53-битным мантиссом, 1 + 8*(double)c является точным, а sqrt(1 + 8*(double)c) правильно округлено. 1 + sqrt(1 + 8*(double)c) является точным с точностью до одной ulp, поскольку последнее слагаемое меньше 2**((32 + 3)/2) = 2**17.5 означает, что единица в последнем разряде последнего слагаемого меньше 1, и, таким образом, (1 + sqrt(1 + 8*(double)c))/2 имеет точность с точностью до одной ulp, поскольку деление на 2 является точным.

Последнее дело – пол. Проблемные случаи здесь возникают, когда (1 + sqrt(1 + 8*(double)c))/2 округляется до целого числа. Это происходит тогда и только тогда, когда sqrt(...) округляется до нечетного целого числа. Поскольку аргумент sqrt является целым числом, наихудшие случаи выглядят как sqrt(z**2 - 1) для положительных нечетных целых чисел z, и мы ограничиваем

z - sqrt(z**2 - 1) = z * (1 - sqrt(1 - 1/z**2)) >= 1/(2*z)

разложением Тейлора. Поскольку z меньше 2**17.5, разрыв до ближайшего целого числа составляет не менее 1/2**18.5 для результата, величина которого меньше 2**17.5, а это означает, что эта ошибка не может возникнуть из-за правильно округленного sqrt.

Принимая упрощение Якка, мы можем написать

(uint32_t)(0.5 + sqrt(0.25 + 2.0*c))

без дополнительной проверки.

person David Eisenstat    schedule 01.10.2014

Если мы начнем с квадратичной формулы, мы быстро достигнем sqrt(1/4 + 2c), округлив до 1/2 или выше.

Теперь, если вы делаете этот расчет с плавающей запятой, могут быть неточности.

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

Однако мы можем покончить с этим осторожным моментом и отметить, что sqrt(1/4 + 2c) будет иметь ошибку меньше, чем 0.5, если значения 32-битные, и мы используем doubles. (Мы не можем дать эту гарантию с floats, так как 2^31 float не может обрабатывать +0.5 без округления).

По сути, мы используем квадратичную формулу, чтобы свести ее к двум возможностям, а затем проверяем эти две.

uint64_t eval(uint64_t x) {
  return x*(x-1)/2;
}
unsigned solve(unsigned c) {
  double test = sqrt( 0.25 + 2.*c );
  if ( eval(test+1.) <= c )
    return test+1.
  ASSERT( eval(test) <= c );
  return test;
}

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

person Yakk - Adam Nevraumont    schedule 01.10.2014

Это может быть немного касательным к вашему вопросу. Но что привлекло мое внимание, так это конкретная формула. Вы пытаетесь найти треугольный корень Tn - 1 (где Tnn-е треугольное число).

I.e.:

Tn = n * (n + 1) / 2

и

Tn - n = Tn - 1 = n * (n - 1) / 2

Из изящного трюка, описанного здесь, для Tn мы имеем:

n = интервал (sqrt (2 * c))

Поиск n такого, что Tn - 1 ≤ c, в этом случае не меняет определение n по той же причине, что и в исходном вопросе.

В вычислительном отношении это экономит несколько операций, поэтому теоретически это быстрее, чем точное решение (1). На самом деле, наверное, примерно так же.

Однако ни это решение, ни решение, представленное Дэвидом, не являются такими же точными, как ваше (1).

Точный против целого(sqrt(2 * c))

floor((1 + sqrt(1 + 8*c))/2) (синий) vs int(sqrt(2 * c)) (красный) vs Exact (белая линия)


Exact vs int(sqrt(0,25 + 2 * c) + 0,5)

floor((1 + sqrt(1 + 8*c))/2) (синий) vs int(sqrt(0,25 + 2 * c) + 0,5 (красный) vs Exact (белая линия)

На самом деле я хочу сказать, что треугольные числа — это забавный набор чисел, которые связаны с квадратами, треугольником Паскаля, числами Фибоначчи и т. д. др.

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

Особый интерес может представлять то, что Tn + Tn - 1 = n2

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

person Seth    schedule 02.10.2014