Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions algorithms/closest_pair_of_points.typ
Original file line number Diff line number Diff line change
@@ -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)
}
$
)
146 changes: 146 additions & 0 deletions algorithms/edit_distance.typ
Original file line number Diff line number Diff line change
@@ -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))
}$)
Loading