Pull to refresh

Comments 75

Лучше рассказал бы, зачем вообще кому-то может понадобиться брать остаток от деления на отрицательное число. Ну, кроме как на собеседовании или позанудствовать, конечно…
Так в шифровании применяется. Я даже помню вопрос на стак выкладвал, потому что никак не мог посчитать уравнение, а там как раз такая арифметика была.

p.s. в итоге решил сам
Ок, если не трудно, подскажите пожалуйста, в каком именно алгоритме шифрования используются отрицательные числа, по которым нужно брать остаток деления. Мне просто интересно, собственно, я таких не знаю и нагуглить сходу не получается.
Афинное шифрование, например. Т.е. у вас есть монограммы и каждая буква шифруется формулой: E(x)=(ax+b)%m. Тут-то и применяется. У меня даже было несколько живых примеров, надо поискать просто(т.к. в тетрадке)
В афинном шифровании m — это кол-во букв в алфавите (биграмм, триграмм..). Оно не может быть отрицательным.
Но вы можете использовать сопоставление отрицательных чисел буквам и тогда получится отрицательный ответ.
Тем не менее, в афинном шифровании (да и в остальных мне известных криптографических методах) взятие остатка от деления на отрицательное число не используется.
Генерация псевдо-случайных чисел оперирует либо битами, либо беззнаковыми числами. Операция деления и взятия остатка, в отличие от умножения, не дешевая, и на практике в генерации псевдо-случайных чисел не используется.
В джаве полно беззнаковых чисел ага?
И более того, если где-то и есть возможность прилепить беззнаковые числа, то лучше разработать такой алгоритм, что бы все таки числа были со знаком, для совместимости оно будет проще потом в реализациях на разных языках.
И вообще кое-где беззнаковые — некомплиант…

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

Я же например стараюсь вообще беззнаковые не использовать, ибо потом можно здорово геморроя хапнуть.
Гхм. Что? Простите. Не так выразился.

ЧТО?!

На самом деле, unsigned в java не завезли, потому что не смогли. Почему не смогли? Скорее всего выбилось из дизайна виртуальной машины, а добавление поддержки «отдельной» арифметики было слишком сложно… Ну… И повелось, гхм. Вообще, беззнаковая арифметика не просто нужна, она кислородоподобна! Но на таких языках, как Java контроль памяти невозможен и обход ограничений через большую размерность не такая уж и проблема. А так, любая работа с криптографией, сетью, числодробительной графикой и прочей магией — убога, ужасна, стуло- и столо- (а в особых случаях, и мониторо-) ломающая.

Что до геморроя. Не знаю, может мне очевидно поведение unsigned. С другой стороны, может я чего-то действительно не понимаю, поэтому банально не вижу проблем. В остальном, сколько не работал с embedded на Си и Си++, пока только uint32_t, uint16_t и uint8_t. int32_t, int16_t и int8_t возникают в довольно редких, практически единичных случаях. Да и в принципе, отрицательные числа, внезапно, оказывается довольно редким явлением. И на самом деле обойтись без них — спокойно, хоть вручную проверяй знаковый бит. Куда запутаннее, сложнее и интереснее плавающие запятые, но это тема отдельной истории.
> В джаве полно беззнаковых чисел ага?

Это скорее её недоработка (хотя начиная с 8ки есть явное оформление ряда операций как беззнаковых).

Вообще, беззнаковость сама по себе не проблема. Проблема — что происходит, когда выходишь за домен значений.

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

Вот в C/C++ часто наоборот: для знаковых переполнение может случайно вызвать дежурного сатанаильчика, а для беззнаковых чётко однозначная двоичная арифметика.
И не только этим шифрованием един. Это также может использоваться в различных реализациях генераторов псевдо-случайных чисел.
Ну собственно, вот статья: там же кроме положительных можно использовать отрицательные в связке с биграммами, триграммами и т.д.
UFO just landed and posted this here

Крайне удобно, но неправильно, если вам важно распределение.

