learn
Week 36 and beyond · About 20 hours, then ongoing

Optional depth

Specialised structures and ideas worth knowing about. Treat this as a menu, not a checklist.

Only come here once the previous stage is genuinely solid. Nothing below is needed for most engineering work, and chasing all of it is a common way to feel busy without getting better.

Two things here have real everyday value: knowing that some problems are genuinely unsolvable at scale, and knowing that randomised approaches exist. The rest is depth for its own sake, which is fine as long as you know that is what it is.

NeetCode 150 · complete category

Bit Manipulation practice lab

Some problems get much smaller when you stop treating a number as a quantity and start treating it as a row of switches.

2 problems
Pattern first

A number is also a bit pattern

Naive

Testing bits by repeated division and remainder works but hides what is actually happening.

Why this works

AND, OR and XOR act on every bit at once, so an operation that would be a loop over 32 positions becomes a single instruction.

Problem 1 of 2

Number of 1 Bits

Solve on NeetCode ↗

How many 1 bits are in 11, whose binary form is 1011?

Picture itSwitching off the rightmost lit lamp over and over, and counting how many times you had to do it.

Conceptual blueprint

Repeatedly clear the lowest set bit with n & (n - 1), counting each clear.

Animated dry-runFrame 1
1 / 5
01
10
21
31

Cells are the bits of 11, most significant on the left: 1, 0, 1, 1.

count0
Lookup structure
n → 1011 (11)
1def hamming_weight(n):
2 count = 0
3 while n:
4 n &= n - 1
5 count += 1
6 return count
Check yourself before reading on
Cost it first

What does this trick save over testing all 32 positions?

Find the bug

n + 1 instead of n - 1. What happens for n = 11?

def hamming_weight(n):
    count = 0
    while n:
        n &= n + 1
        count += 1
    return count
Predict the next step

n is 1010 after the first clear. What is n after the next step?

Fill in the blanks

Recall each hidden line before revealing it. 2 left.

1def hamming_weight(n):
2 count = 0
3
4
5 count += 1
6 return count
Why the naive approach falls short

Check all 32 bit positions even when only three are set.

Why the optimized approach works

Subtracting one flips the lowest set bit to 0 and everything below it to 1, so ANDing removes exactly that bit and nothing else.

Time

O(number of set bits) — not O(width of the word).

Space

O(1).

Segment trees and Fenwick trees

A segment tree stores answers for nested ranges so a changed value only forces a few range answers to be rebuilt.

Reach for it whenUse a segment or Fenwick tree instead of prefix sums when values change and range queries stay frequent; use prefix sums when data is static and queries only need an invertible total.

A answers range questions instantly but has to be rebuilt whenever a value changes. These structures accept slightly slower queries in exchange for cheap updates, which is the right trade when data is live.

A Fenwick tree is remarkably compact — about ten lines — but the index arithmetic is genuinely unintuitive. A segment tree is longer and much easier to reason about.

Syntax you can use
size = 1 while size < n: size *= 2
Rounds the leaf capacity up to a power of two.
tree = [0] * (2 * size)
Allocates an iterative segment tree with leaves after index size.
tree[size + index] = value
Stores one input value in its leaf position.
tree[index] = tree[2 * index] + tree[2 * index + 1]
Combines two child ranges into their parent range.
0..70..34..70..12..3
Notice that the highlighted interval is answered by combining stored blocks instead of reading all eight leaves.
Several ways to see it
Its shapeStored intervals tile a query without touching every item

A segment tree node summarizes one interval and its children split that interval into halves. A query range is covered by a small set of disjoint stored intervals rather than by each underlying position.

Against the alternativeFenwick trees trade generality for a smaller machine

A Fenwick tree stores overlapping prefix blocks in one compact and is excellent for sums and invertible prefix operations. A segment tree uses more memory and code but supports a wider choice of associative aggregates and lazy range updates.

What the machine doesThe lowest set bit encodes each Fenwick block

At index i, `i & -i` gives the size of the suffix block summarized there. Adding that value climbs to update larger containing blocks; subtracting it walks backward through the pieces of a prefix.

In the wild
  • AtCoder Library segtree — provides point updates and range products over a user-defined associative operation.
  • AtCoder Library fenwick_tree — supports point additions and prefix or range sums in logarithmic time.
Commonly gets misread as
  • "A segment tree stores every possible range." Correction: it stores a reusable hierarchy of selected ranges.
  • "Fenwick and segment trees handle identical operations." Correction: Fenwick trees are smaller and simpler but support a narrower algebra.

