Предотвращение переполнения и сохранение точности при масштабировании (длинных) целых чисел

Допустим, у меня есть позиция pos в пределах заданного диапазона, так что:

0 ‹= позициядиапазон

Эта позиция в пределах диапазона может состоять из двух разных контекстов: в одном диапазон представляет собой целочисленное значение, т. е. posrange ‹ 231, и другой, где диапазон представляет собой длинное целое число, то есть до posrange ‹ 263. Если я хочу перемещаться между этими контекстами, мне нужно масштабировать положение до нового диапазона, чтобы оно было правильно округлено до ближайшего (длинного) целочисленного значения. Итак, технически, все, что я хочу сделать, это:

позицияновая = пол( позициястарая * диапазонновая / диапазонстарый )

К сожалению, этот прямой подход не помогает, поскольку он либо переполняется (как posold * rangenew > может достигать ~294), если я сначала выполняю умножение, или выдает ошибки округления, если я сначала выполняю деление. Использование значений с плавающей запятой для выполнения математических операций в целом также не помогает, поскольку они не обеспечивают достаточной точности и, следовательно, также могут привести к неправильному округлению (у меня доступна только двойная точность).

Я нашел способ правильно масштабировать диапазон целых чисел до диапазона длинных целых чисел:

public long scaleUp(int oldPos, int oldRange, long newRange) {
    return (newRange / oldRange) * oldPos + 
           (newRange % oldRange) * oldPos / oldRange;
}

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

Сейчас я пытаюсь понять, как сделать обратное масштабирование:

public int scaleDown(long oldPos, long oldRange, int newRange) {
    return ??? ;
}

Не уверен, что это должно быть сложнее, чем другая функция, но почему-то я этого не вижу.

Пара замечаний:

  • Я хотел бы избежать использования арифметики с плавающей запятой, так как мне всегда очень трудно убедить себя, что данная формула действительно не может дать неожиданный результат в некоторых очень редких случаях из-за округления
  • Я бы предпочел не использовать библиотеку BigInteger
  • Хотя приведенные здесь образцы кода относятся к Java, на самом деле это вопрос, не зависящий от языка.