Однажды мне довелось столкнуться с этим — писал свой клон майнкрафта на Юнити, и мне нужно было получить значение координаты Х и У относительно «текущего» чанка (т.е. к примеру, глобальная округленная координата Х равна 35, чанки имеют размер 16 на 16, следовательно, первая мысль — взять остаток от деления 35 % 16 и получить искомое «3»), но как только мы подаем на вход отрицательную координату, получаем не то, что ожидали. К слову, подобная функция конвертации глобальных координат в локальные использовалась много где (каждый чанк хранит свои данные о ландшафте в виде массива [16, 16, 256]).
Отрицательная координата ещё куда ни шло, но речь идет о взятии остатка от деления на отрицательное число, а не отрицательного на положительное.
Конечно, Ваша ситуация вполне подошла бы, если бы размер чанка был отрицательным )
Сейчас сочиню вам пример, если желаете:
У вас есть прибор с валкодером. Вы пишете ПО для этого прибора. Прибор умеет считать сколько раз повернули валкодер. Против часовой стрелки он считает шаги с «плюсом», а по часовой с «минусом». Раньше этим валкодером регулировалась только громкость и всё было более-менее в порядке. В какой-то момент заказчик захотел Прибор 2.0, в котором на валкодере будет метка, а на корпусе пиктограммы с какими-то режимами. Очевидно, что пользователи будут крутить валкодер во все стороны и много раз. Если программист пользуется операцией `%` здорового человека, то ему достаочно текущее суммарное кол-во шагов валкодера поделить по модулю кол-ва пунктов меню и он получит текущий номер пункта меню. В случае с JS, например, это не так.
Однако вся статья была бы гораздо понятнее в виде вот такой таблицы:
'''
 Expression   |   JS   | Python |
--------------+--------+--------+
(19)%(12)     |    7   |   7    |
(-19)%(12)    |   -7   |   5    |
(19)%(-12)    |    7   |  -5    |
(-19)%(-12)   |   -7   |  -7    |
'''
В Вашем случае берется остаток от деления позиции валкодера (может быть отрицательныс числом) на кол-во пунктов меню (всегда положительное) — остаток от деления на отрицательное число не берется.
Не обязательно делить _на_ отрицательное число, достаточно, чтобы делимое было отрицательным.

При делении -13 на 4, например, T-деление (обычное для процессоров и стандартизованное в C) даёт частное -3 и остаток -1.
F-деление (например, операция "//" в Python3) даёт -4 и остаток 3.

Вот список возможных вариантов. T- и F-деление — два самых частых. Но встречаются и E-деление (остаток всегда неотрицательный), и более экзотические варианты.

А получить задачу для F-деления тривиально. Вот, например, подсчитать номер дня недели. В некоторых методах можно получить отрицательное промежуточное число…
В эллиптической криптографии необходимо находить «обратные числа по модулю» с помощью расширенного алгоритма евклида, а так же находить много остатков от деления. При этом, алгоритм евклида даёт в результате как отрицательные, так и положительные числа. Например, 3 mod 11. Обратным числом будет являться 4, так как 3 * 4 mod 11 = 1.
Но -1 — тоже правильный ответ, поскольку, 3 * (-1) = -3. А -3 mod 11 = 1. И такие вычисления там повсеместно.

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

А -3 mod 11 = 1

Объясните, пожалуйста. А то у меня из базовой логики получается 8, а не 1.

Кстати, заметили ли вы что -5 (один ответ) и -7(другой) дают -12?
Если не привлекать циферблаты и куда крутить то можно объяснить так:
Классическое определение деления с остатком: a = b*q + r, где r — и есть остаток.
Для положительных чисел это формула тривиальна: b*q меньше a. А вот для отрицательных возникает вопрос — каким должен быть b*q — меньше? Так если меньше тогда абсолютное значение b*q больше, чем a и остаток всегда будет положительным.
Разные языки программирования\системы используют оба варианта реализации.
UFO just landed and posted this here
В классическом определении есть ещё вот это:
«На остаток налагается дополнительное условие: 0 <= r < |b| то есть остаток от деления должен быть неотрицательным числом и по абсолютной величине меньше делителя. „
Угу, после матлаба, где функция mod работает как учат в курсе математики долго не мог привыкнуть к поведению оператора % в C/C++.
Потому что C/C++ это так:
(x % y) = x — trunc(x/y)*y, а в математике принято так:
(x % y) = x — floor(x/y)*y.
Вот и приходится постоянно всюду дописывать свою функцию.

В C++ (и в C) требуется чтобы выполнялось равенство (a / b) * b + a % b = a. Я натыкался на различное поведение в разных компиляторах для отрицательных чисел.