What it costsBoth give range queries and O(log n) updates.

String search algorithms

String-search remember enough of a failed match to avoid checking the same characters again.

Reach for it whenUse KMP or a linear string matcher instead of restarting at every position when worst-case guarantees matter; use the language's built-in search for ordinary application code.

The obvious approach compares the pattern against every position, which repeats work whenever a partial match fails. KMP precomputes a table of where to resume after a failure, so it never re-examines the same character.

Rabin-Karp takes a different route: it hashes the pattern and compares hashes as it slides along, which makes searching for many patterns at once cheap.

Syntax you can use
pattern in text
Checks whether a substring occurs anywhere in text.
text.find(pattern)
Returns the first match position, or -1 when there is none.
from collections import Counter
Imports a frequency table for character-count comparisons.
Counter(word) == Counter(candidate)
Tests whether two words are anagrams.
prefix = [0] * len(pattern)
Starts the prefix table used by Knuth-Morris-Pratt matching.
ABABAC
Notice that the repeated AB prefix tells KMP where matching can resume; it does not slide back to the next text character blindly.
Several ways to see it
Why it existsMismatches were throwing away knowledge already earned

Naive search restarts the pattern after a mismatch and may compare the same text characters repeatedly. KMP records how much of the matched prefix is also a suffix, so it can shift without reconsidering proven characters.

Against the alternativeKMP guarantees; rolling hash filters

KMP uses exact prefix structure and guarantees linear time. Rabin-Karp compares rolling fingerprints cheaply and verifies matches, making it convenient for many patterns or windows but vulnerable to extra collision checks.

How it breaksUnicode normalization can hide an exact match

Two strings can display identically while using different code-point sequences, such as a composed accent versus letter plus combining mark. Normalize both sides when the application means human-equivalent text rather than byte-for-byte identity.

In the wild
  • Linux kernel textsearch — offers a KMP configuration for linear-time pattern matching over text buffers.
  • CPython str.find — uses its fastsearch implementation, which can switch to Two-Way matching for suitable substring searches.
Commonly gets misread as
  • "KMP skips possible matches." Correction: its prefix table skips only comparisons already implied by the matched prefix.
  • "Rolling hashes prove two strings match." Correction: hash collisions require verification when correctness must be exact.

What it costsO(text length + pattern length), rather than the naive O(text × pattern).

Network flow

Network flow asks how much can move from a source to a destination when every connection has a limit.

Reach for it whenUse flow instead of a shortest-path algorithm for capacities, assignments, or disjoint routes; use matching directly when the problem is already a simple bipartite pairing.

Picture water pipes of differing widths. The question is how much can flow from source to destination at once, and the answer is limited by bottlenecks rather than by any single pipe.

The surprising part is how many problems that look nothing like plumbing turn out to be this in disguise — particularly matching problems, such as assigning people to jobs where only certain pairings are allowed.

3223SABT
Notice that flow can split across routes, but the shared edge into T caps their combined contribution.
Several ways to see it
Its shapeResidual edges record choices that can still be revised

After sending flow along an edge, the residual graph reduces forward capacity and adds reverse capacity. That reverse edge is permission to undo part of an earlier route when a later augmenting path uses the network better.

Why it existsCapacity turns connectivity into allocation

Ordinary paths ask whether one route exists; flow asks how much can travel through all routes without exceeding shared limits. The same abstraction models pipes, traffic, job assignment, and bipartite matching.

How it breaksA locally wide path can block a better total

Greedily choosing the path with the largest immediate capacity can consume an edge needed by several other routes. Residual correction is what lets a flow algorithm recover from an initially awkward augmentation.

In the wild
  • Google OR-Tools — provides SimpleMaxFlow and SimpleMinCostFlow solvers for capacitated networks.
  • NetworkX — implements maximum-flow and minimum-cost-flow algorithms over directed graphs with capacities.
Commonly gets misread as
  • "Maximum flow is the fastest route." Correction: it maximizes total transferable amount, not travel time.
  • "Flow used on one edge is gone forever." Correction: residual edges let an algorithm revise earlier choices.

Randomised and approximate algorithms

Randomised and approximate algorithms trade a controlled amount of certainty for less time or memory.

Reach for it whenUse them instead of exact algorithms when data is huge and a bounded error is acceptable; do not use them where one false answer causes an unsafe decision.

Quicksort picks its pivot at random specifically so that no particular input ordering can reliably make it slow. Randomness as a defence against worst cases is a genuinely useful idea.

Reservoir sampling picks a fair random sample from a stream of unknown length using constant memory — useful when you cannot hold everything.

