From a1945b1cf691a74e0a87d54112425afd652ff416 Mon Sep 17 00:00:00 2001 From: Dileepadari Date: Sun, 16 Nov 2025 23:15:23 +0530 Subject: [PATCH 1/2] Added Leetcode problems for House Robber, Unique Paths, and Climbing Stairs --- algorithms/leetcode/L198_house-robber.typ | 164 +++++++++++++++++++ algorithms/leetcode/L62_unique-paths.typ | 169 ++++++++++++++++++++ algorithms/leetcode/L70_climbing-stairs.typ | 165 +++++++++++++++++++ 3 files changed, 498 insertions(+) create mode 100644 algorithms/leetcode/L198_house-robber.typ create mode 100644 algorithms/leetcode/L62_unique-paths.typ create mode 100644 algorithms/leetcode/L70_climbing-stairs.typ diff --git a/algorithms/leetcode/L198_house-robber.typ b/algorithms/leetcode/L198_house-robber.typ new file mode 100644 index 0000000..3ce8764 --- /dev/null +++ b/algorithms/leetcode/L198_house-robber.typ @@ -0,0 +1,164 @@ +#import "../../lib/style.typ": * +#import "../../lib/mapcode.typ": * + + +== Leetcode:198. #link("https://leetcode.com/problems/house-robber/")[House Robber] + +You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night. + +Given an integer array `nums` representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police. + +=== Recurrence Relation + +$ +"MaxRob"(i) = cases( + 0 & "if" i < 0, + "money"[0] & "if" i = 0, + max("MaxRob"(i-1), "money"[i] + "MaxRob"(i-2)) & "if" i > 0 +) +$ + +*Example:* Houses = $[2, 7, 9, 3, 1]$ → Maximum: $12$ (rob houses $0, 2, 4$: $2+9+1$) + +*Constraints:* +- $1 <= "nums"."length" <= 100$ +- $0 <= "nums"[i] <= 400$ + +=== Recursion Analysis + +This exhibits non-trivial recursion through: +1. *Two recursive branches*: Each decision depends on previous two states +2. *Optimization problem*: Finding maximum over multiple possibilities +3. *State dependency*: Current optimal depends on solutions to smaller subproblems +4. *Linear DP with choice*: Classic example of decision-making in dynamic programming + +=== Mapcode Formalization + +*Primitives:* $max$, $+$ + +$ +I &= "houses": NN^n \ +X &= [0..n-1] -> NN_bot \ +A &= NN \ +rho("houses") &= {i |-> bot | i in {0 dots n-1}} \ +F_("houses")(x)(i) &= cases( + "houses"[0] & "if" i = 0, + max(x[i-1], "houses"[i] + (x[i-2] "if" i >= 2 "else" 0)) & "if" x[i-1] != bot and (i < 2 or x[i-2] != bot), + bot & "otherwise" +) \ +pi(x) &= x[n-1] +$ + +=== Complexity Analysis + +- *Time Complexity*: $O(n)$ where $n$ is the number of houses +- *Space Complexity*: $O(n)$ for storing intermediate results + +#let inst_houses = (2, 7, 9, 3, 1); + +#figure( + caption: [House Robber computation for houses $#inst_houses$ showing maximum robbery amount.], + $#{ + let rho = (houses) => { + let n = houses.len() + let x = () + for i in range(0, n) { + x.push(none) + } + x + } + + let F_i = (houses) => (x) => ((i,)) => { + let n = houses.len() + + if i == 0 { + houses.at(0) + } else { + let skip = x.at(i - 1) + let take_prev2 = if i >= 2 { x.at(i - 2) } else { 0 } + + if skip == none or (i >= 2 and take_prev2 == none) { + none + } else { + let take = houses.at(i) + (if take_prev2 == none { 0 } else { take_prev2 }) + calc.max(skip, take) + } + } + } + + let F = (houses) => map_tensor(F_i(houses), dim: 1) + + let pi = (houses) => (x) => { + x.at(houses.len() - 1) + } + + let x_h(x, diff_mask: none) = { + set text(size: 9pt) + let rows = () + + let header = () + let values = () + + for i in range(0, x.len()) { + header.push(rect(fill: blue.transparentize(85%), inset: 4pt, stroke: 0.5pt)[*House #i*]) + + let val = x.at(i) + let display = if val == none { $bot$ } else { str(val) } + + let cell_fill = if diff_mask != none and diff_mask.at(i) { + yellow.transparentize(60%) + } else { + none + } + + values.push(rect(stroke: 0.5pt + gray, fill: cell_fill, inset: 4pt)[#display]) + } + + rows.push(grid(columns: x.len() * (auto,), rows: auto, align: center + horizon, column-gutter: 2pt, ..header)) + rows.push(grid(columns: x.len() * (auto,), rows: auto, align: center + horizon, column-gutter: 2pt, ..values)) + stack(dir: ttb, spacing: 2pt, ..rows) + } + + let I_h = (houses) => { + let house_display = houses.enumerate().map(((i, m)) => [House #i: $#m$]) + stack( + dir: ttb, + spacing: 2pt, + [*Houses with money:*], + ..house_display + ) + } + + let A_h = (a) => { + box(fill: green.transparentize(70%), inset: 8pt, radius: 4pt)[ + *Maximum Amount:* $#a$ + ] + } + + mapcode-viz( + rho, F(inst_houses), pi(inst_houses), + I_h: I_h, + X_h: x_h, + A_h: A_h, + F_name: [$F_("houses")$], + pi_name: [$mpi (#(inst_houses.len() - 1))$], + group-size: 1, + cell-size: 10mm, + scale-fig: 90% + )(inst_houses) + }$ +) + +=== Trace Analysis + +At each house $i$, we decide: +1. *Skip house $i$*: Take maximum from previous house: $x[i-1]$ +2. *Rob house $i$*: Add current money to maximum from $i-2$: $"houses"[i] + x[i-2]$ +3. *Choose maximum*: $max("skip", "rob")$ + +For $[2, 7, 9, 3, 1]$: +- House 0: Rob = $2$ +- House 1: Max(2, 7) = $7$ +- House 2: Max(7, 9+2) = $11$ +- House 3: Max(11, 3+7) = $11$ +- House 4: Max(11, 1+11) = $12$ which is maximum and correct diff --git a/algorithms/leetcode/L62_unique-paths.typ b/algorithms/leetcode/L62_unique-paths.typ new file mode 100644 index 0000000..ebfe062 --- /dev/null +++ b/algorithms/leetcode/L62_unique-paths.typ @@ -0,0 +1,169 @@ +#import "../../lib/style.typ": * +#import "../../lib/mapcode.typ": * + + +== Leetcode:62. #link("https://leetcode.com/problems/unique-paths/")[Unique Paths] + +There is a robot on an $m times n$ grid. The robot is initially located at the top-left corner (i.e., `grid[0][0]`). The robot tries to move to the bottom-right corner (i.e., `grid[m - 1][n - 1]`). The robot can only move either down or right at any point in time. + +Given the two integers $m$ and $n$, return the number of possible unique paths that the robot can take to reach the bottom-right corner. + +=== Recurrence Relation + +$ +"Paths"(i, j) = cases( + 1 & "if" i = 0 or j = 0, + "Paths"(i-1, j) + "Paths"(i, j-1) & "otherwise" +) +$ + +*Example:* $3 times 7$ grid → $28$ unique paths + +*Constraints:* +- $1 <= m, n <= 100$ + +=== Recursion Analysis + +This exhibits non-trivial recursion through: +1. *Two recursive branches*: Each cell reachable from top or left +2. *Two-dimensional state space*: Grid-based DP problem +3. *Combinatorial counting*: Summing paths from multiple directions +4. *Pascal's triangle structure*: Relates to binomial coefficients + +=== Mapcode Formalization + +*Primitives:* $+$ + +$ +I &= (m: NN, n: NN) \ +X &= [0..m-1] times [0..n-1] -> NN_bot \ +A &= NN \ +rho(m, n) &= {(i,j) |-> bot | i in {0 dots m-1}, j in {0 dots n-1}} \ +F(x)(i,j) &= cases( + 1 & "if" i = 0 or j = 0, + x[i-1,j] + x[i,j-1] & "if" x[i-1,j] != bot and x[i,j-1] != bot, + bot & "otherwise" +) \ +pi(x) &= x[m-1, n-1] +$ + +=== Complexity Analysis +- *Time Complexity*: $O(m times n)$ where $m$ and $n$ are grid dimensions +- *Space Complexity*: $O(m times n)$ for storing intermediate results + +#let inst_m = 4; +#let inst_n = 5; + +#figure( + caption: [Unique Paths computation for $#inst_m times #inst_n$ grid showing path counts.], + $#{ + let rho = ((m, n)) => { + let x = () + for i in range(0, m) { + let row = () + for j in range(0, n) { + row.push(none) + } + x.push(row) + } + x + } + + let F_i = (x) => ((i, j)) => { + if i == 0 or j == 0 { + 1 + } else { + let from_top = x.at(i - 1).at(j) + let from_left = x.at(i).at(j - 1) + + if from_top != none and from_left != none { + from_top + from_left + } else { + none + } + } + } + + let F = map_tensor(F_i, dim: 2) + + let pi = ((m, n)) => (x) => { + x.at(m - 1).at(n - 1) + } + + let x_h(x, diff_mask: none) = { + set text(size: 8pt) + let rows = () + + // Header row + let header_cells = () + header_cells.push(rect(stroke: none, inset: 3pt)[]) + for j in range(0, x.at(0).len()) { + header_cells.push(rect(fill: orange.transparentize(80%), inset: 3pt, stroke: 0.5pt)[*#j*]) + } + rows.push(grid(columns: header_cells.len() * (16pt,), rows: 14pt, align: center + horizon, ..header_cells)) + + for i in range(0, x.len()) { + let row = () + row.push(rect(fill: green.transparentize(80%), inset: 3pt, stroke: 0.5pt)[*#i*]) + + for j in range(0, x.at(i).len()) { + let val = x.at(i).at(j) + let display = if val == none { $bot$ } else { str(val) } + + let cell_fill = if diff_mask != none and diff_mask.at(i).at(j) { + yellow.transparentize(60%) + } else if i == 0 or j == 0 { + blue.transparentize(85%) + } else { + none + } + + row.push(rect(stroke: 0.5pt + gray, fill: cell_fill, inset: 3pt)[#display]) + } + rows.push(grid(columns: row.len() * (16pt,), rows: 14pt, align: center + horizon, ..row)) + } + grid(align: center, row-gutter: 0pt, ..rows) + } + + let I_h = ((m, n)) => { + diagram( + node((0, 0), [*Start*], stroke: 2pt + green, shape: rect), + node((3, 2), [*End*], stroke: 2pt + red, shape: rect), + edge((0, 0), (3, 0), "->", bend: 20deg), + edge((0, 0), (0, 2), "->", bend: -20deg), + edge((3, 0), (3, 2), "->"), + edge((0, 2), (3, 2), "->"), + node((1.5, 1), [$#m times #n$ grid]) + ) + } + + let A_h = (a) => { + box(fill: green.transparentize(70%), inset: 8pt, radius: 4pt)[ + *Unique Paths:* $#a$ + ] + } + + mapcode-viz( + rho, F, pi((inst_m, inst_n)), + I_h: I_h, + X_h: x_h, + A_h: A_h, + F_name: [$F$], + pi_name: [$mpi ((#(inst_m - 1), #(inst_n - 1)))$], + group-size: 3, + cell-size: 60mm, + scale-fig: 70% + )((inst_m, inst_n)) + }$ +) + +=== Trace Analysis + +The table shows: +1. *First row/column*: All $1$s (only one way to reach any cell on edge) +2. *Interior cells*: Sum of paths from above and left +3. *Pattern*: Resembles Pascal's triangle rotated + +For $4 times 5$ grid: $35$ unique paths to reach bottom-right corner. + +This can be verified combinatorially: to reach $(m-1, n-1)$ from $(0,0)$, we need exactly $m-1$ down moves and $n-1$ right moves. Total ways = $binom(m+n-2, m-1) = binom(7, 3) = 35$ which is correct. \ No newline at end of file diff --git a/algorithms/leetcode/L70_climbing-stairs.typ b/algorithms/leetcode/L70_climbing-stairs.typ new file mode 100644 index 0000000..7fd9178 --- /dev/null +++ b/algorithms/leetcode/L70_climbing-stairs.typ @@ -0,0 +1,165 @@ +#import "../../lib/style.typ": * +#import "../../lib/mapcode.typ": * + + +== Leetcode::70. #link("https://leetcode.com/problems/climbing-stairs/")[Climbing Stairs] + +You are climbing a staircase. It takes $n$ steps to reach the top. + +Each time you can either climb $1$ or $2$ steps. In how many distinct ways can you climb to the top? + +=== Recurrence Relation + +$ +"Ways"(n) = cases( + 1 & "if" n = 0, + 1 & "if" n = 1, + "Ways"(n-1) + "Ways"(n-2) & "if" n > 1 +) +$ + +*Example:* $n = 5$ → $8$ ways + +*Constraints:* +- $1 <= n <= 45$ + + + +=== Recursion Analysis + +This exhibits non-trivial recursion through: +1. *Two recursive calls*: Each step can be reached from previous two steps +2. *Fibonacci-like structure*: Classic combinatorial recursion +3. *Exponential without memoization*: Time complexity $O(2^n)$ naively, $O(n)$ with DP +4. *Counting problem*: Summing all possible paths + +=== Mapcode Formalization + +*Primitives:* $+$ + +$ +I &= n: NN \ +X &= [0..n] -> NN_bot \ +A &= NN \ +rho(n) &= {i |-> bot | i in {0 dots n}} \ +F(x)(i) &= cases( + 1 & "if" i = 0 or i = 1, + x[i-1] + x[i-2] & "if" x[i-1] != bot and x[i-2] != bot, + bot & "otherwise" +) \ +pi(x) &= x[n] +$ + +=== Complexity Analysis +- *Time Complexity*: $O(n)$ where $n$ is the number of steps +- *Space Complexity*: $O(n)$ for storing intermediate results + +#pagebreak() + +#let inst_stairs = 8; + +#figure( + caption: [Climbing Stairs computation for $n = #inst_stairs$ steps showing distinct ways.], + $#{ + let rho = (n) => { + let x = () + for i in range(0, n + 1) { + x.push(none) + } + x + } + + let F_i = (x) => ((i,)) => { + if i == 0 or i == 1 { + 1 + } else { + let way1 = x.at(i - 1) + let way2 = x.at(i - 2) + + if way1 != none and way2 != none { + way1 + way2 + } else { + none + } + } + } + + let F = map_tensor(F_i, dim: 1) + + let pi = (n) => (x) => { + x.at(n) + } + + let x_h(x, diff_mask: none) = { + set text(size: 8pt) + let rows = () + + let header = () + let values = () + + for i in range(0, x.len()) { + header.push(rect(fill: blue.transparentize(85%), inset: 3pt, stroke: 0.5pt, width: 16pt)[*#i*]) + + let val = x.at(i) + let display = if val == none { $bot$ } else { str(val) } + + let cell_fill = if diff_mask != none and diff_mask.at(i) { + yellow.transparentize(60%) + } else if i <= 1 { + green.transparentize(85%) + } else { + none + } + + values.push(rect(stroke: 0.5pt + gray, fill: cell_fill, inset: 3pt, width: 16pt)[#display]) + } + + rows.push(grid(columns: x.len() * (16pt,), rows: 14pt, align: center + horizon, column-gutter: 1pt, ..header)) + rows.push(grid(columns: x.len() * (16pt,), rows: 14pt, align: center + horizon, column-gutter: 1pt, ..values)) + stack(dir: ttb, spacing: 2pt, ..rows) + } + + let I_h = (n) => { + box(inset: 5pt)[ + *Staircase:* $n = #n$ steps + ] + } + + let A_h = (a) => { + box(fill: green.transparentize(70%), inset: 8pt, radius: 4pt)[ + *Distinct Ways:* $#a$ + ] + } + + mapcode-viz( + rho, F, pi(inst_stairs), + I_h: I_h, + X_h: x_h, + A_h: A_h, + F_name: [$F$], + pi_name: [$mpi (#inst_stairs)$], + group-size: 2, + cell-size: 20mm, + scale-fig: 90% + )(inst_stairs) + }$ +) + +=== Trace Analysis +At each step $i$, the number of ways to reach it is: +1. From step $i-1$ (taking 1 step) +2. From step $i-2$ (taking 2 steps) +Thus, total ways: = $"Ways"(i-1) + "Ways"(i-2)$ + +=== Correctness Verification + +The sequence follows Fibonacci pattern starting from $1, 1$: +$ +1, 1, 2, 3, 5, 8, 13, 21, 34, ... +$ + +For $n = 8$: $34$ ways which is correct. + +This is because to reach step $n$, you can either: +- Come from step $n-1$ (taking 1 step) +- Come from step $n-2$ (taking 2 steps) From 82d91301c7485670e5a876e2705cdcb6646d3aaa Mon Sep 17 00:00:00 2001 From: Dileepadari Date: Sun, 30 Nov 2025 11:59:32 +0530 Subject: [PATCH 2/2] feat: add Coin Change problem description and analysis in L322_coin-change.typ --- algorithms/leetcode/L322_coin-change.typ | 187 +++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 algorithms/leetcode/L322_coin-change.typ diff --git a/algorithms/leetcode/L322_coin-change.typ b/algorithms/leetcode/L322_coin-change.typ new file mode 100644 index 0000000..4bf0553 --- /dev/null +++ b/algorithms/leetcode/L322_coin-change.typ @@ -0,0 +1,187 @@ +#import "../../lib/style.typ": * +#import "../../lib/mapcode.typ": * + +== Leetcode:322. #link("https://leetcode.com/problems/coin-change/")[Coin Change] + +You are given an integer array `coins` representing coins of different denominations and an integer amount representing a total `amount` of money. + +Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return $-1$. + +You may assume that you have an infinite number of each kind of coin. +=== Recurrence Relation + +$ +"MinCoins"(n) = cases( + 0 & "if" n = 0, + min_(c in "coins", c <= n) {1 + "MinCoins"(n - c)} & "if" n > 0, + infinity & "if no solution exists" +) +$ + +*Example:* Coins = $[1, 2, 4]$, Amount = $13$ → Minimum: $4$ coins $(4+4+4+1)$ + +*Constraints:* +- $1 <= "coins"."length" <= 12$ +- $1 <= "coins"[i] <= 2^{31} - 1$ +- $0 <= "amount" <= 10^4$ + +=== Recursion Analysis + +This exhibits non-trivial recursion through: +1. *Multiple recursive branches*: For amount $n$, we explore $|"coins"|$ different branches +2. *Overlapping subproblems*: Same subproblem $"MinCoins"(k)$ computed via multiple paths +3. *Variable branching factor*: Number of branches depends on valid coins for each amount +4. *Optimal substructure*: Optimal solution contains optimal solutions to subproblems + +=== Mapcode Formalization + +*Primitives:* $min$, $+$, $infinity$ | $bot$, $<=$ + +$ +I &= ("amount": NN, "coins": NN^k) \ +X &= [0.."amount"] -> NN union {bot} \ +A &= NN union {-1} \ +rho("amount", "coins") &= {i |-> bot | i in {0 dots "amount"}} \ +F_("coins")(x)(n) &= cases( + 0 & "if" n = 0, + min_(c in "coins", c <= n) {1 + x[n-c]} & "if" forall c: x[n-c] != bot, + bot & "otherwise" +) \ +pi(x) &= x["amount"] "if" x["amount"] != infinity "else" -1 +$ + +=== Complexity Analysis +- *Time Complexity*: $O("amount" times k)$ where $k$ is number of coin types +- *Space Complexity*: $O("amount")$ for storing intermediate results + +#let inst_coins = (1, 2, 4); +#let inst_amount = 13; + +#figure( + caption: [Coin Change computation for amount $#inst_amount$ with coins $#inst_coins$ using mapcode framework.], + $#{ + let INF = 999999 + + let rho = ((amount, coins)) => { + let x = () + for i in range(0, amount + 1) { + x.push(none) + } + x + } + + let F_i = (coins) => (x) => ((n,)) => { + if n == 0 { + 0 + } else { + let min_coins = INF + let all_available = true + + for c in coins { + if c <= n { + let prev = x.at(n - c) + if prev == none { + all_available = false + break + } else if prev != INF { + min_coins = calc.min(min_coins, 1 + prev) + } + } + } + + if all_available { + min_coins + } else { + none + } + } + } + + let F = (coins) => map_tensor(F_i(coins), dim: 1) + + let pi = (amount) => (x) => { + x.at(amount) + } + + let x_h(x, diff_mask: none) = { + set text(size: 7pt) + let rows = () + + // Show in groups for readability + let chunk_size = 7 + for chunk_start in range(0, x.len(), step: chunk_size) { + let chunk_end = calc.min(chunk_start + chunk_size, x.len()) + let header = () + let values = () + + for i in range(chunk_start, chunk_end) { + header.push(rect(fill: blue.transparentize(85%), inset: 2pt, stroke: 0.4pt, width: 11pt)[#i]) + + let val = x.at(i) + let display = if val == none { + $bot$ + } else if val >= INF { + $infinity$ + } else { + str(val) + } + + let cell_fill = if diff_mask != none and diff_mask.at(i) { + yellow.transparentize(60%) + } else if val == 0 { + green.transparentize(85%) + } else { + none + } + + values.push(rect(stroke: 0.5pt + gray, fill: cell_fill, inset: 2pt, width: 12pt)[#display]) + } + + rows.push(grid(columns: (chunk_end - chunk_start) * (12pt,), rows: 12pt, align: center + horizon, column-gutter: 1pt, ..header)) + rows.push(grid(columns: (chunk_end - chunk_start) * (12pt,), rows: 12pt, align: center + horizon, column-gutter: 1pt, ..values)) + rows.push(v(4pt)) + } + stack(dir: ttb, spacing: 0pt, ..rows) + } + + let I_h = ((amount, coins)) => { + stack( + dir: ttb, + spacing: 3pt, + [*Target Amount:* $#amount$], + [*Coins:* $#coins$] + ) + } + + let A_h = (a) => { + let display = if a == 999999 { "-1" } else { str(a) } + box(fill: green.transparentize(70%), inset: 8pt, radius: 4pt)[ + *Minimum Coins:* $#display$ + ] + } + + mapcode-viz( + rho, F(inst_coins), pi(inst_amount), + I_h: I_h, + X_h: x_h, + A_h: A_h, + F_name: [$F_("coins")$], + pi_name: [$mpi (#inst_amount)$], + group-size: 3, + cell-size: 10mm, + scale-fig: 90% + )((inst_amount, inst_coins)) + }$ +) + +=== Trace Analysis +At each amount $n$, we consider each coin $c$: +1. If $c <= n$, we look at subproblem $"MinCoins"(n - c)$ +2. We add $1$ to the result of the subproblem to account for using coin $c$ +3. We take the minimum over all valid coins to get $"MinCoins"(n)$ + +=== Correctness Verification + +For amount $13$ with coins $[1, 2, 4]$: +- Optimal: $4$ coins = $4 + 4 + 4 + 1$ +- Our result: $4$ which is correct