L20–L25 Trie, Greedy, DP, Backtracking, Bits
L20. Trie
- Video: L20. Trie (43:36)
Definition: a prefix tree. Each edge is one character. Words that share a prefix share the same path. A node’s end flag marks a complete word.
Real-world use:
- Phone keyboard autocomplete and search-as-you-type.
- Spell check: walk the letters the user typed; missing child means “unknown word.”
- IP or URL prefix routing.
- Dictionary of product SKUs that share a brand prefix (
app,apple,apply).
1. Insert
Walk one node per character. Create a child if missing. Mark end on the last character.
Real-world use: add apple to a dictionary so later app can reuse the same path.
Insert "apple":
root
└─ a
└─ p
└─ p
└─ l
└─ e (end=true)
Then insert "app": reuse a-p-p and mark that p as end.
Complexity: time O(L) for a word of length L. Extra space O(L) new nodes in the worst case.
2. Implement Trie
Search needs end == true. StartsWith only needs the walk to succeed. "apple" is a word; "app" is only a prefix until you insert it.
Real-world use: search-as-you-type: StartsWith("app") is true while Search("app") is still false.
type Trie struct {
children [26]*Trie
end bool
}
func (t *Trie) Insert(word string) {
node := t
for i := 0; i < len(word); i++ {
idx := word[i] - 'a'
if node.children[idx] == nil {
node.children[idx] = &Trie{}
}
node = node.children[idx]
}
node.end = true
}
func (t *Trie) Search(word string) bool {
node := t.walk(word)
return node != nil && node.end
}
func (t *Trie) StartsWith(prefix string) bool {
return t.walk(prefix) != nil
}
Complexity: Insert / Search / StartsWith time O(L). Extra space for the trie is O(total characters inserted) after prefix sharing. Each call uses O(1) extra besides the tree.
L21. Greedy
- Video: L21. Greedy (41:00)
Definition: take the locally best choice and never undo it. It is correct only when that local choice is also globally optimal — farthest reach, earliest finish, unique start that can complete a cycle.
Real-world use:
- Can a frog (or a delivery hop) reach the last stone if each stone says how far you may jump?
- Fewest refuels or fewest hops to cross a highway.
- Unique gas-station start that lets a truck complete a circular route.
- Interval scheduling: always take the meeting that finishes first.
1. Jump Game I
Can you reach the last index? Track the farthest index reachable so far. If i walks past that, you are stuck.
Real-world use: can a frog (or a delivery hop) reach the last stone if each stone says how far you may jump?
[2,3,1,1,4] → true. [3,2,1,0,4] → false (stuck at the 0).
func CanJump(nums []int) bool {
reach := 0
for i, jump := range nums {
if i > reach {
return false
}
if i+jump > reach {
reach = i + jump
}
}
return true
}
Complexity: time O(n), extra space O(1).
2. Jump Game II
Minimum jumps, assuming a path exists. The current jump covers [prevEnd+1, end]. When i hits end, take one more jump and extend end to farthest.
Real-world use: fewest refuels or fewest hops to cross a highway when each stop has a max reach.
[2,3,1,1,4] → 2 (0→1→4).
func Jump(nums []int) int {
jumps, end, farthest := 0, 0, 0
for i := 0; i < len(nums)-1; i++ {
if i+nums[i] > farthest {
farthest = i + nums[i]
}
if i == end {
jumps++
end = farthest
}
}
return jumps
}
Complexity: time O(n), extra space O(1).
3. Gas station
Unique circular tour. If total gas < total cost, impossible. Otherwise the start is the station after the last time the tank went negative.
Real-world use: the unique depot a truck must start from to complete a circular route without running dry.
gas=[1,2,3,4,5], cost=[3,4,5,1,2] → start index 3.
func CanCompleteCircuit(gas, cost []int) int {
total, tank, start := 0, 0, 0
for i := range gas {
diff := gas[i] - cost[i]
total += diff
tank += diff
if tank < 0 {
start = i + 1
tank = 0
}
}
if total < 0 {
return -1
}
return start
}
Complexity: time O(n), extra space O(1).
L22. Dynamic Programming — House Robber
- Video: L22. DP / House Robber (45:38)
Definition: solve a big problem from overlapping smaller ones. Store each sub-answer (memo or table) so you never recompute it. House robber: you may not take two adjacent houses, so dp[i] = max(skip, take + dp[i-2]).
Real-world use:
- A thief (or a planner) picking non-adjacent houses / ad slots / work shifts.
- Any “choose or skip this item, but yesterday’s choice blocks today.”
- Same three styles show up in knapsack, climb-stairs, and decode-ways.
You cannot rob two adjacent houses:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
[1,2,3,1] → 4 (1+3). [2,7,9,3,1] → 12 (2+9+1).
Type 1 — top-down (recursion + memo)
Real-world use: write the recurrence the way you think (“from house i, take or skip”), then cache answers so a planner does not re-ask the same house.
func RobTopDown(nums []int) int {
memo := make([]int, len(nums))
for i := range memo {
memo[i] = -1
}
var robFrom func(int) int
robFrom = func(i int) int {
if i >= len(nums) {
return 0
}
if memo[i] >= 0 {
return memo[i]
}
take := nums[i] + robFrom(i+2)
skip := robFrom(i + 1)
if take < skip {
take = skip
}
memo[i] = take
return take
}
return robFrom(0)
}
Complexity: time O(n), extra space O(n) for the memo plus O(n) recursion.
Type 2 — bottom-up table
Real-world use: fill a spreadsheet of “best so far at house i” from left to right when you want no recursion on a long street.
func RobBottomUp(nums []int) int {
n := len(nums)
if n == 0 {
return 0
}
if n == 1 {
return nums[0]
}
dp := make([]int, n)
dp[0] = nums[0]
dp[1] = nums[0]
if nums[1] > dp[1] {
dp[1] = nums[1]
}
for i := 2; i < n; i++ {
dp[i] = dp[i-1]
if dp[i-2]+nums[i] > dp[i] {
dp[i] = dp[i-2] + nums[i]
}
}
return dp[n-1]
}
Complexity: time O(n), extra space O(n).
Type 3 — space optimization
Only dp[i-1] and dp[i-2] are needed.
Real-world use: a streaming planner that only remembers yesterday and the day before — no full table in memory.
func Rob(nums []int) int {
prev2, prev1 := 0, 0
for _, money := range nums {
cur := prev1
if prev2+money > prev1 {
cur = prev2 + money
}
prev2, prev1 = prev1, cur
}
return prev1
}
Complexity: time O(n), extra space O(1).
All three are O(n) time. Extra space is O(n), O(n), then O(1).
L23. Minimum Path Sum
- Video: L23. Minimum Path Sum (33:14)
Definition: a grid DP. From each cell you may move only right or down. The cheapest way to a cell is cell + min(from above, from left).
Real-world use:
- Cheapest walk through a city grid of tolls.
- Minimum time for a robot that can only move east or south on a warehouse floor.
- Least-cost path in a spreadsheet of cell costs.
dp[r][c] = grid[r][c] + min(from_above, from_left)
1 3 1
1 5 1 path 1→3→1→1→1 = 7
4 2 1
Approach 1 — recursion with memo
Real-world use: ask “cheapest from this cell to the exit?” and cache the answer, like a GPS that memos intersections.
func MinPathSumMemo(grid [][]int) int {
rows, cols := len(grid), len(grid[0])
memo := make([][]int, rows)
for i := range memo {
memo[i] = make([]int, cols)
for j := range memo[i] {
memo[i][j] = -1
}
}
var dfs func(r, c int) int
dfs = func(r, c int) int {
if r == rows-1 && c == cols-1 {
return grid[r][c]
}
if memo[r][c] >= 0 {
return memo[r][c]
}
best := int(^uint(0) >> 1)
if r+1 < rows {
best = dfs(r+1, c)
}
if c+1 < cols {
if right := dfs(r, c+1); right < best {
best = right
}
}
memo[r][c] = grid[r][c] + best
return memo[r][c]
}
return dfs(0, 0)
}
Complexity: time O(rows × cols), extra space O(rows × cols) for the memo plus O(rows + cols) recursion.
Approach 2 — full table
Fill first row and first column as prefix sums, then the rest.
Real-world use: a warehouse cost grid: each cell stores the cheapest way to arrive there from the dock.
func MinPathSumTable(grid [][]int) int {
rows, cols := len(grid), len(grid[0])
dp := make([][]int, rows)
for i := range dp {
dp[i] = append([]int(nil), grid[i]...)
}
for c := 1; c < cols; c++ {
dp[0][c] += dp[0][c-1]
}
for r := 1; r < rows; r++ {
dp[r][0] += dp[r-1][0]
for c := 1; c < cols; c++ {
if dp[r-1][c] < dp[r][c-1] {
dp[r][c] += dp[r-1][c]
} else {
dp[r][c] += dp[r][c-1]
}
}
}
return dp[rows-1][cols-1]
}
Complexity: time O(rows × cols), extra space O(rows × cols).
Approach 3 — one row
dp[c] is the previous row until you overwrite it. Compare dp[c] (above) with dp[c-1] (left).
Real-world use: same cheapest-path grid when you only keep the previous street of costs in memory.
func MinPathSum(grid [][]int) int { /* see solutions.go */ }
Complexity: time O(rows × cols), extra space O(cols).
L24. Backtracking
- Video: L24. Backtracking (36:26)
Definition: build a partial answer, try a choice, recurse, then undo the choice if it fails (path = path[:len-1], unmark visited). Use it when you must enumerate or search a decision tree.
Real-world use:
- Every possible pizza topping combo (subsets).
- A rat (or a warehouse robot) finding every open path through a maze.
- Seat people, place queens, or fill a Sudoku: try, check, undo.
- Generate all passwords that match a pattern.
1. Subset generation
For each number: skip it, or take it. 2^n subsets.
Real-world use: every pizza topping combo, or every subset of optional features a customer can buy.
[1,2,3] → [], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3] (order depends on skip-first vs take-first).
func SubsetsBacktrack(nums []int) [][]int {
var out [][]int
var path []int
var dfs func(int)
dfs = func(i int) {
if i == len(nums) {
out = append(out, append([]int(nil), path...))
return
}
dfs(i + 1) // skip
path = append(path, nums[i])
dfs(i + 1) // take
path = path[:len(path)-1]
}
dfs(0)
return out
}
The iterative version (Subsets in solutions.go) grows the answer list by cloning every existing subset and appending the new number.
Complexity: time O(n · 2ⁿ), extra space O(n) for the path plus O(n · 2ⁿ) for the output.
2. Rat in a Maze
n x n grid. 1 is open, 0 is blocked. Start (0,0), end (n-1,n-1). Moves in order D L R U. Mark visited, recurse, unmark.
Real-world use: a warehouse robot listing every open path from the dock to the packing bay.
1 0 0 0
1 1 0 1
1 1 0 0
0 1 1 1
Paths: DDRDRR, DRDDRR.
func RatInMaze(grid [][]int) []string {
n := len(grid)
visited := make([][]bool, n)
for i := range visited {
visited[i] = make([]bool, n)
}
var paths []string
var path []byte
dirs := []struct {
dr, dc int
ch byte
}{{1, 0, 'D'}, {0, -1, 'L'}, {0, 1, 'R'}, {-1, 0, 'U'}}
var dfs func(r, c int)
dfs = func(r, c int) {
if r == n-1 && c == n-1 {
paths = append(paths, string(path))
return
}
for _, d := range dirs {
nr, nc := r+d.dr, c+d.dc
if nr < 0 || nc < 0 || nr >= n || nc >= n || visited[nr][nc] || grid[nr][nc] == 0 {
continue
}
visited[nr][nc] = true
path = append(path, d.ch)
dfs(nr, nc)
path = path[:len(path)-1]
visited[nr][nc] = false
}
}
if n == 0 || grid[0][0] == 0 {
return nil
}
visited[0][0] = true
dfs(0, 0)
return paths
}
Complexity: time O(4^(n²)) in the worst open grid, extra space O(n²) for visited plus O(n²) path depth.
L25. Bitwise Operations
- Video: L25. Bitwise Operations (33:26)
Definition: work on the binary digits of an integer. AND / OR / XOR / NOT and shifts replace loops over flags. Index i is 0-based from the least significant bit.
Real-world use:
- Feature flags packed into one integer (
bit 0 = dark mode). - Power-of-two checks for buffer sizes and memory pages.
- XOR finds the one package id that did not come in a pair.
- Count how many permission bits are on, or turn one bit on/off.
| Operator | Go | Meaning | |
|---|---|---|---|
| AND | a & b | bits set in both | |
| OR | `a \ | b` | bits set in either |
| XOR | a ^ b | bits that differ | |
| NOT | ^a | flip all bits | |
| left shift | a << k | multiply by 2^k | |
| right shift | a >> k | divide by 2^k (sign-extends signed ints) |
n & (n-1) clears the lowest set bit. n & 1 is the lowest bit.
1. Power of two
Positive and exactly one set bit. 16 yes, 18 no, 0 no.
Real-world use: check that a buffer size or memory page is a power of two before rounding.
func IsPowerOfTwo(n int) bool { return n > 0 && n&(n-1) == 0 }
Complexity: time O(1), extra space O(1).
2. Count set bits
Real-world use: how many permission flags or feature bits are on in one integer.
func HammingWeight(n int) int {
count := 0
for n != 0 {
n &= n - 1
count++
}
return count
}
11 is 1011 → 3 bits.
Complexity: time O(number of set bits), extra space O(1).
3. Only non-repeating element
XOR cancels pairs. [4,1,2,1,2] → 4.
Real-world use: the one package id that did not come in a pair, or the unique unpaired locker key.
func SingleNumber(nums []int) int {
x := 0
for _, n := range nums {
x ^= n
}
return x
}
Complexity: time O(n), extra space O(1).
4. Get the i-th bit
10 is 1010. Bit 1 is 1.
Real-world use: read one feature flag: “is dark mode (bit 1) on?”
func GetIthBit(n, i int) int { return (n >> i) & 1 }
Complexity: time O(1), extra space O(1).
5. Set the i-th bit
10 | (1<<0) → 11.
Real-world use: turn on one permission without touching the others.
func SetIthBit(n, i int) int { return n | (1 << i) }
Complexity: time O(1), extra space O(1).
Clear is the natural pair: n &^ (1 << i). Same complexity.