Syntax you can use
import random
Imports Python's ordinary pseudo-random tools.
random.shuffle(items)
Randomly rearranges a list in place.
choice = random.choice(items)
Selects one item uniformly from a non-empty sequence.
sample = random.sample(items, k=3)
Selects distinct items without replacement.
from secrets import token_urlsafe token = token_urlsafe(32)
Creates a security-sensitive random token.random is predictable enough for simulations and games, not passwords, reset links or tokens.
Several ways to see it
Why it existsExact answers became more expensive than useful answers

At web scale, storing every event or exploring every candidate can cost more than the decision is worth. Sketches, sampling, and randomized choices spend a controlled error budget to cap time or memory.

Against the alternativeMonte Carlo may be wrong; Las Vegas may be slow

A Monte Carlo algorithm has bounded running time and a measurable chance of error. A Las Vegas algorithm always returns a correct result but randomizes how long it takes, as randomized quicksort does through pivot choice.

How it breaksBiased randomness produces confident nonsense

Sampling only convenient users or reusing predictable seeds does not create representative evidence. State the error model, choose randomness independently where required, and test the distribution rather than trusting the word 'random'.

In the wild
  • Redis HyperLogLog — estimates the number of distinct values using a small fixed amount of memory.
  • PostgreSQL TABLESAMPLE — uses SYSTEM or BERNOULLI sampling to read an approximate subset of table rows.
Commonly gets misread as
  • "Approximate means untrustworthy." Correction: the error can be measured and chosen to fit the decision.
  • "Randomised means different wrong output every run." Correction: many algorithms remain exact while randomness changes their path or expected cost.

P, NP and what cannot be done quickly

Complexity classes separate problems we know how to solve as they grow from problems where we can only check a proposed answer quickly.

Reach for it whenUse this lens instead of hunting endlessly for a fast exact algorithm when a problem is NP-hard; then choose constraints, approximation, heuristics, or smaller inputs.

P is the set of problems solvable in reasonable time. NP is the set where, if someone hands you an answer, you can check it quickly — even if finding it is hard. Whether those two sets are actually the same is the most famous open question in computer science.

NP-complete problems are the hardest in NP, and they include very ordinary-sounding things: finding the shortest route visiting a list of cities, or fitting items into a container with no wasted space.

workdata →nn log nn^2
Notice that polynomial curves are already costly, yet combinatorial search grows beyond this chart faster still; feasibility depends on the input range.
Several ways to see it
Why it existsEasy verification and easy discovery came apart

Many problems let you check a proposed solution quickly even though no quick method is known for finding one. Complexity classes formalize that gap so difficulty can be compared through reductions instead of anecdotes.

Against the alternativeNP-hard is not the same claim as NP-complete

An NP-complete decision problem is both in NP and at least as hard as every problem in NP. NP-hard also includes optimization problems and even problems outside NP, so the label makes no promise about quick verification.

How it breaksWorst-case hardness does not forbid useful software

Small inputs, special structure, approximation, parameterization, and good heuristics can make hard problems tractable in practice. The classification warns against expecting one exact polynomial algorithm for every instance; it does not say 'never try'.

In the wild
  • Google OR-Tools CP-SAT — uses propagation, learning, and search to solve NP-hard scheduling and assignment models without promising polynomial time.
  • PostgreSQL GEQO — uses a genetic search for large join-order problems rather than enumerating every possible plan.
Commonly gets misread as
  • "NP means not polynomial." Correction: it means a proposed solution can be verified in polynomial time.
  • "NP-hard means impossible in practice." Correction: bounded instances and good heuristics are solved every day.

Reading about this stage is not the same as finishing it. Type every one of these from a blank file rather than copying — the writing is the part that teaches.

Build a Bloom filter

Use several hash functions over a block of bits. Add a thousand items, then test how often it wrongly reports having seen something it has not. Change the size and watch the error rate move. The most practically useful thing in this stage.

Implement KMP

Build the failure table first and make sure you can explain what each entry means before writing the search itself. That table is the whole algorithm.

Exit checkYou can explain why some problems have no fast solution, and what to do instead. Anything else in this stage is optional depth.

If you cannot do this, repeat the stage rather than moving on. Nothing later gets easier by skipping it.

Worth knowing before you start

  • Do not chase every topic here. Segment trees and network flow are rarely needed outside competitive programming. Extra depth in dynamic programming and graphs pays far more.

This stage corresponds to DSA Phase 6 — Advanced Topics & Consolidation in the source roadmap.