No history yet

Modular Operations

Transcript

Beau

Okay, so last time we talked about congruence and residue classes... and my brain is still kind of spinning with the whole 'buckets of numbers' idea.

Jo

It's a big shift, right? Moving from the remainder as just an answer to seeing it as a whole system. But it's that system that lets us do some really powerful stuff, especially when numbers get ridiculously big.

Beau

Which is exactly where I get stuck! I was working on a problem, a combinatorics thing, and the intermediate products were... huge. They blew past what even a 'long long' in C++ could hold. I know the solution involves taking the modulo, but I don't get *why* I'm allowed to just... shrink the numbers in the middle of a calculation. It feels like cheating.

Jo

It's not cheating, it's just doing math in a different world. Remember the congruence idea? If two numbers are in the same 'bucket' modulo n, we can kind of treat them as interchangeable for certain operations. This is where the distributive properties come in.

Beau

Distributive... okay, walk me through it. Let's start simple, with addition.

Jo

Perfect. So, the rule is that `(a + b) % n` is exactly the same as `((a % n) + (b % n)) % n`.

Beau

So... I can shrink `a` and `b` first, add them, and then shrink the result? And that gives the same answer as adding the giant numbers and then shrinking that final sum?

Jo

Exactly. Imagine we're working with a clock, so modulo 12. Let's say we have 15 and 20. `(15 + 20) % 12` is `35 % 12`, which is 11.

Beau

Okay, simple enough.

Jo

Now let's use the property. What's `15 % 12`?

Beau

Three.

Jo

And `20 % 12`?

Beau

Eight.

Jo

So now we do `(3 + 8) % 12`. That's `11 % 12`, which is... 11. Same result. We never had to deal with a number bigger than 12.

Beau

Aha! And that's the key for my coding problem. Instead of letting my `long long` overflow when adding two massive numbers, I can just take the modulo of each one first. The numbers stay small.

Jo

Precisely. And the same logic applies to subtraction. `(a - b) % n` is the same as `((a % n) - (b % n)) % n`. But there's a little trap here.

Beau

I think I see it. What if `a % n` is smaller than `b % n`? We'd get a negative number. And... C++ and math handle negative modulo differently, which we talked about.

Jo

You got it. `(5 - 8) % 12` is `-3`. And in C++, `-3 % 12` is `-3`. But in pure math, we want a result between 0 and 11. We want 9.

Beau

So how do we fix that in code without a messy `if` statement?

Jo

There's a neat trick. You do the subtraction, add the modulus, and then take the modulus again. So, `(a - b + n) % n`.

Beau

Okay... `(5 - 8 + 12) % 12`. That's `(-3 + 12) % 12`, which is `9 % 12`. And that's 9. It works. What if the result was already positive?

Jo

Try it. Let's say, `(8 - 5)`. `(8 - 5 + 12) % 12` is `(3 + 12) % 12`, which is `15 % 12`... which is 3. It works either way. Adding `n` just bumps a negative result up into the right range without pushing a positive result out of its correct congruence class.

Beau

That's clean. I like that. So, for my code, I'd write something like `long long diff = (a % MOD - b % MOD + MOD) % MOD;`. And that's safe.

Jo

Perfectly safe. Now, let's get to the one that really causes overflows: multiplication.

Beau

Okay, I'm guessing the property is similar? `(a * b) % n` is the same as `((a % n) * (b % n)) % n`?

Jo

You nailed it. That's the one. Let's take `(10 * 10) % 7`. That's `100 % 7`. Uh... a little mental math... `100` is `70 + 30`, `30` is `28 + 2`, so the remainder is 2.

Beau

Okay, I'm with you. The result is 2.

Jo

Now the other way. `10 % 7` is 3. So we get `(3 * 3) % 7`, which is `9 % 7`... which is 2. Again, same result, but we never dealt with a number as big as 100.

Beau

And this is the core of solving those combinatorial problems. If I have to calculate something like `(a * b * c * d) % n`, I can just do `(((a % n * b % n) % n * c % n) % n * d % n) % n`. I take the modulo at every single step.

Jo

That's it! By taking the modulo after each multiplication, you guarantee the number you're working with never grows larger than `n` squared, which keeps it well within the bounds of a `long long` for typical competitive programming moduli.

Beau

So wait, if my modulus is, say, `10^9 + 7`, and my numbers `a` and `b` are also just under that, then `a % MOD` and `b % MOD` could be close to `10^9`. Their product would be close to `10^18`. That fits inside a `long long`.

Jo

Exactly. The maximum value for a `long long` is about `9 * 10^18`, so you're safe. You just have to remember to cast to `long long` before you multiply to avoid the intermediate overflow. So in C++, it would be `(1LL * a * b) % MOD`.

Beau

That `1LL` trick. Got it. So basically, these properties let me stay in the 'modulo world' throughout my entire calculation, ensuring my numbers never escape and overflow. It's not cheating, it's just... working smarter.

Jo

It's working in a finite field, but yeah, 'working smarter' sounds better. Now, what do you think happens when we bring division into this?

Beau

Oh no. I have a feeling it's not as simple as just using the division operator.