«Отмена» целочисленного переноса


20

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

Предположим, у вас есть приложение (C #), которое содержит некоторое число в int, называемое x. (Значение х не является фиксированным). Когда программа запускается, x умножается на 33 и затем записывается в файл.

Основной исходный код выглядит так:

int x = getSomeInt();
x = x * 33;
file.WriteLine(x); // Writes x to the file in decimal format

Несколько лет спустя вы обнаружите, что вам нужны исходные значения X обратно. Некоторые вычисления просты: просто разделите число в файле на 33. Однако в других случаях X достаточно велико, чтобы умножение вызывало целочисленное переполнение. Согласно документам , C # будет обрезать старшие биты до тех пор, пока число не станет меньше int.MaxValue. Возможно ли в этом случае:

  1. Восстановить сам X или
  2. Восстановить список возможных значений для X?

Мне кажется (хотя моя логика, безусловно, может быть ошибочной), что один или оба должны быть возможны, так как более простой случай сложения работает (по существу, если вы добавляете 10 к X, и оно переносится, вы можете вычесть 10 и снова получить X ) и умножение это просто повторное сложение. Также помогает (я полагаю) тот факт, что X умножается на одно и то же значение во всех случаях - константу 33.

Это танцевало вокруг моего черепа в нечетные моменты годами. Это произойдет со мной, я потрачу некоторое время, пытаясь обдумать это, а потом забуду об этом на несколько месяцев. Я устал гоняться за этой проблемой! Кто-нибудь может предложить понимание?

(Примечание: я действительно не знаю, как пометить это. Предложения приветствуются.)

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


13
Нечто
похожее

1
@ rwong: ваш комментарий - единственный правильный ответ.
Кевин Клайн

Да, и метод Эйлера кажется особенно эффективным, так как факторизация mвсего 2 ^ 32 или 2 ^ 64, плюс возведение в степень по aмодулю mявляется простым (просто игнорируйте переполнение там)
MSalters

1
Я думаю, что конкретная проблема на самом деле является рациональной реконструкцией
MSalters

1
@MSalters: Нет, это то, что у вас есть, r*s^-1 mod mи вам нужно найти и то, rи другое s. Здесь у нас есть r*s mod mи мы знаем все, кроме r.
user2357112 поддерживает Монику

Ответы:


50

Умножьте на 1041204193.

Когда результат умножения не помещается в int, вы не получите точный результат, но вы получите число, эквивалентное точному результату по модулю 2 ** 32 . Это означает, что если число, на которое вы умножили, было взаимно простым до 2 ** 32 (что просто означает, что оно должно быть нечетным), вы можете умножить его на обратное мультипликативное значение, чтобы получить ваш номер обратно. Wolfram Alpha или расширенный евклидов алгоритм может сказать нам, что мультипликативное обратное 33 по модулю 2 ** 32 равно 1041204193. Итак, умножьте на 1041204193, и вы получите исходный x обратно.

Если бы у нас было, скажем, 60 вместо 33, мы бы не смогли восстановить исходное число, но мы смогли бы сузить его до нескольких возможностей. Размножая 60 на 4 * 15, вычисляя обратное значение 15 mod 2 ** 32 и умножая на это, мы можем восстановить в 4 раза исходное число, оставив только 2 старших разряда числа для грубой силы. Вольфрам Альфа дает нам 4008636143 для инверсии, которая не вписывается в int, но это нормально. Мы просто находим число, эквивалентное 4008636143 mod 2 ** 32, или в любом случае принудительно устанавливаем его в int, чтобы компилятор сделал это для нас, и результат также будет обратным к 15 mod 2 ** 32. ( Получаем -286331153. )


5
О, парень. Таким образом, вся работа, которую проделал мой компьютер при создании карты, была уже выполнена Евклидом.
v010дя

21
Мне нравится фактичность в вашем первом предложении. «О, это 1041204193, конечно. Разве вы не запомнили это?» :-P
Дверная ручка

2
Было бы полезно показать пример этой работы для пары чисел, например, один, где x * 33 не переполнен, и один, где это произошло.
Роб Уоттс

2
Разум взорван. Вау.
Майкл Газонда

4
Вам не нужно ни Евклида, ни WolframAlpha (конечно!), Чтобы найти обратное 33 по модулю $ 2 ^ {32} $. Поскольку $ x = 32 = 2 ^ 5 $ является нильпотентным (порядка $ 7 $) по модулю $ 2 ^ 32 $, вы можете просто применить тождество геометрического ряда $ (1 + x) ^ {- 1} = 1-x + x ^ 2-x ^ 3 + \ cdots + x ^ 6 $ (после чего серия обрывается), чтобы найти число $ 33 ^ {- 1} = 1-2 ^ 5 + 2 ^ {10} -2 ^ {15} + \ cdots + 2 ^ {30} $, что составляет $ 111110000011111000001111100001_2 = 1041204193_ {10} $.
Марк ван Леувен

6

Это, возможно, лучше подходит в качестве вопроса к Math (Sic) SE. Вы в основном имеете дело с модульной арифметикой, поскольку отбрасывание крайних левых битов - это то же самое.

Я не так хорош в математике, как люди, которые на математике, но я постараюсь ответить.

Здесь мы имеем то, что число умножается на 33 (3 * 11), и его единственный общий знаменатель с вашим модом равен 1. Это потому, что по определению биты в компьютере являются степенями двух, и, следовательно, ваш мод некоторая сила двух.

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

Если бы это было не 33, а простое число или некоторая степень простого числа, я думаю, что ответом будет «да», но в этом случае ... спросите на Math.SE!

Программный тест

Это на C ++, потому что я не знаю C #, но концепция все еще держится. Это, кажется, показывает, что вы можете:

#include <iostream>
#include <map>

int main(void)
{
    unsigned short count = 0;
    unsigned short x = 0;
    std::map<unsigned short, unsigned short> nextprev;

    nextprev[0] = 0;
    while(++x) nextprev[x] = 0;

    unsigned short nextX;
    while(++x)
    {
            nextX = x*33;
            if(nextprev[nextX])
            {
                    std::cout << nextprev[nextX] << "*33==" << nextX << " && " << x << "*33==" << nextX << std::endl;
                    ++count;
            }
            else
            {
                    nextprev[nextX] = x;
                    //std::cout << x << "*33==" << nextX << std::endl;
            }
    }

    std::cout << count << " collisions found" << std::endl;

    return 0;
}

Заполнив такую ​​карту, вы всегда сможете получить предыдущий X, если знаете следующий. Всегда существует только одно значение.


Почему работа с неотрицательным типом данных будет проще? Разве подпись и неподписание не обрабатываются одинаково на компьютере, отличается только их формат вывода для человека?
Xcelled

@ Xcelled194 Ну, мне легче думать об этих числах.
v010дя

Достаточно справедливо xD Человеческий фактор ~
Xcelled

Я удалил это утверждение о неотрицательных, чтобы сделать его более очевидным.
v010дя

1
@ Xcelled194: неподписанные типы данных следуют обычным правилам модульной арифметики; подписанных типов нет. В частности, maxval+10 только для неподписанных типов.
MSalters

2

Один из способов получить это - использовать грубую силу. Извините, я не знаю C #, но ниже приведен псевдокод c-like, иллюстрирующий решение:

for (x=0; x<=INT_MAX; x++) {
    if (x*33 == test_value) {
        printf("%d\n", x);
    }
}

Технически, вам нужно x*33%(INT_MAX+1) == test_valueтолько целочисленное переполнение, которое автоматически выполнит эту %операцию за вас, если ваш язык не использует произвольные целые числа точности (bigint).

Это дает вам серию цифр, которые могли быть исходными. Первое напечатанное число будет числом, которое будет генерировать один раунд переполнения. Второе число будет числом, которое будет генерировать два раунда переполнения. И так далее..

Итак, если вы знаете свои данные лучше, вы можете сделать более правильное предположение. Например, обычная математика часов (переполнение каждые 12 часов), как правило, делает первое число более вероятным, поскольку большинство людей интересуются событиями, которые произошли сегодня.


C # ведет себя как C с базовыми типами - т.е. intэто 4-байтовое целое число со знаком, которое оборачивается, поэтому ваш ответ по-прежнему хорош, хотя перебор не будет лучшим способом, если у вас много входных данных! :)
Xcelled

Да, я попытался сделать это на бумаге с правилами алгебры по модулю отсюда: math.stackexchange.com/questions/346271/… . Но я застрял, пытаясь понять это, и в итоге
нашел

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

@slebetman Посмотри на мой код. Кажется, что есть только один ответ, когда дело доходит до умножения на 33.
v010dya

2
Исправление: C intне гарантированно оборачивается (см. Документацию вашего компилятора). Это верно для неподписанных типов, хотя.
Томас Эдинг

1

Вы могли бы решить SMT Z3, чтобы попросить дать вам удовлетворительное задание для формулы x * 33 = valueFromFile. Это инвертирует это уравнение для вас и даст вам все возможные значения x. Z3 поддерживает точную битовую векторную арифметику, включая умножение.

    public static void InvertMultiplication()
    {
        int multiplicationResult = new Random().Next();
        int knownFactor = 33;

        using (var context = new Context(new Dictionary<string, string>() { { "MODEL", "true" } }))
        {
            uint bitvectorSize = 32;
            var xExpr = context.MkBVConst("x", bitvectorSize);
            var yExpr = context.MkBVConst("y", bitvectorSize);
            var mulExpr = context.MkBVMul(xExpr, yExpr);
            var eqResultExpr = context.MkEq(mulExpr, context.MkBV(multiplicationResult, bitvectorSize));
            var eqXExpr = context.MkEq(xExpr, context.MkBV(knownFactor, bitvectorSize));

            var solver = context.MkSimpleSolver();
            solver.Assert(eqResultExpr);
            solver.Assert(eqXExpr);

            var status = solver.Check();
            Console.WriteLine(status);
            if (status == Status.SATISFIABLE)
            {
                Console.WriteLine(solver.Model);
                Console.WriteLine("{0} * {1} = {2}", solver.Model.Eval(xExpr), solver.Model.Eval(yExpr), solver.Model.Eval(mulExpr));
            }
        }
    }

Вывод выглядит так:

SATISFIABLE
(define-fun y () (_ BitVec 32)
  #xa33fec22)
(define-fun x () (_ BitVec 32)
  #x00000021)
33 * 2738875426 = 188575842

0

Отмена этого результата даст вам ненулевое конечное количество чисел (обычно бесконечное, но intконечное подмножество ℤ). Если это приемлемо, просто сгенерируйте числа (см. Другие ответы).

В противном случае вам нужно вести список истории (конечной или бесконечной длины) истории переменной.


0

Как всегда, есть решение от ученого и решение от инженера.

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

Вот быстрое решение от инженера, которое не заставит вас попробовать все возможные целые числа.

val multiplier = 33 //used with 0x23456789
val problemAsLong = (-1947051863).toLong & 0xFFFFFFFFL

val overflowBit = 0x100000000L
for(test <- 0 until multiplier) {
  if((problemAsLong + overflowBit * test) % multiplier == 0) {
    val originalLong = (problemAsLong + overflowBit * test) / multiplier
    val original = originalLong.toInt
    println(s"$original (test = $test)")
  }
}

Какие есть идеи?

  1. Мы получили переполнение, поэтому давайте использовать более крупные типы для восстановления (Int -> Long )
  2. Мы, вероятно, потеряли некоторые биты из-за переполнения, давайте восстановим их
  3. Переполнение было не более Int.MaxValue * multiplier

Полный исполняемый код находится по адресу http://ideone.com/zVMbGV

Детали:

  • val problemAsLong = (-1947051863).toLong & 0xFFFFFFFFL
    Здесь мы конвертируем наше сохраненное число в Long, но поскольку Int и Long подписаны, мы должны сделать это правильно.
    Таким образом, мы ограничиваем число, используя побитовое И с битами Int.
  • val overflowBit = 0x100000000L
    Этот бит или его умножение могут быть потеряны при начальном умножении.
    Это первый бит за пределами диапазона Int.
  • for(test <- 0 until multiplier)
    Согласно 3-й идее, максимальное переполнение ограничено множителем, поэтому не пытайтесь делать больше, чем нам действительно нужно.
  • if((problemAsLong + overflowBit * test) % multiplier == 0)
    Проверьте, добавив, возможно, потерянное переполнение, мы пришли к решению
  • val original = originalLong.toInt
    Первоначальная проблема была в диапазоне Int, поэтому давайте вернемся к ней. В противном случае мы могли бы неправильно восстановить числа, которые были отрицательными.
  • println(s"$original (test = $test)")
    Не ломайте после первого решения, потому что могут быть другие возможные решения.

PS: 3-я идея не совсем верна, но оставлена ​​так, чтобы быть понятной.
Int.MaxValueесть 0x7FFFFFFF, но максимальное переполнение есть 0xFFFFFFFF * multiplier.
Поэтому правильным текстом будет «Переполнение было не более чем -1 * multiplier».
Это правильно, но не все это поймут.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.