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

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

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

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

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

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

1
becko 2 Окт 2014 в 00:10
Каков ожидаемый диапазон c?
 – 
phs
2 Окт 2014 в 00:12
Вы можете предположить, что c является 32-битным целым числом без знака.
 – 
becko
2 Окт 2014 в 00:17
Спасибо. Еще один вопрос: насколько эффективным является «эффективный»? Вы уже нашли закрытую форму O(1) (полагаю, я еще не прогонял ваши коэффициенты по формуле quad.) Вы ищете хитрости?
 – 
phs
2 Окт 2014 в 00:20
Настолько эффективным, насколько это возможно, до тех пор, пока это не является полностью неисправимым. Если под закрытой формой O(1) вы подразумеваете выражение floor((1 + sqrt(1 + 8*c))/2), обратите внимание, что оно включает арифметику с плавающей запятой, которая может быть неточной. Я не проверял, всегда ли в этом случае он дает правильный результат.
 – 
becko
2 Окт 2014 в 00:29
2
Ну, для первого перестройте отношение так, чтобы у вас в левой части было x, т.е. решите его математически. Затем подумайте, как написать соответствующий код. Во всяком случае, что вы пробовали сами до сих пор?
 – 
Ulrich Eckhardt
2 Окт 2014 в 00:33

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))

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

5
David Eisenstat 2 Окт 2014 в 01:39

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

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

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

Однако мы можем избавиться от этого осторожного шага и отметить, что sqrt(1/4 + 2c) будет иметь ошибку меньше, чем 0.5, если значения 32-битные, и мы используем double s. (Мы не можем предоставить эту гарантию с float s, поскольку 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.

1
Yakk - Adam Nevraumont 2 Окт 2014 в 00:57

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

То есть:

Т n = n * (n + 1) / 2

а также

Т n - n = T n - 1 = n * (n - 1) / 2

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

п = целое (sqrt (2 * c))

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

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

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

Exact vs int(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) (синий) против int (sqrt (0,25 + 2 * c) + 0,5 (красный) против Exact (белая линия)

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

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

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

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

1
Community 20 Июн 2020 в 12:12