skip to content

In a counter accumulating a product mod 1e9+7, why reduce after every multiply instead of at the end?

level: middleimportance: must knowfreq 55%

answer

  1. what grows if you never reduce?
  2. the final answer is unchanged either way
  3. think about the accumulator's fixed width
  4. both factors stay below the modulus
  5. so the product stays below the modulus squared

basics

~20 s

Reducing every step and reducing once at the end give the same answer but not the same intermediates. With both factors below the modulus every product fits a fixed-width accumulator; deferring reduction lets the running value overflow silently.

solid answer

~50 s

The mathematics is identical either way: multiplication is compatible with congruence, so `(a * b) mod m == ((a mod m) * (b mod m)) mod m`. What differs is the *representation* of the intermediate. If you reduce every step, both operands entering each multiply are below `m`, so the product is below `m^2` — with `m` around `10^9` that is under `10^18`, which fits a 64-bit signed accumulator with room to spare. If you defer, the running product grows by roughly `log2(factor)` bits per iteration and passes the accumulator's width after a handful of terms; on a fixed-width machine it wraps silently and the output is meaningless, and on an arbitrary-precision runtime it stays correct but the multiply stops being constant-time and the loop degrades badly. Reduce the incoming factor too, not just the accumulator, if it can exceed the modulus.

code

pseudocode · 14 lines
pseudocode
M = 1000000007

// deferred: intermediate grows without bound
result = 1
for i in 0..n-1
    result = result * choices[i]
...
answer = result mod M

// reduced: every operand stays below M, so every product stays below M*M
result = 1
for i in 0..n-1
    result = (result * (choices[i] mod M)) mod M
answer = result

go deeper

for a junior

Know the rule and apply it: put the reduction inside the loop, on every multiply and every add. Be able to say that the final answer is unchanged and only the intermediate size differs.

for a middle

Explain the bound out loud — both operands below the modulus means the product is below the modulus squared, which is what keeps the accumulator from overflowing. State the congruence identity that makes the rewrite exact.

for a senior

Show you can size this: given an accumulator width, derive the largest safe modulus, and name the silent-wrap failure mode that passes small tests and fails at scale. Flag unreduced incoming factors and unnormalized subtractions in review.

for a principal

Own the convention across a codebase: a single reduce-and-normalize helper used by every counting path beats per-site discipline, and it makes the accumulator-width assumption explicit and testable rather than folklore carried in reviewers' heads.

