DEV Community

Timevolt
Timevolt

Posted on

The Matrix: Python tricks for competitive programming you didn't know

The Quest Begins (The "Why")

I still remember my first Codeforces round like it was yesterday. I was staring at a problem that asked for the maximum prefix sum after each update, and my solution was a naïve O(n²) loop that timed out faster than a boss fight in Dark Souls on level 1. I kept thinking, “There has to be a smarter way,” but every time I opened the Python docs I felt like I was lost in a maze. After a few frustrating hours (and a lot of caffeine), I stumbled upon a few language features that felt like secret cheat codes. Once I wrapped my head around them, my runtime dropped from seconds to milliseconds, and I finally felt like Neo dodging bullets—except the bullets were time limit exceeded errors.

If you’ve ever felt stuck writing the same old loops over and over, or wondered why your “clean” Python solution still lags behind the C++ crowd, keep reading. The treasures I’m about to share aren’t just obscure syntax; they’re practical tools that can turn a sluggish script into a speed demon.

The Revelation (The Insight)

1. itertools.accumulate – the running‑total spell

Most of us reach for a simple for loop when we need prefix sums, running maxima, or any cumulative operation. The gotcha? Writing that loop is easy, but it’s also easy to slip into O(n²) when you nest it inside another loop (think: updating a prefix after each point update).

itertools.accumulate does the heavy lifting in C, so it’s blazingly fast, and it lets you plug in any binary function—sum, max, min, even a custom lambda. The surprise? If you forget to pass a function, it defaults to addition, which is perfect for prefix sums, but if you do pass a function you suddenly gain the ability to compute running maxima or even running products without a single explicit loop.

2. bisect with a secret key list – searching without pain

Binary search is a competitive programmer’s best friend, yet many shy away from bisect because they think it only works on plain lists of numbers. The reality? You can search on any sorted sequence as long as you provide a list of keys that matches the order. The trap? Trying to bisect directly on a list of custom objects without defining __lt__ raises a TypeError and leaves you scratching your head.

The trick is simple: keep a parallel list of the attribute you want to search on (or a list of tuples where the first element is the key). bisect_left(keys, target) gives you the insertion point, and you can use that index to fetch the original object. No need to overload comparison operators—just a tiny bit of extra bookkeeping.

3. Counter arithmetic – the battle‑ready tally

Counting frequencies is a staple, and collections.Counter makes it trivial. What most people miss is that Counter objects support arithmetic: addition, subtraction, intersection, and union. The gotcha? The subtraction operator (c1 - c2) drops any zero or negative results, which can be puzzling if you expect a signed difference.

If you need to keep those negatives (for example, when computing the difference of two frequency vectors), you should use the .subtract() method, which updates the counter in place and preserves negative counts. Knowing when to use - vs .subtract() can save you from subtle bugs that only surface on edge‑case test data.

Wielding the Power (Code & Examples)

Example 1: Prefix maximum with accumulate

The struggle – naive loop:

def prefix_max_bruteforce(arr):
    res = []
    cur = -10**9
    for v in arr:
        if v > cur:
            cur = v
        res.append(cur)
    return res
Enter fullscreen mode Exit fullscreen mode

It works, but imagine calling this inside another loop that updates arr thousands of times—suddenly you’re O(n²).

The victory – using accumulate:

from itertools import accumulate

def prefix_max_fast(arr):
    # accumulate with max as the binary function
    return list(accumulate(arr, max))
Enter fullscreen mode Exit fullscreen mode

That’s it. One line, C‑speed, and you can swap max for min, operator.add, or even a lambda like lambda x, y: x if x % 2 == 0 else y to get running “first even”.

Example 2: Binary search on objects with a key list

Suppose we have a list of points sorted by their x‑coordinate and we need to find the first point with x ≥ target.

The trap – trying to bisect directly:

from bisect import bisect_left

points = [(1, 5), (3, 2), (4, 8), (7, 1)]
# bisect_left(points, 4)  # TypeError: '<' not supported between instances of 'tuple' and 'int'
Enter fullscreen mode Exit fullscreen mode

The victory – separate key list:

from bisect import bisect_left

points = [(1, 5), (3, 2), (4, 8), (7, 1)]
xs = [p[0] for p in points]          # the key list
idx = bisect_left(xs, 4)              # -> 2
point = points[idx]                   # (4, 8)
Enter fullscreen mode Exit fullscreen mode

Now you can reuse xs for many queries without rebuilding it each time.

Example 3: Counter subtraction vs .subtract()

The struggle – assuming - keeps negatives:

from collections import Counter

a = Counter({'apple': 5, 'banana': 2})
b = Counter({'apple': 3, 'banana': 4})

c = a - b
print(c)   # Counter({'apple': 2})   <-- banana vanished!
Enter fullscreen mode Exit fullscreen mode

The victory – using .subtract() when you need the full difference:

a = Counter({'apple': 5, 'banana': 2})
b = Counter({'apple': 3, 'banana': 4})

a.subtract(b)          # in‑place update
print(a)               # Counter({'apple': 2, 'banana': -2})
Enter fullscreen mode Exit fullscreen mode

Now the negative count is preserved, which is crucial when you later check for deficits or need to feed the result into another algorithm.

Why This New Power Matters

Mastering these tiny gems does more than shave a few milliseconds off your runtime—it changes the way you think about problems.

  • accumulate teaches you to look for opportunities to replace explicit loops with higher‑order abstractions. Once you start seeing patterns like “running X”, you’ll reach for it instinctively, making your code cleaner and less error‑prone.
  • Keyed bisect shows that you don’t need to shoehorn everything into a plain list of ints. By keeping a parallel key array, you gain the speed of binary search on complex data structures while staying within Python’s rich object model. It’s a pattern that pops up in interval scheduling, sweep line algorithms, and even in geometry tricks.
  • Counter arithmetic reminds you that the standard library already implements many set‑like operations you might otherwise rewrite from scratch. Knowing the difference between - and .subtract() prevents those sneaky bugs that only appear when the input hits a corner case—exactly the kind of thing that separates a passing solution from a hacked‑together one.

When you internalize these tools, you stop writing “just‑good‑enough” code and start crafting solutions that feel elegant—the kind that make you smile when you see them run in under the limit.

Your Turn – A Mini Quest

Here’s a challenge to flex your newfound spells:

Given an array a of length n, answer q queries of the form (l, r, k): after setting a[l] = k, report the maximum prefix sum of the array.

Constraints: n, q ≤ 2·10⁵.

Try solving it with a Fenwick tree or segment tree and use itertools.accumulate to recompute the prefix maximum after each update in O(log n) time (you can keep a auxiliary array of differences and rebuild the prefix in O(n) only when needed, then use accumulate for the fast scan).

If you crack it, drop your solution in the comments—let’s see who can make the Neo‑est code of them all!

Happy hacking, and may your bugs be few and your speed be legendary. 🚀

Top comments (0)