В равенстве где-то ошибка: после преобразования получилось:a + a % b =a-это значит что остаток всегда равен 0, а это неверно.

a / b здесь — целочисленное деление, если оба операнда

Оно выполняется для обоих подходов.

В том-то и дело. Разные компиляторы могут реализовать разные подходы, поэтому написать переносимый код становится несколько сложнее.

Очень полезное замечание.
И аналогия с циферблатом весьма наглядна. Мне, кстати, больше нравится идея крутить его в обратную сторону для отрицательного модуля.

Осталось только не забыть его к тому моменту, когда вдруг действительно потребуется вычислять modulo с отрицательным модулем.
По мне, остаток от деления должен быть вообще всегда неотрицательным. Т.е. 19 mod 12 === 7, 19 mod (-12) === 7, (-19) mod 12 === 5. И никаких -5 или -7!
По мне, остаток от деления должен быть вообще всегда неотрицательным.
В математике это так же, а в программировании — как получится.
Как я понимаю, началось с того, что в 8086 IDIV делил со знаком и пихал остаток тоже со знаком, возможно, так было проще разработать процессор. Потом в Си, чтобы упростить трансляцию на х86, разрешили x%y быть меньше нуля, и понеслась. stackoverflow.com/questions/49602476/intel-x86-can-idiv-yield-a-negative-remainder
80x86 тут далеко не первый (кстати, в исходном 8086 умножение и деление были только беззнаковые; знаковое появилось одновременно в 80186 и 80286). Т-деление было ещё в S/360. Вообще, сложно найти процессор с другим поведением. Обычно это обосновывают тем, что T-деление
1) проще всего реализуется в широком диапазоне подходов аппаратного деления;
2) лучше всего способствует многоэтапному делению в длинной арифметике.
«Впрочем, не будет отклоняться от пути.»
Кто не будет?

В Java:


public class ModuloTest {
    public static void main(String[] args) {
        System.out.println("19 %  12 = " + 19 % 12);
        System.out.println("19 % -12 = " + 19 % (-12));
    }
}

Будет вот так:


19 %  12 = 7
19 % -12 = 7
А вот Lua вполне справляется:
print(19 % 12) -- 7
print(19 % -12) -- -5
> Если я скажу вам, что 9 является результатом возведения в квадрат, вы можете легко определить, что на входе было 3.

[зануда mode]
-3 в квадрате так же даст 9 ;)
[/зануда mode]
Два-три варианта лучше чем чем бесконечность.
[зануда mode]
Бесконечность не помещается в фиксированное количество бит, так что не бесконечность.
[/зануда mode]
ru.wikipedia.org/wiki/IEEE_754-2008
Формат IEEE 754 представляет собой «совокупность представлений числовых значений и символов». Формат может также включать в себя способ кодирования.
Формат включает:
Числа
Положительный нуль +0 и отрицательный нуль -0.
Две бесконечности: +∞ и −∞.
Два вида NaN
Это, конечно, похвально, что вы знаете про IEEE 754. Но модуль (или остаток, называйте как хотите) не применим на эти бесконечности — результат NaN. В любом случае даже IEEE754 не может вместить в себя все возможные числа — с фиксированным числом бит это просто невозможно.
Если задаться целью то вполне можно реализовать генерацию чисел до того которого очень хочется. Только возникает вопрос зачем и готовы ли вы ждать бесконечность и предоставить бесконечное количество энергии? Думаю никто не доживет и первого десятка лет
Теперь переходим к сути: modulo и простой остаток одинаковы, когда числа положительны, но отличаются в случае отрицательных чисел.
Ну и ну. Мне казалось, что это даже на собеседованиях уже не спрашивают, а тут на хабре такой срыв покровов, прям целый перевод.
Вы не поверите, но спрашивают. Буквально пол года назад в Amazon…
Это будет 6 + 16 mod 12


Хоть в данном примере результат и не меняется, но все-таки (6 + 16) mod 12.
Питон:
>>> divmod(19, 12)
(1, 7)
>>> 19//12, 19%12
(1, 7)
>>> divmod(19, -12)
(-2, -5)
>>> 19//-12, 19%-12
(-2, -5)
В Паскалях несколько иначе:
begin Writeln(19 mod 12:4, -19 mod -12:4, -19 mod 12:4, 19 mod -12:4) end.
7 7 -7 -7