person Markus A.    schedule 01.07.2013    source источник
comment
Это больше зависит от языка, чем вы думаете: IIRC java делает несколько более сложным/неэффективным использование основных инструментов, таких как подсчет ведущих нулей числа или получение 128-битного произведения двух 64-битных целых чисел. Или даже возможность выполнять беззнаковые арифметические действия! (не говоря уже о том, что некоторые языки имеют встроенные целые числа произвольной точности; трудно представить, что реализация Python может быть лучше, чем p * r // R)   -  person    schedule 04.07.2013


Ответы (3)


Я нашел ответ, который не является полным на 100%, но охватывает все особые случаи, возникающие в моей программе. Вы можете найти подробности вывода в ответе, который я разместил на свой соответствующий вопрос на Mathematics StackExchange: https://math.stackexchange.com/q/433729/84557

Вот примерный план:

public int scaleDown(long oldPos, long oldRange, int newRange) {
    if (oldPos <= Long.MAX_VALUE/newRange)
        return (int) (oldPos*newRange/oldRange);
    assert oldRange >= newRange*newRange : "Case not supported yet"; // Never happens in my code
    int newPos = (int) (oldPos / (oldRange/newRange));
    if (!isOk(newPos)) newPos--; // Check might be implementation specific
    return newPos;
}

Не полный, но может кому пригодится.

person Markus A.    schedule 05.07.2013

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

Обратите внимание, что я использовал термины x для вашего pos, y для вашего oldRange и w для вашего newRange, то есть решая для z в уравнении x/y = z/w.

Вот решения, которые я рассмотрел:

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

Я мог бы переопределить входной домен так, чтобы он был меньше выходного диапазона не более чем наполовину, а затем увеличить масштаб, т. е. q = y / w; scaleUp(x / q / 2, y / q / 2, w);, и в худшем случае младший бит был бы полностью потерян, а в лучшем случае не было бы никакой информации. вообще быть потерянным, но я ни в коем случае не хочу терять младший бит.

Я мог бы использовать своего рода двоичный поиск, чтобы найти значение, которое нужно вычесть из x / (y / w), чтобы компенсировать ошибку, но я не считаю влияние использования цикла или рекурсии на производительность приемлемым.

Лучшее, что я придумал сейчас, это нормально вычислить x / (y / w) - (y % w) * x / y, а когда умножение переполнится — что, конечно, должно быть только для относительно небольшой области входных данных (конечно, меньше входных данных, чем просто вычисление x * w / y) — использовать алгоритм например, этот, чтобы захватить старшие биты продукта.

Но я все еще чувствую, что должен быть более простой способ сделать это.

person btd    schedule 20.02.2017
comment
Похоже, ваша формула похожа на первый шаг в рекурсивном решении, которое я разместил здесь: math.stackexchange.com/questions/433729/ (последняя формула в UPDATE) - person Markus A.; 20.02.2017
comment
Я подошел к этому так же, как и к случаю y <= w, который должен был определить приблизительный ответ простым делением и с добавленным (в случае y <= w) или вычтенным (в случае y > w) членом компенсации ошибки. Я придумал int q = y / w, затем x / q - (y / q - w) * x / y. По сути, это уменьшает диапазон x in [0, y) до x in [0, n), где w <= n < 2w без потери точности. Это не исключает возможность переполнения, но значительно уменьшает область входных данных, для которых это происходит. Операция эквивалентна x / (y / w) - (y % w) * x / y. - person btd; 20.02.2017
comment
О, нет, я совершенно плотный, и его нельзя переписать по модулю. Это мое плохо. x / q - (y / q - w) * x / y точка. (Хотя я также изучал вариант написания x / q - (y / q - w) * (x / q) / (y / q) для дальнейшего уменьшения области ввода, вызывающей переполнение - я думаю, но не уверен на 100%, что это не снизит точность результата.) - person btd; 20.02.2017
comment
Ваш случай также таков, что вы можете быть уверены, что y >= w^2? Если это так, вы сможете заставить работать решение, которое я опубликовал выше. Если нет, то для случаев, когда y < w^2, вы можете вернуться к математической библиотеке произвольной точности (или ее упрощенной версии, которая позволяет выполнять математику до 128-битных целых чисел)... - person Markus A.; 20.02.2017
comment
Но если вы найдете более элегантный ответ, пожалуйста, дайте мне знать! :) Я ломал голову над этой проблемой большую часть месяца назад и хотел бы получить более полное решение. - person Markus A.; 20.02.2017
comment
(Я обнаружил, что думаю о проблеме с неявным округлением из-за деления, показанного явно, как я сделал здесь: math.stackexchange.com/ a/435903/84557, чтобы быть весьма полезным для этого...) - person Markus A.; 20.02.2017

EDIT: Это решение неверно, см. обсуждение в комментариях.

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

int scaleDown2(long longPos, long longRange, int shortRange) {
    int p = longPos / (longRange / shortRange);
    return p - ((p * (longRange % shortRange)) / longRange);
}
person Falk Hüffner    schedule 18.04.2020
comment
Спасибо за ответ! Я попробую и посмотрю, смогу ли я доказать, что это работает. - person Markus A.; 18.04.2020
comment
Похоже, эта формула на самом деле неверна. Очень простой контрпример: scaleDown2(1, 3, 2) возвращает 1, но фактический ответ должен быть 0, поскольку 1*2/3 = 0,6666..., что должно округляться до 0. - person Markus A.; 18.04.2020
comment
Кажется, я неправильно понял, какую функцию вы ищете. Я пытался буквально изменить масштабирование. Чтобы scaleUp(X, 2, 3) выдавало 1, нам нужно X=1, поэтому я предположил, что это то, что мы ищем. - person Falk Hüffner; 18.04.2020
comment
В этом есть смысл. Думаю, мне следовало указать, что scaleDown не обязательно является обратным scaleUp. Мне - или, я думаю, в 2013 году ;) - нужен результат, округленный в меньшую сторону, в соответствии с формулой для posₙₑᵥᵥ. Я использовал эти сопоставления для реализации алгоритма кодирования диапазона. - person Markus A.; 19.04.2020