The Boundary Condition I Have to Check Every Single Time
My mistake log's top category isn't some subtle algorithmic error. It's a loop bound that's one index short — twice in one week, in two different shapes.
I keep a running log of every mistake I make on these practice problems, tagged by category and severity, mostly so I can stop lying to myself about which mistakes are random and which ones are patterns. The highest-severity recurring category, by a wide margin, is off-by-one errors. Not "I misunderstood the algorithm." Just: the loop ran one iteration too many, or one too few, because I trusted a boundary condition instead of actually checking it.
The setup
Two-pointer problems keep coming down to one line for me: what happens right at the edge of the loop. while (l < r) with nothing after it, versus while (l < r) followed by a check for whoever's left standing once l and r meet. for i in range(len(nums)) versus range(1, len(nums)) — where a range starts, not just where it stops. When I'm skimming my own code these look interchangeable, and I do skim them — by the time I've written the pointer-update logic in the middle of the loop, my brain's already decided the boundary is "obviously" whatever felt natural, and moved on without actually checking it. That's what burned me twice in the same week.
What went wrong
First was "Boats to Save People." Two pointers closing in from either end of a sorted array, pairing the lightest and heaviest remaining people into boats. I wrote the loop as while (l < r), closing one pair and incrementing the boat counter on every pass — and stopped there. Looked complete — once l and r cross, there's nobody left, right? Except when the array has an odd length, l and r land on the same index with exactly one person still unprocessed, who still needs exactly one boat, and my loop had already stopped by then, since l < r is false the moment they're equal. I was under-counting the boat for the last remaining person. Traced it by hand against a 3-person input and watched my answer come out one boat too low, every single time, for any odd-length input. Even-length inputs never leave anyone stranded at l == r, so they passed silently, and I'd nearly convinced myself the function was done before I happened to try an odd one.
Second was "Two Sum," a problem I'd have told you I was past making mistakes on, which is probably exactly why I made one. I wrote the brute-force check as for i in range(1, len(nums)). Felt right because I was thinking about the inner loop needing i + 1 as a valid starting point, and some part of my brain smeared that same "plus one" onto where the outer loop started too, for a symmetry that doesn't actually exist. The result: index 0 was never considered as nums[i] — and since the inner loop only ever looks at indices greater than whatever i currently is, index 0 could never show up as nums[j] either. It just fell out of the search space entirely. If the only valid pair involved index 0, my function returned nothing, silently, no error, just a wrong answer that looked exactly like "no solution exists."
The one that hasn't happened yet
There's another category sitting right next to off-by-one in the same log: not handling edge cases — empty input, a single-element array, an array where every value is identical. Right now that entry just says "none logged yet." Nothing's actually caught me on it. I'm putting it in this post anyway because it feels dishonest to only write about the stuff that's already blown up, when I'm already fairly sure this is next. Maybe I'm wrong and it never happens. I'd rather look a little silly for that than pretend I didn't see it coming.
The fix
The actual fix was tiny in both cases, but not the same tiny fix. For Two Sum it was range(len(nums)) instead of range(1, len(nums)). For Boats, l < r was already the right condition — the comparison operator was never the problem. What was missing was the line after the loop: if l == r: boats += 1, to account for the one person the loop itself never touches once the pointers meet. A one-character diff, or one missing line right after a loop, doesn't jump out when I'm scanning my own code, because my eyes read what I meant to write, not what's actually on the screen. What I'm trying now is tracing two iterations by hand before I call a loop done — the very first one, with whatever the starting indices actually are, and the very last one, with the indices at whatever value makes the loop condition go false — and then separately asking whether anything real is still sitting unprocessed at that final value. For Two Sum that means actually asking out loud, "does i ever equal 0?" instead of glancing at the range() call and assuming it's fine. For Boats it means asking what happens to whoever's standing at l == r the instant the loop condition goes false, since "the loop ended" and "everyone's been given a boat" turned out not to be the same claim. For the edge-case stuff, same idea before I write a single line: what does this do on empty input, one element, all-identical values. I've said "I'm going to start doing X" in this log before and not kept it up, so I'm not going to pretend this one's guaranteed to stick either.
Both of these come down to the same thing, honestly — not thinking through the edges of a problem, whether the edge is a loop index or a weird input. The middle of the logic, the actual algorithm, is usually the part I get right, probably because it's the part I actually pay attention to. The boundary is the part I assume I already know, so I don't look at it closely. Going forward — supposedly — for any loop with two indices or a moving boundary, I'm going to write out what happens when the indices are equal, or adjacent, or sitting at the start or end of the array, before I write the loop body itself. I say "supposedly" because I've made myself promises like this before. The boundary keeps not feeling like it needs checking, which is apparently the whole reason it keeps getting me.
Notes from readers
Comments — via GitHub