Я понял! Минус считается отдельно от логики простым умножением 11=1,-11=-1, -1*-1=1 типа кому нужно сам abs поставит.

Знак умножения удалился...

Два знака умножения сделали курсив (надо было отключить "[ ] Markdown"?).

Скобки надо расставить для чистоты эксперимента. Хотя унарный минус объявлен как операция с высшим приоритетом, мало ли как расставляет приоритеты конкретный паскаль. Но -7 — как-то перебор.

Edit: пост от a-tk показывает, что этот паскаль взял в лоб idiv. Тогда нормально.
Так много написано, с кусками кода в картинках (что, как мне кажется, выглядит не очень красиво). А тем временем на википедии по ссылке отличное описание в один абзац, а ниже таблица с тем, как же берется остаток от деления в конкретном языке программирования.
В той же java вы можете использовать или так или эдак :)
Я помню как будучи ещё школьником мне об этом рассказывал профессор криптографии из МФТИ, когда он объяснял мне афинное шифрование. Я каждый раз впадал в эйфорию, когда мог разгадать(вычислить) очередной ключ!
А вот что считает x86 в этой ситуации:
    std::cout << m(19, 12) << std::endl;
    std::cout << m(19, -12) << std::endl;
    std::cout << m(-19, 12) << std::endl;
    std::cout << m(-19, -12) << std::endl;

Реализация:
int __cdecl m(int a, int b)
{
    __asm
    {
        mov eax, a
        mov ebx, b
        cdq
        idiv ebx
        xchg eax, edx
    }
}

Вывод: 7 7 -7 -7
19 % 12 - отложить вектор +12 +1 раз, остается 7
                                        7
                                     ------>
                        .------------------>     
                        .----------->
                        0

19 % -12 - отложить вектор -12 -1 раз, остается 7
                                        7
                                     ------>
                        .------------------>     
                        .<-----------
                        0

-19 % 12 - отложить вектор +12 -1 раз, остается -7
       -7      
     <------            
     <------------------.
            ----------->.
                        0

-19 % -12 - отложить вектор -12 1 раз, остается -7
       -7      
     <------            
     <------------------.
            <-----------.
                        0

А для модулей видимо получается так:


A всегда раньше по направлению, чем B.

 -19 % 12                              19 % 12

  5                                    7
---->.                              ------>     
     >                                    >     
----------->----------->----------->----------->
A    B                  0           A     B

 -19 % -12                             19 % -12

       -7                                    -5
     <------                              .<----
     <                                    <     
<-----------<-----------<-----------<-----------
     B     A           0                  B    A
Насколько я понимаю, речь идёт о построении кольца вычетов по некоторому натуральному модулю n, т.ч. отрицательные n не рассматриваются. Из колец вычетов можно получить поля вычетов. При n меньше единицы имеющаяся теория не работает, нужно строить другую (хотя, наверное, незачем). ПМСМ, автор исходной статьи просто плохо знаком с теорией.

В криптографии сравнения можно встретить в системах с открытым ключом, использующих, например, алгоритм RSA или протокол Диффи — Хеллмана. Также, модульная арифметика обеспечивает конечные поля, над которыми затем строятся эллиптические кривые, и используется в различных протоколах с симметричным ключом (AES, IDEA). (источник)
Целочисленно делим 19 на -12, получаем -1.
Далее остаток от деления 19 — (-12 * -1) = 7
Тут все логично.
А что такое -5 я так и не понял, Очень похоже, что это действительно ошибка в процессоре, баг возведенный в ранг фичи.
А, дошло, они не отбрасывают дробную часть, как полагалось бы, а берут меньшее целое число. Не сразу сообразил, как альтернативная математика работает.
Не сразу сообразил, как альтернативная математика работает.
Это как раз обычная математика.
Из курса "Основы программирования на Python" (Coursera):

