diff --git a/algorithms/closest_pair_of_points.typ b/algorithms/closest_pair_of_points.typ new file mode 100644 index 0000000..b75f327 --- /dev/null +++ b/algorithms/closest_pair_of_points.typ @@ -0,0 +1,216 @@ +#import "../lib/style.typ": * +#import "../lib/mapcode.typ": * + +== Closest Pair of Points +#set math.equation(numbering: none) + +Compute the minimum Euclidean distance between any two points in a given set of 2D points. + +Formal definition (Divide and Conquer): +Let $P_x$ be the list of points sorted by x-coordinate. +Let $f(i, j)$ be the minimum squared distance in the slice $P_x[i..j]$. + +$ +f(i, j) = cases( + "brute_force_sq"(P_x[i..j]) & "if " j - i + 1 <= 3, + min(f(i, k), f(k+1, j), delta_"strip_sq") & "if " j - i + 1 > 3 +) +$ +where $k = floor((i + j) / 2)$, and $delta_"strip_sq"$ is the minimum squared distance found in the vertical strip. + +Example: +- $P = {(2, 3), (5, 1), (3, 4)} -> "dist" = sqrt(2) approx 1.414$ (between (2,3) and (3,4)) + +*As mapcode:* + +_primitives_: `min`, `sqrt`, `abs`, `dist` + +$ +// I is a list of points P. Let n = |P|. +// P_x is the static, pre-sorted list of points. +// X[i, j] stores f(i, j), the min *squared* dist in P_x[i..j] +I = P:"List"["Point"] quad quad +X_n & = [0..n-1] times [0..n-1] -> RR_bot quad quad +A = RR\ + +rho(P) & = { (i,j) -> bot | 0 <= i < n, 0 <= j < n} \ + +// F depends on the pre-sorted list P_x +F_(P_x)(x_(i,j)) & = cases( + "brute_force_sq"(P_x[i..j]) & "if " j - i + 1 <= 3, + min(x_(i, k), x_(k+1, j), delta_"strip_sq") & "if " j - i + 1 > 3 " and " x_(i,k) != bot, x_(k+1,j) != bot + )\ + +pi_P (x) & = "sqrt"(x_(0, n-1)) quad quad "where" n = |P| +$ + +// --- Visualization --- +// Helper functions (from Python implementation) +#let dist_sq = (p1, p2) => { + (p1.at(0) - p2.at(0))*(p1.at(0) - p2.at(0)) + (p1.at(1) - p2.at(1))*(p1.at(1) - p2.at(1)) +} + +#let brute_force_cpp = (points) => { + let min_d_sq = 1e100 // infinity + let n = points.len() + if n < 2 { return min_d_sq } + for i in range(n) { + for j in range(i + 1, n) { + let d_sq = dist_sq(points.at(i), points.at(j)) + if d_sq < min_d_sq { min_d_sq = d_sq } + } + } + min_d_sq +} + +#let strip_closest = (strip, min_delta_sq) => { + let min_d_sq = min_delta_sq + let n = strip.len() + for i in range(n) { + for j in range(i + 1, n) { + let y_diff = strip.at(j).at(1) - strip.at(i).at(1) + if (y_diff * y_diff) >= min_d_sq { break } + + let d_sq = dist_sq(strip.at(i), strip.at(j)) + if d_sq < min_d_sq { min_d_sq = d_sq } + } + } + min_d_sq +} + + +#let inst_P = ((2, 3), (1, 9), (4, 5), (5, 1), (2, 1), (3, 4), (7, 8), (2, 6), (8, 2), (6, 3), (1, 1), (2, 2), (3, 3)); +#let P_x_sorted = inst_P.sorted(key: p => p.at(0)); +#let inst_n = inst_P.len(); + +#figure( + caption: [Closest Pair of Points computation using mapcode for $n = #inst_n$ points; dynamic-programming table visualization. The table $X[i, j]$ stores the minimum squared distance for points $P_x[i..j]$.], +$ +#{ + let rho = (P) => { + let n = P.len() + let x = () + for i in range(n) { + let row = () + for j in range(n) { + // We only care about (i, j) where j > i + row.push(none) + } + x.push(row) + } + x + } + + // F_i takes the pre-sorted list P_x as an argument + let F_i = (P_x) => (x) => ((i, j)) => { + // We only compute for the upper triangle j > i + if j <= i { return none } + + let length = j - i + 1 + let current_slice = P_x.slice(i, j + 1) + + // Base case: brute force for <= 3 points + if length <= 3 { + return brute_force_cpp(current_slice) + } + + // Recursive case + let k = calc.floor((i + j) / 2) + + let delta_L_sq = x.at(i).at(k) + let delta_R_sq = x.at(k + 1).at(j) + + if delta_L_sq == none or delta_R_sq == none { + return none // Dependencies not met + } + + let delta_sq = calc.min(delta_L_sq, delta_R_sq) + let delta = calc.sqrt(delta_sq) + let median_x = P_x.at(k).at(0) + + // Build the strip + let strip = () + for p in current_slice { + if calc.abs(p.at(0) - median_x) < delta { + strip.push(p) + } + } + + // Sort strip by y + let strip_y = strip.sorted(key: p => p.at(1)) + + let delta_strip_sq = strip_closest(strip_y, delta_sq) + + return calc.min(delta_sq, delta_strip_sq) + } + + let F = (P_x) => map_tensor(F_i(P_x), dim: 2) + + let pi = (P) => (x) => { + let n = P.len() + if n < 2 { return 1e100 } + let final_dist_sq = x.at(0).at(n - 1) + + if final_dist_sq == none { + return none + } + // Return the actual distance, not the squared distance + return calc.sqrt(final_dist_sq) + } + + // draw DP table (n x n) + let x_h(x, diff_mask:none) = { + set text(weight: "bold") + let rows = () + let n = x.len() + + // Header row: show j index + let header_cells = () + header_cells.push(rect(stroke: none, inset: 4pt)[$i/j$]) // Top-left corner + for j in range(n) { + header_cells.push(rect(fill: orange.transparentize(70%), inset: 4pt)[$#j$]) + } + rows.push(grid(columns: header_cells.len() * (14pt,), rows: 14pt, align: center + horizon, ..header_cells)) + + + for i in range(n) { + let row = () + // Left label: i index + row.push(rect(fill: green.transparentize(70%), inset: 4pt)[$#i$]) + + for j in range(n) { + let val_raw = x.at(i).at(j) + // Show rounded squared distances + let val = if val_raw != none { + [$#calc.round(val_raw, digits: 1)$] + } else { + [$bot$] + } + + // Gray out the lower triangle (j <= i) + let cell_fill = if j <= i { gray.transparentize(70%) } else { none } + + if diff_mask != none and diff_mask.at(i).at(j) { + row.push(rect(stroke: gray, fill: yellow.transparentize(70%), inset: 4pt)[$#val$]) + } else { + row.push(rect(stroke: gray, fill: cell_fill, inset: 4pt)[$#val$]) + } + } + rows.push(grid(columns: row.len() * (14pt,), rows: 14pt, align: center + horizon, ..row)) + } + grid(align: center, ..rows) + } + + + mapcode-viz( + rho, + F(P_x_sorted), // Pass the sorted list to F + pi(inst_P), + X_h: x_h, + pi_name: [$mpi(P)$], + group-size: calc.min(2, inst_n), + cell-size: 60mm, scale-fig: 75% + )(inst_P) +} +$ +) \ No newline at end of file diff --git a/algorithms/edit_distance.typ b/algorithms/edit_distance.typ new file mode 100644 index 0000000..10d4134 --- /dev/null +++ b/algorithms/edit_distance.typ @@ -0,0 +1,146 @@ +#import "../lib/style.typ": * +#import "../lib/mapcode.typ": * + +== Edit Distance +#set math.equation(numbering: none) + +Compute the Edit Distance (Levenshtein distance) between two strings, S and T. This is the minimum number of single-character edits (insertions, deletions, or substitutions) required to change S into T. + +Formal definition: +Let $"ED"(i, j)$ be the edit distance between the first $i$ characters of $S$ and the first $j$ characters of $T$. +$ +"ED"(i, j) = cases( + i & "if " j = 0, + j & "if " i = 0, + min( + "ED"(i-1, j) + 1, + "ED"(i, j-1) + 1, + "ED"(i-1, j-1) + "cost" + ) & "otherwise" +) +$ +where "cost" is 0 if $S_i = T_j$ and 1 otherwise. + +Example: +- $S = "kitten", T = "sitting" -> "ED" = 3$ + +*As mapcode:* + +_primitives_: `min`, `sum`($+$) + +$ +I &= S:"Str" times T:"Str" quad quad "let" m = |S|, n = |T| \ +X_(i,j) & = [0..m] times [0..n] -> NN_bot quad quad +A = NN\ +rho(m,n) & = { (i,j) -> bot | +i in {0 dots m}, j in {0 dots n}} \ +F_(S, T)(x_(i,j)) & = cases( + i & "if " j = 0, + j & "if " i = 0, + min( + x_(i-1, j) + 1, + x_(i, j-1) + 1, + x_(i-1, j-1) + "cost" + ) & "otherwise" +)\ +pi_(S,T) (x) & = x_(m,n) quad quad "where" m = |S|, n = |T| +$ + +#let inst_S = "kitten"; +#let inst_T = "sitting"; +#let inst_m = inst_S.len(); +#let inst_n = inst_T.len(); + +#figure( + caption: [Edit Distance computation using mapcode for $S = #inst_S$ and $T = #inst_T$; dynamic-programming table visualization.], +$#{ + let rho = ((m, n)) => { + let x = () + for i in range(0, m + 1) { + let row = () + for j in range(0, n + 1) { + row.push(none) + } + x.push(row) + } + x + } + + let F_i = ((S, T)) => (x) => ((i,j)) => { + if i == 0 { + j // Base case: ED(0, j) = j + } else if j == 0 { + i // Base case: ED(i, 0) = i + } else { + // Check dependencies + let val_del = x.at(i - 1).at(j) + let val_ins = x.at(i).at(j - 1) + let val_sub = x.at(i - 1).at(j - 1) + + if val_del == none or val_ins == none or val_sub == none { + none + } else { + // S.at(i - 1) corresponds to S_i + let cost = if S.at(i - 1) == T.at(j - 1) { 0 } else { 1 } + + calc.min( + val_del + 1, // Deletion + val_ins + 1, // Insertion + val_sub + cost // Substitution + ) + } + } + } + let F = ((S, T)) => map_tensor(F_i((S, T)), dim: 2) + + let pi = ((S, T)) => (x) => { + let m = S.len() + let n = T.len() + x.at(m).at(n) + } + + // draw DP table with sequence labels + let x_h(x, diff_mask:none) = { + set text(weight: "bold") + let rows = () + + // header row: show T characters (with an initial empty corner) + let header_cells = () + header_cells.push(rect(stroke: none, inset: 4pt)[$bot$]) + header_cells.push(rect(fill: orange.transparentize(70%), inset: 4pt)[$emptyset$]) + for j in range(0, inst_n) { + header_cells.push(rect(fill: orange.transparentize(70%), inset: 4pt)[$#inst_T.at(j)$]) + } + rows.push(grid(columns: header_cells.len() * (14pt,), rows: 14pt, align: center + horizon, ..header_cells)) + + + for i in range(0, x.len()) { + let row = () + // left label: S character for i>0, empty for i=0 + if i == 0 { + row.push(rect(fill: green.transparentize(70%), inset: 4pt)[$emptyset$]) + } else { + row.push(rect(fill: green.transparentize(70%), inset: 4pt)[$#inst_S.at(i - 1)$]) + } + + for j in range(0, x.at(i).len()) { + let val = if x.at(i).at(j) != none {[$#x.at(i).at(j)$]} else {[$bot$]} + if diff_mask != none and diff_mask.at(i).at(j) { + row.push(rect(stroke: gray, fill: yellow.transparentize(70%), inset: 4pt)[$#val$]) + } else { + row.push(rect(stroke: gray, inset: 4pt)[$#val$]) + } + } + rows.push(grid(columns: row.len() * (14pt,), rows: 14pt, align: center + horizon, ..row)) + } + grid(align: center, ..rows) + } + + mapcode-viz( + rho,F((inst_S, inst_T)), pi((inst_S, inst_T)), + X_h: x_h, + pi_name: [$mpi ((#inst_m, #inst_n))$], + group-size: calc.min(4, inst_m), + cell-size: 60mm, scale-fig: 60% + )((inst_m, inst_n)) +}$) \ No newline at end of file diff --git a/algorithms/integer_partition.typ b/algorithms/integer_partition.typ new file mode 100644 index 0000000..dab473c --- /dev/null +++ b/algorithms/integer_partition.typ @@ -0,0 +1,128 @@ + +#import "../lib/style.typ": * +#import "../lib/mapcode.typ": * + +== Integer Partitions +#set math.equation(numbering: none) + +Compute $p(n, k)$, the number of partitions of an integer $n$ using parts less than or equal to $k$. + +Formal definition: +$ +p(n, k) = cases( + 1 & "if " n = 0, + 0 & "if " n > 0 " and " k = 0, + p(n, k-1) + p(n-k, k) & "if " n > 0 " and " k > 0 +) +$ +(where $p(n-k, k)$ is 0 if $n-k < 0$) + +Examples: +- $p(5, 3) = 5$ +- $p(5, 5) = 7$ +- $p(7, 5) = 15$ + +*As mapcode:* + +_primitives_: `sum`($+$) + +$ +I = n:NN times k:NN quad quad quad +X_(n,k) & = [0..n] times [0..k] -> NN_bot quad quad quad +A = NN\ +rho(n,k) & = { (i,j) -> bot | i in {0 dots n}, j in {0 dots k}} \ +F(x_(i,j)) & = cases( + 1 & "if " i = 0, + 0 & "if " i > 0 " and " j = 0, + x_(i, j-1) + x_(i-j, j) & "if " i > 0 " and " j > 0 + )\ +pi_(n,k) (x) & = x_(n,k) +$ + +#let inst_n = 7; +#let inst_k = 5; +#figure( + caption: [Integer Partition computation using mapcode for $n = #inst_n$ and $k = #inst_k$; dynamic-programming table visualization.], +$ +#{ + let rho = ((inst_n, inst_k)) => { + let x = () + for i in range(0, inst_n + 1) { + let row = () + for j in range(0, inst_k + 1) { + row.push(none) + } + x.push(row) + } + x + } + + let F_i = (x) => ((i,j)) => { + if i == 0 { + 1 // p(0, k) = 1 + } else if j == 0 { + 0 // p(n, 0) = 0 (for n > 0) + } else { + let val1 = x.at(i).at(j - 1) // p(n, k-1) + + let val2 = none // p(n-k, k) + if i - j == 0 { + val2 = 1 + } else if i - j > 0 { + val2 = x.at(i - j).at(j) + } else { + val2 = 0 + } + + if val1 != none and val2 != none { + val1 + val2 // Recurrence: p(n, k-1) + p(n-k, k) + } else { + none + } + } + } + let F = map_tensor(F_i, dim: 2) + + let pi = ((n, k)) => (x) => x.at(n).at(k) + + // draw DP table with n and k labels + let x_h(x, diff_mask:none) = { + set text(weight: "bold") + let rows = () + + // header row: show k values + let header_cells = () + header_cells.push(rect(stroke: none, inset: 4pt)[$bot$]) // Top-left empty corner + for j in range(0, inst_k + 1) { + header_cells.push(rect(fill: orange.transparentize(70%), inset: 4pt)[$#j$]) + } + rows.push(grid(columns: header_cells.len() * (14pt,), rows: 14pt, align: center + horizon, ..header_cells)) + + + for i in range(0, x.len()) { + let row = () + row.push(rect(fill: green.transparentize(70%), inset: 4pt)[$#i$]) + + for j in range(0, x.at(i).len()) { + let val = if x.at(i).at(j) != none {[$#x.at(i).at(j)$]} else {[$bot$]} + if diff_mask != none and diff_mask.at(i).at(j) { + row.push(rect(stroke: gray, fill: yellow.transparentize(70%), inset: 4pt)[$#val$]) + } else { + row.push(rect(stroke: gray, inset: 4pt)[$#val$]) + } + } + rows.push(grid(columns: row.len() * (14pt,), rows: 14pt, align: center + horizon, ..row)) + } + grid(align: center, ..rows) + } + + mapcode-viz( + rho,F, pi((inst_n, inst_k)), + X_h: x_h, + pi_name: [$mpi ((#inst_n, #inst_k))$], + group-size: calc.min(3, inst_n), + cell-size: 60mm, scale-fig: 65% + )((inst_n, inst_k)) +} +$ +) \ No newline at end of file diff --git a/algorithms/n_queens.typ b/algorithms/n_queens.typ new file mode 100644 index 0000000..1e7b717 --- /dev/null +++ b/algorithms/n_queens.typ @@ -0,0 +1,131 @@ + +#import "../lib/style.typ": * +#import "../lib/mapcode.typ": * + +== N-Queens +#set math.equation(numbering: none) + +Compute the number of solutions to the N-Queens problem: placing $N$ queens on an $N times N$ chessboard so that no two queens attack each other. + +Formal definition: +Let $f(k)$ be the set of all valid partial queen placements in rows $0$ to $k-1$. +$ +f(k) = cases( + { () } & "if " k = 0, + union_("sol" in f(k-1)) { "sol" + (c,) | c in [0..N-1], "is_safe"("sol", c) } & "if " k > 0 +) +$ +The total number of solutions for an $N times N$ board, denoted $"nqueens"(N)$, is the size of the set of full solutions, i.e., $|f(N)|$. + +Examples: +- $"nqueens"(1) -> |f(1)| = 1$ +- $"nqueens"(4) -> |f(4)| = 2$ +- $"nqueens"(8) -> |f(8)| = 92$ + +*As mapcode:* + +_primitives_: `union`, `iteration` + +$ +I &= n:NN quad quad quad +// X[k] = f(k), the set of partial solutions for rows 0..k-1 +X_n = [0..n] -> "Set"["Tuple"]bot quad quad quad A = NN\ + +rho(n) & = {k -> bot | k in [0..n]}\ + +F(x_k) & = cases( { () } & "if " k = 0, union_("sol" in x_(k-1)) { "sol" + (c,) | c in [0..n-1], "is_safe"("sol", c) } & "if " k > 0 " and " x_(k-1) != bot )\ + +pi(x) & = |x_n| = |x_(|x| - 1)| +$ + +#let inst = 4; +#figure( + caption: [N-Queens computation using mapcode for $n = #inst$. The state vector $X[k]$ shows the valid partial solutions using $k$ queens (in rows $0..k-1$).], +$ +#{ + let rho = (inst) => { + let x = () + for i in range(0, inst + 1) { + x.push(none) + } + x + } + + let is_safe = (partial_solution, new_col) => { + let new_row = partial_solution.len() + + // column conflict + if partial_solution.find(c => c == new_col) != none { + return false + } + + // diagonal conflicts + for (prev_row, prev_col) in partial_solution.enumerate() { + if (new_row - prev_row == new_col - prev_col or new_row - prev_row == prev_col - new_col) { + return false + } + } + return true + } + + let F_i = (n) => (x) => ((i,)) => { + if i == 0 { + ( (), ) + } else if x.at(i - 1) != none { + let prev_solutions = x.at(i - 1) + let new_solutions_for_i = () + + for partial_sol in prev_solutions { + for new_col in range(n) { + if is_safe(partial_sol, new_col) { + // add (partial_sol + (new_col,)) + new_solutions_for_i.push(partial_sol + (new_col,)) + } + } + } + new_solutions_for_i + } else { + none + } + } + let F = (n) => map_tensor(F_i(n), dim: 1) + + let pi = (i) => (x) => { + if x.at(i) != none { + x.at(i).len() + } else { + none + } + } + + let X_h = (x, diff_mask: none) => { + let cells = x.enumerate().map(((i, x_i)) => { + let val = if x_i != none { + // display the count of partial solutions + [$#x_i$] + // [$#x_i.len()$] + } else { + [$bot$] + } + if diff_mask != none and diff_mask.at(i) { + // changed element: highlight + rect(fill: yellow.transparentize(70%), inset: 2pt)[$#val$] + } else { + rect(stroke: none, inset: 2pt)[$#val$] + } + }) + $vec(delim: "[", ..cells)$ + } + + mapcode-viz( + rho, + F(inst), + pi(inst), + X_h: X_h, + pi_name: [$mpi (inst)$], + group-size: calc.min(1, inst + 1), + cell-size: 10mm, scale-fig: 85% + )(inst) +} +$ +) \ No newline at end of file diff --git a/main.typ b/main.typ index 9952d9b..e22f629 100644 --- a/main.typ +++ b/main.typ @@ -51,4 +51,12 @@ All primitives are _strict_ meaning they do not allow for undefined values (i.e. #pagebreak() #include "algorithms/LongestCommonSubsequence.typ" #pagebreak() +#include "algorithms/integer_partition.typ" +#pagebreak() +#include "algorithms/closest_pair_of_points.typ" +#pagebreak() +#include "algorithms/edit_distance.typ" +#pagebreak() +#include "algorithms/n_queens.typ" +#pagebreak() #include "algorithms/leetcode/P2_add-two-numbers.typ"