Policemen Catch Thieves is a clean greedy problem that doubles as a two-pointer warm-up. You scan a row of police and thieves once and match them up locally — the trick is proving that a greedy nearest-match is always optimal.
Problem. You are given a row (a string or array) where each cell is 'P' (a policeman) or 'T' (a
thief), and a number k. A policeman can catch a thief only if they are at most k cells apart, and
each policeman catches at most one thief. Return the maximum number of thieves that can be caught.
Example: row = [P, T, P, T, T, P], k = 1 → answer 3 (police at 0/2/5 catch thieves at 1/3/4).
The slow way first
The brute-force instinct is to try every assignment of police to nearby thieves and keep the best — a combinatorial search that blows up fast. Even a more careful matching (bipartite graph + max-flow) is overkill here.
The question to ask: if I walk the row left to right, who should catch whom? It turns out the leftmost free policeman should always grab the nearest unclaimed thief within reach. Once you believe that, a single pass with two pointers solves it.
The idea: greedy nearest match with two pointers
Keep two pointers walking forward: p finds the next policeman, t finds the next thief. Compare their positions. If they are within k, match them and advance both. If not, advance whichever pointer is behind — that element can never improve by waiting, so we move past it.
Why greedy is safe: pairing the earliest policeman with the closest reachable thief never blocks a better future match. Any thief that policeman could skip is still available to a later, equally-close policeman.
Walk through it
Step through the animation. Pointer p (green) lands on each policeman; pointer t (blue) lands on each thief. When |p - t| <= k, both cells turn green and caught ticks up. With k = 1 every police/thief pair here is exactly one cell apart, so all three matches land.
Pseudocode
caught = 0
p = t = 0
while both pointers are inside the row:
move p forward until it sits on a 'P'
move t forward until it sits on a 'T'
if both are still in range:
if |p - t| <= k:
caught += 1
advance both p and t
else:
advance whichever pointer is behind
return caughtThe Python solution
def catch_thieves(row, k):
caught = 0
p = t = 0
n = len(row)
while p < n and t < n:
while p < n and row[p] != 'P': p += 1
while t < n and row[t] != 'T': t += 1
if p < n and t < n:
if abs(p - t) <= k:
caught += 1
p += 1; t += 1
elif p < t: p += 1
else: t += 1
return caughtpandtare independent scan pointers; they never move backward.- The two inner
whileloops fast-forward each pointer to the next relevant character. abs(p - t) <= kis the reach test — the heart of the greedy match.- On a match we advance both pointers so neither element is reused.
- On a miss we advance only the trailing pointer, since the leading one might still pair with someone closer ahead.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute-force matching | O(n!) / O(n^3) (slow) | search or max-flow, overkill |
| Greedy two pointers | O(n) (moderate) | each pointer crosses the row once |
O(1) (fast)Both pointers only ever move forward across n cells, so the total work is linear and we use just a couple of integer variables.
When this pattern shows up
When a problem asks to maximize matches between two kinds of items laid out in order, and a match has a local distance/capacity rule, try a forward two-pointer greedy before reaching for flow or DP. "Boats to save people," "assign cookies," and interval-pairing problems share this shape.
The reach test is inclusive — a thief exactly k cells away is still catchable, so use <=, not <.
And remember to advance the trailing pointer on a miss; advancing the wrong one can skip a valid match.
Practice
For row = [P, T, P, T, T, P] with k = 1, after the police at index 0 catches the thief at index 1, where do p and t point next?
1. Why is advancing the trailing pointer the right move on a miss?
2. A thief sits exactly k cells from a policeman. Is it catchable?
3. What is the time complexity of the two-pointer solution?
4. Why does the greedy nearest-match never hurt a future pairing?