Особо остановимся на операциях вычисления целой части и остатка от деления от числа.
Пусть заданы два числа A и B, причем B > 0. Обозначим за C целую часть от деления A на B, C = A // B, а за D — остаток от деления A на B, D = A % B.
Тогда должны выполняться следующие утверждения:
A = B × C + D
0 ≤ D < B
Эти утверждения необходимы для понимания процесса взятия остатка от деления отрицательного числа на положительное. Нетрудно убедиться, что если -5 разделить на 2, то целая часть должна быть равна -3, а остаток равен 1.
В некоторых других языках программирования остатки в такой ситуации могут быть отрицательными, что неправильно по математическим определениям.
В случае, если B < 0 выполняются следующие утверждения:
A = B × C + D
B < D ≤ 0

Например, при делении 11 на -5 мы получим целую часть равную -3, а остаток будет равен -4. Если же разделить -11 на -5, то целая часть будет равна 2, а остаток будет равен -1.
В официальной документации объяснение лаконичнее:
Почему -22 // 10 (прим.: целочисленное деление) возвращает -3?
Это следствие того, что i % j (прим.: остаток от деления i на j) имеет тот же знак, что и j. Чтобы так было, а также выполнялось:
i == (i // j) * j + (i % j)

целочисленное деление должно округлять вниз [в сторону минус бесконечности]. «Си» тоже требует соблюдать это равенство, и компиляторы, отбрасывающие дробную часть при i // j, должны заставить i % j принимать тот же знак, что и i.

Мало реальных случаев применения i % j, когда j отрицательно, но таких случаев много, когда j положительно, и практически во всех из них гораздо полезнее, чтобы i % j был >= 0. Если на часах сейчас 10, сколько на них было 200 часов назад? -190 % 12 == 2 — полезно; -190 % 12 == -10 — ошибка, которая ещё аукнется.

Оригинал
Why does -22 // 10 return -3?
It’s primarily driven by the desire that i % j have the same sign as j. If you want that, and also want:
i == (i // j) * j + (i % j)

then integer division has to return the floor. C also requires that identity to hold, and then compilers that truncate i // j need to make i % j have the same sign as i.

There are few real use cases for i % j when j is negative. When j is positive, there are many, and in virtually all of them it’s more useful for i % j to be >= 0. If the clock says 10 now, what did it say 200 hours ago? -190 % 12 == 2 is useful; -190 % 12 == -10 is a bug waiting to bite.

(Programming FAQ — Python 3.7.0 documentation)

Кстати, я нахожу забавным, что оператор взятия остатка от деления в официальной документации всё того же «Пайтона» называется и «modulo» (описание бинарных операций на странице «Выражения» справочника по языку), и «remainder» (таблица приоритетов операций на той же самой странице). Вопреки мнению автора данной статьи.
Если следовать этой логике, то -100 минут будет -2ч+20м, вместо -1ч40м.
Хотя стрелка будет указывать на 20м (если отсчитывать -100 минут от 0:00), да…
В общем, и так и так можно нарваться на неприятности, поэтому случаи с отрицательными числами при взятии остатка лучше обрабатывать отдельной логикой, а не полагаться на реализацию в компиляторе или cpu.
php
echo "19 % 12 = " . 19 % 12 . "\n";
echo "19 % -12 = " . 19 % -12;

Выводит
19 % 12 = 7
19 % -12 = 7

Haskell (GHC 8.0.1), как в Python


>  19 `divMod` 12
=> (1,7)
>  19 `divMod` (-12)
=> (-2,-5)
Вообще на практике можно увидеть до 5 вариантов определения, как выбирать знак остатка при операции деления.
Небольшое описание всех вариантов с их свойствами.
Список вариантов реализации в разных языках.
T-, F-, E-деление уже обсудили тут во всех подробностях.
С-деление — самое редкое, есть, например, в GMP library (семейство функций mpz_cdiv_*).
R-деление стандартизовано в IEEE754, потому что в этом случае получается всегда вычислить точный остаток. Но это уже специфика плавающей точки, не актуальная для целых чисел.
Так и деление может быть с округлением в меньшую сторону или к нулю. Остаток/модуль должен быть с делением согласован.
1C. Версия 8.3.10.2699:
Сообщить(19 % 12); // 7
Сообщить(19 % -12); // 7
Сообщить(-19 % 12); // -7
Сообщить(-19 % -12); // -7

1С. 7.70.027
Сообщить(19 % 12); // 7
Сообщить(19 % (-12)); // 7

Спасибо за статью, вчера попался на этом: -11 % 2 - в Питоне 3.9 будет 1, в С# 5 будет -1

Sign up to leave a comment.

Articles