## Why counting problems carry a modulus at all A combinatorial counter — how many distinct configurations satisfy some constraint — produces answers that grow factorially or exponentially in the input size. The exact value for `n = 200` has hundreds of digits and does not fit in any machine word. Rather than force every solution to carry big-integer arithmetic, the convention is to ask for the answer *modulo* a fixed prime, almost always `1000000007`. That turns an unbounded number into one that fits in a machine word, and it makes the expected output a single comparable value. ## The congruence that licenses early reduction Modular reduction is compatible with addition, subtraction and multiplication: ``` (a + b) mod m == ((a mod m) + (b mod m)) mod m (a * b) mod m == ((a mod m) * (b mod m)) mod m ``` So pushing the reduction inside the loop does not perturb the final answer at all — it is an exact rewrite, not an approximation. Candidates sometimes hesitate here, worried they are "losing information". They are not: every value the loop cares about is already only defined up to congruence. ## What actually changes: the size of the intermediate Hold the two loops side by side. In the deferred version the running product is the *true* product, and its bit-width is the sum of the bit-widths of everything multiplied in so far. Multiply in ten values around a billion and you already need about 300 bits. In the reduced version, every operand entering a multiply is at most `m - 1`, so the product is at most `(m-1)^2`. With `m = 1000000007`, that is just under `10^18`, comfortably inside the roughly `9.2 * 10^18` a 64-bit signed accumulator holds. Reduce again and you are back below `m`, ready for the next iteration. The loop's intermediates are bounded no matter how many iterations run. That bound is the entire design. It is also why the conventional modulus sits just under `2^30`: two residues multiply to under `2^60`, which leaves headroom in a 64-bit word, while sums of two residues need only 31 bits. ## Two different failure modes What goes wrong when you defer depends on what a multiply means on your platform, and mainstream runtimes made genuinely different calls here. Python and Ruby promote transparently to arbitrary-precision integers: the deferred loop stays *correct*, but each multiply now costs time proportional to the operand sizes, so a loop that should be linear becomes markedly superlinear and the memory for the intermediate grows without bound. C, Java and Go use fixed-width machine integers: the deferred loop wraps silently at the word boundary with no signal at all, and the final reduction is applied to a number that lost its high bits many iterations ago. The second failure is the dangerous one — the program is fast, quiet and completely wrong, and small test inputs pass because they never reach the overflow threshold. Note what the wrapped value is *not*: it is a correct result modulo `2^64`, and knowing a number modulo `2^64` tells you nothing about it modulo `1000000007`, because the two moduli are coprime. There is no salvage step. ## Details that trip people up **Reduce the factor, not only the accumulator.** If the incoming term can itself exceed the modulus — a precomputed value, a user-supplied count, a sum you built elsewhere — reduce it before the multiply, or the product can exceed `m^2` and the width argument collapses. **Subtraction needs a second thought.** Inclusion-exclusion counters subtract. Two residues in `0..m-1` can produce a negative difference, and taking a remainder of an already-small negative number leaves it negative. Add the modulus back before continuing, or the next multiply propagates a negative value through the rest of the loop. **Division is not on this list.** The congruence above covers `+`, `-` and `*`. Dividing residues is a different operation entirely and needs a modular inverse. **Exponentiation follows the same discipline.** Raising to a power is repeated multiplication, so the same reduce-every-step rule applies at each squaring; the bound argument is unchanged. ## Sizing it yourself The general rule: if you are reducing modulo `m`, your accumulator must hold `(m-1)^2` for a multiply-based pipeline, or roughly `2m` for an addition-only pipeline. Work backwards from the accumulator width you have to the largest modulus you can afford, rather than picking a modulus and hoping. That calculation is what an interviewer is really probing when they ask why the reduction goes inside the loop.

  • Does reducing at every step ever change the final answer?
    No. Multiplication and addition are compatible with congruence, so replacing any operand by its residue leaves the final residue identical. It is an exact rewrite, not an approximation. The only thing early reduction changes is the magnitude of the intermediate values — which is exactly why you do it.
  • How do you decide the accumulator width a given modulus needs?
    Work from the largest intermediate. In a multiply-based pipeline both operands are at most `m - 1`, so the accumulator must hold `(m-1)^2`; a modulus just under `2^30` therefore needs about 60 bits. An addition-only pipeline only needs to hold `2m`, one bit more than the modulus. Size the accumulator from that product, then pick the modulus to fit.
  • The loop reduces every step and still returns a negative number. What happened?
    Something subtracted. Inclusion-exclusion style counters subtract residues, and when the left operand is smaller the difference is negative; a further reduction leaves it negative, since its magnitude is already below the modulus. Add the modulus back after every subtraction, before the value feeds the next multiply.
  • What if the caller needs the exact count, not a residue?
    Then modular arithmetic cannot help — a residue does not determine the original number. You need arbitrary-precision integers and must accept the superlinear multiply cost and the memory for hundreds of digits. The mod convention exists precisely so that answers fit a machine word and can be compared cheaply; it is a change to the problem statement, not a compression of the answer.

Keeping change in a coin jar instead of a truck: emptying the jar every time it fills means you never need a bigger container, no matter how long you collect.

saying these in an interview costs you the question

  • Thinks reducing early changes the answer
  • Reduces only at the end and calls the overflow a language bug
  • Assumes any product fits a 64-bit accumulator
  • Believes fixed-width overflow raises an error
  • Reduces the accumulator but not the incoming factor
  • Thinks a value wrapped mod 2^64 can be rescued afterwards

context