Determinants
Concept
The determinant is a single number attached to a square matrix that measures how the matrix scales signed area (2Γ2), volume (3Γ3), or hypervolume (nΓn) β and it is zero exactly when the matrix is singular. In ch06 you saw the 2Γ2 inverse formula $\mathbf{A}^{-1} = \frac{1}{ad - bc} \begin{pmatrix} d & -b \\ -c & a \end{pmatrix}$, where the denominator $ad - bc$ is the determinant. This chapter formalizes that number, gives two ways to compute it, and pins down the properties that make it the single most useful singularity test in linear algebra.
Why This Matters
The determinant is the bridge between the computational linear algebra of ch03βch06 and the structural theory that comes next. It tells you whether a transformation collapses a direction (det = 0) without ever running Gauss-Jordan β which is how you'll recognize singular matrices, degenerate data, and rank-deficient systems at a glance. It feeds directly into ch08 (rank and vector spaces: a matrix is singular exactly when its rows/columns are linearly dependent), ch11 (eigenvalues: $\det(\mathbf{A} - \lambda\mathbf{I}) = 0$ is the characteristic equation), and ch24 (Jacobians: the determinant of the Jacobian is the factor that converts areas/volumes under a change of variables β the reason probability densities need Jacobian terms in transformed distributions).
Mathematical Notation
The determinant
For a square matrix $\mathbf{A} \in \mathbb{R}^{n \times n}$, the determinant is written $\det(\mathbf{A})$ or $|\mathbf{A}|$ (read "determinant of A" β the bars are not absolute value). It is a single real number defined by three properties:
- $\det(\mathbf{I}_n) = 1$ β the identity scales nothing, so its scaling factor is 1;
- swapping two rows multiplies the determinant by $-1$;
- the determinant is linear in each row separately.
These three rules determine the determinant uniquely, and everything else follows from them.
For $2 \times 2$ and $3 \times 3$ matrices there are closed forms:
(The $3 \times 3$ pattern is Sarrus' rule β products down-right minus products down-left; it works only for 3Γ3.)
Motivation: the determinant's job is to answer one yes/no question β is this matrix invertible? β plus the quantitative version β by what factor does it stretch space? Both answers fall out of the same number.
Minors and cofactors
The minor $M_{ij}$ of $\mathbf{A}$ is the $(n-1) \times (n-1)$ matrix you get by deleting row $i$ and column $j$. The cofactor is the signed minor:
The sign $(-1)^{i+j}$ alternates in a checkerboard pattern, starting with $+$ in the top-left corner:
Motivation: minors and cofactors are the machinery of the expansion formula below β they break an $n \times n$ determinant into a sum of $(n-1) \times (n-1)$ ones, recursively down to $1 \times 1$.
Cofactor expansion
The determinant can be computed by expanding along any row or column. Along the first row:
Motivation: cofactor expansion is the definitional algorithm β the one that proves the determinant is well-defined and that textbooks use to derive every property. It is not how you compute determinants in practice: expanding an $n \times n$ determinant this way takes $n!$ operations, which is why our practical implementation (below) uses elimination instead.
Key properties
Every property below is used somewhere in this course, so each gets its motivation:
| Property | Statement | Motivation |
|---|---|---|
| Singularity test | $\det(\mathbf{A}) = 0 \iff \mathbf{A}$ is singular | The inverse exists iff $\det \neq 0$ β the ch06 connection |
| Row swap | Swapping two rows flips the sign | This is why elimination tracks sign (below); also the orientation flip in geometry |
| Row addition | Adding a multiple of one row to another leaves $\det$ unchanged | The second rule that makes elimination legal |
| Row scaling | Multiplying a row by $c$ multiplies $\det$ by $c$ | We avoid this operation in elimination precisely because it changes the answer |
| Triangular matrices | $\det$ = product of diagonal entries | The payoff of elimination: an upper-triangular matrix's determinant is free |
| Multiplicativity | $\det(\mathbf{A}\mathbf{B}) = \det(\mathbf{A})\det(\mathbf{B})$ | How scaling factors compose; used in ch11+ whenever a matrix is factored |
| Transpose | $\det(\mathbf{A}^T) = \det(\mathbf{A})$ | Rows and columns are interchangeable for the determinant |
| Singular rows | Two identical (or proportional) rows $\Rightarrow \det = 0$ | Instant singularity detection, no computation needed |
Intuition
Determinant as signed area
Take the columns of a $2 \times 2$ matrix $\mathbf{A}$ as two vectors $\mathbf{u}, \mathbf{v}$ drawn from the origin. They span a parallelogram, and $|\det(\mathbf{A})|$ is exactly its area:
v = (c, d) β
/ \
/ \ area of parallelogram
/ \ = |ad β bc|
βββββββββ
u = (a, b)
The sign of the determinant records orientation: if $\mathbf{v}$ is counterclockwise from $\mathbf{u}$ the determinant is positive, clockwise gives negative. Swapping the two columns (or rows) flips the sign β that's the row-swap property, now with a picture. In 3D the same story holds: the columns span a parallelepiped and $|\det|$ is its volume.
Singular = collapse
When the two columns point in the same direction (the parallelogram degenerates to a line), the area is 0 β and the matrix has no inverse, exactly as ch06 taught: the transformation squashes 2D space down to a line, destroying information irreversibly. The determinant is the algebraic version of "how much space survives the transformation": full space (det β 0) or collapsed (det = 0).
Why elimination can compute it
Gaussian elimination turns a general matrix into an upper-triangular one using exactly two moves β adding a multiple of one row to another and swapping rows. Both moves have known effects on the determinant (none, and sign-flip), and a triangular matrix's determinant is just the diagonal product. So: eliminate, count the swaps, multiply the diagonal. This is the O(nΒ³) algorithm, and it's the one real libraries use.
Worked Examples
Example 1: a 2Γ2 β signed area
The columns $(4, 2)$ and $(7, 6)$ span a parallelogram of area 10, and the sign is positive: the second column is counterclockwise from the first.
Example 2: a 3Γ3 by cofactor expansion
Expand along the first row ($+a_{11}C_{11} - a_{12}C_{12} + a_{13}C_{13}$, with the checkerboard starting at $+$):
- 1 \cdot \det\begin{pmatrix} 1 & 1 \\ 0 & 2 \end{pmatrix}
- 0 \cdot \det\begin{pmatrix} 1 & 2 \\ 0 & 1 \end{pmatrix} $$
The zero entry kills the third term β a good reason to expand along the row or column with the most zeros. Then:
$$ = 2(4 - 1) - 1(2 - 0) + 0 = 6 - 2 = 4 $$
Example 3: elimination with a row swap
Subtract row 1 from rows 2 and 3:
The column-2 pivot position holds a 0, so we swap rows 2 and 3 β one swap, sign flips to $-1$:
Example 4: a singular matrix
Rows 1 and 3 are identical β the parallelogram (here, parallelepiped) is degenerate, so no computation is needed: $\det(\mathbf{A}) = 0$, and $\mathbf{A}$ is singular.
Rust Implementation
Add a new crate to your workspace:
cd code && cargo new --lib --edition 2024 ch07-determinants
Register it in code/Cargo.toml:
members = [..., "ch07-determinants"]
This crate builds on the Matrix struct from ch03. Open code/ch07-determinants/src/lib.rs and start by copying the full Matrix impl from ch03-linear-algebra-matrices/src/lib.rs (as you did in ch04 and ch06). Then add the two determinant methods plus their helpers:
/// A matrix is stored in row-major order as a flat Vec<f64>.
///
/// Row-major means the first row occupies indices 0..cols,
/// the second row occupies indices cols..2*cols, and so on.
/// The element at (row, col) is at index row * cols + col.
#[derive(Debug, Clone, PartialEq)]
pub struct Matrix {
data: Vec<f64>,
rows: usize,
cols: usize,
}
impl Matrix {
/// Create a new matrix from a flat Vec<f64> in row-major order.
///
/// # Panics
/// Panics if data.len() != rows * cols.
pub fn new(data: Vec<f64>, rows: usize, cols: usize) -> Self {
assert_eq!(
data.len(),
rows * cols,
"Data length {} does not match dimensions {}Γ{}",
data.len(),
rows,
cols
);
Matrix { data, rows, cols }
}
/// Number of rows.
pub fn rows(&self) -> usize {
self.rows
}
/// Number of columns.
pub fn cols(&self) -> usize {
self.cols
}
/// Shape as (rows, cols).
pub fn shape(&self) -> (usize, usize) {
(self.rows, self.cols)
}
/// Access the element at (row, col) β 0-indexed.
///
/// # Panics
/// Panics if row or col is out of bounds.
pub fn get(&self, row: usize, col: usize) -> f64 {
assert!(row < self.rows, "Row {} out of bounds (rows={})", row, self.rows);
assert!(col < self.cols, "Col {} out of bounds (cols={})", col, self.cols);
self.data[row * self.cols + col]
}
/// The nΓn identity matrix: 1 on the diagonal, 0 elsewhere.
pub fn identity(n: usize) -> Matrix {
let mut data = vec![0.0; n * n];
for i in 0..n {
data[i * n + i] = 1.0;
}
Matrix {
data,
rows: n,
cols: n,
}
}
/// Matrix multiplication: self * other.
///
/// For A β β^{mΓn} and B β β^{nΓp}, the result C β β^{mΓp}.
/// C_{ij} = Ξ£_{k=0}^{n-1} A_{ik} * B_{kj}
///
/// # Panics
/// Panics if self.cols != other.rows (inner dimensions must match).
pub fn multiply(&self, other: &Matrix) -> Matrix {
assert_eq!(
self.cols, other.rows,
"Cannot multiply {}Γ{} by {}Γ{}",
self.rows, self.cols, other.rows, other.cols
);
let mut data = vec![0.0; self.rows * other.cols];
for i in 0..self.rows {
for k in 0..self.cols {
let aik = self.get(i, k);
for j in 0..other.cols {
data[i * other.cols + j] += aik * other.get(k, j);
}
}
}
Matrix {
data,
rows: self.rows,
cols: other.cols,
}
}
/// ββ Determinants ββ
/// Compute the determinant by **cofactor expansion** along the first row.
///
/// For a 1Γ1 matrix the determinant is the single entry. For n > 1 it is
///
/// det(A) = Ξ£β±Ό (-1)^j Β· a_{0j} Β· det(M_{0j})
///
/// where M_{0j} is the (nβ1)Γ(nβ1) **minor** obtained by deleting row 0
/// and column j.
///
/// This is the definitional algorithm β it is O(n!), so it is only
/// practical for small matrices (up to about 4Γ4).
///
/// # Panics
/// Panics if the matrix is not square.
pub fn determinant_cofactor(&self) -> f64 {
assert_eq!(
self.rows, self.cols,
"Determinant requires a square matrix (got {}Γ{})",
self.rows, self.cols
);
let n = self.rows;
match n {
// Empty-product convention: the determinant of a 0Γ0 matrix is 1.
0 => 1.0,
1 => self.get(0, 0),
_ => {
let mut det = 0.0;
for j in 0..n {
let sign = if j % 2 == 0 { 1.0 } else { -1.0 };
det += sign * self.get(0, j) * self.minor(0, j).determinant_cofactor();
}
det
}
}
}
/// Compute the determinant by **Gaussian elimination**.
///
/// The matrix is reduced to upper-triangular form in place (on a copy)
/// using only two row operations that leave the determinant unchanged or
/// track it exactly:
///
/// - adding a multiple of one row to another β determinant unchanged;
/// - swapping two rows β determinant changes sign.
///
/// The determinant is then the product of the diagonal entries, with the
/// sign applied. This is O(nΒ³) and is the algorithm used in practice.
///
/// A singular matrix has determinant 0, returned as exactly 0.0.
///
/// # Panics
/// Panics if the matrix is not square.
pub fn determinant(&self) -> f64 {
assert_eq!(
self.rows, self.cols,
"Determinant requires a square matrix (got {}Γ{})",
self.rows, self.cols
);
let n = self.rows;
let mut m = self.clone();
let mut sign = 1.0;
for col in 0..n {
// Partial pivoting: among rows col..n, pick the one with the
// largest |value| in this column and swap it into the pivot row.
let mut pivot = col;
for row in (col + 1)..n {
if m.get(row, col).abs() > m.get(pivot, col).abs() {
pivot = row;
}
}
// If every candidate pivot is ~0, the column is all zeros below
// the diagonal: the matrix is singular and det = 0. The 1e-10
// threshold is an absolute one that assumes a well-scaled matrix
// (entries of order 1), the convention used throughout this course.
if m.get(pivot, col).abs() < 1e-10 {
return 0.0;
}
if pivot != col {
m.swap_rows(col, pivot);
sign = -sign;
}
// Zero out the entries below the pivot. Adding a multiple of the
// pivot row to a lower row never changes the determinant.
for row in (col + 1)..n {
let factor = m.get(row, col) / m.get(col, col);
if factor != 0.0 {
m.add_scaled_row(row, col, -factor);
}
}
}
// Upper-triangular: det = sign Β· βα΅’ a_ii.
let mut det = sign;
for i in 0..n {
det *= m.get(i, i);
}
det
}
/// ββ Private helpers ββ
/// The (nβ1)Γ(nβ1) **minor** M_{ij}: the matrix obtained by deleting
/// row `row` and column `col` from `self`.
///
/// # Panics
/// Panics if row or col is out of bounds, or if the matrix is 1Γ1 or
/// smaller (there is nothing left after deleting).
fn minor(&self, row: usize, col: usize) -> Matrix {
assert!(row < self.rows, "Row {} out of bounds (rows={})", row, self.rows);
assert!(col < self.cols, "Col {} out of bounds (cols={})", col, self.cols);
assert!(self.rows > 1 && self.cols > 1, "Minor requires at least a 2Γ2 matrix");
let mut data = Vec::with_capacity((self.rows - 1) * (self.cols - 1));
for i in 0..self.rows {
if i == row {
continue;
}
for j in 0..self.cols {
if j == col {
continue;
}
data.push(self.get(i, j));
}
}
Matrix {
data,
rows: self.rows - 1,
cols: self.cols - 1,
}
}
/// Swap rows `a` and `b` in place.
fn swap_rows(&mut self, a: usize, b: usize) {
if a == b {
return;
}
for j in 0..self.cols {
let tmp = self.data[a * self.cols + j];
self.data[a * self.cols + j] = self.data[b * self.cols + j];
self.data[b * self.cols + j] = tmp;
}
}
/// Add `factor` times row `src` to row `dst` in place:
/// row_dst β row_dst + factor * row_src.
fn add_scaled_row(&mut self, dst: usize, src: usize, factor: f64) {
for j in 0..self.cols {
self.data[dst * self.cols + j] += factor * self.data[src * self.cols + j];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-10
}
#[test]
fn test_1x1_det_is_the_entry() {
assert!(approx_eq(Matrix::new(vec![5.0], 1, 1).determinant(), 5.0));
assert!(approx_eq(Matrix::new(vec![-3.0], 1, 1).determinant_cofactor(), -3.0));
}
#[test]
fn test_2x2_ad_minus_bc() {
// det([[4,7],[2,6]]) = 4Β·6 β 7Β·2 = 10
let a = Matrix::new(vec![4.0, 7.0, 2.0, 6.0], 2, 2);
assert!(approx_eq(a.determinant(), 10.0));
assert!(approx_eq(a.determinant_cofactor(), 10.0));
}
#[test]
fn test_2x2_row_swap_flips_sign() {
// det([[0,1],[1,0]]) = β1
let a = Matrix::new(vec![0.0, 1.0, 1.0, 0.0], 2, 2);
assert!(approx_eq(a.determinant(), -1.0));
assert!(approx_eq(a.determinant_cofactor(), -1.0));
}
#[test]
fn test_3x3_known_value() {
// det([[2,1,0],[1,2,1],[0,1,2]]) = 4
let a = Matrix::new(vec![2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 2.0], 3, 3);
assert!(approx_eq(a.determinant(), 4.0));
assert!(approx_eq(a.determinant_cofactor(), 4.0));
}
#[test]
fn test_3x3_zero_pivot_requires_swap() {
// det([[1,1,1],[1,1,2],[1,2,1]]) = β1; the first elimination step
// leaves a zero pivot in column 1 that needs a row swap.
let a = Matrix::new(vec![1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 1.0, 2.0, 1.0], 3, 3);
assert!(approx_eq(a.determinant(), -1.0));
assert!(approx_eq(a.determinant_cofactor(), -1.0));
}
#[test]
fn test_triangular_det_is_diagonal_product() {
// Upper triangular: det = 2Β·3Β·4 = 24
let a = Matrix::new(vec![2.0, 5.0, 7.0, 0.0, 3.0, 1.0, 0.0, 0.0, 4.0], 3, 3);
assert!(approx_eq(a.determinant(), 24.0));
assert!(approx_eq(a.determinant_cofactor(), 24.0));
}
#[test]
fn test_identity_det_is_one() {
for n in 1..=5 {
assert!(approx_eq(Matrix::identity(n).determinant(), 1.0));
assert!(approx_eq(Matrix::identity(n).determinant_cofactor(), 1.0));
}
}
#[test]
fn test_singular_det_is_zero() {
// Two identical rows β singular β det = 0.
let a = Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 2.0, 3.0], 3, 3);
assert!(approx_eq(a.determinant(), 0.0));
assert!(approx_eq(a.determinant_cofactor(), 0.0));
}
#[test]
fn test_4x4_cofactor_matches_elimination() {
let a = Matrix::new(
vec![1.0, 2.0, 0.0, 1.0, 0.0, 1.0, 3.0, 0.0, 2.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 2.0],
4,
4,
);
let det_elim = a.determinant();
let det_cof = a.determinant_cofactor();
assert!(
approx_eq(det_elim, det_cof),
"elimination {} vs cofactor {}",
det_elim,
det_cof
);
// Verified with exact (rational) arithmetic: det = 16.
assert!(approx_eq(det_elim, 16.0));
}
#[test]
fn test_4x4_band_matrix_matches_elimination() {
let a = Matrix::new(
vec![2.0, 1.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0, 0.0, 1.0, 2.0, 1.0, 0.0, 0.0, 1.0, 2.0],
4,
4,
);
let det_elim = a.determinant();
let det_cof = a.determinant_cofactor();
assert!(approx_eq(det_elim, det_cof));
// det = 5 for this tridiagonal matrix (verified by cofactor).
assert!(approx_eq(det_elim, 5.0));
}
#[test]
fn test_det_of_product_equals_product_of_dets() {
// det(AB) = det(A)Β·det(B) β a key property worth pinning down.
let a = Matrix::new(vec![1.0, 2.0, 3.0, 4.0], 2, 2);
let b = Matrix::new(vec![0.0, 1.0, 1.0, 0.0], 2, 2);
let ab = a.multiply(&b);
assert!(approx_eq(ab.determinant(), a.determinant() * b.determinant()));
assert!(approx_eq(
ab.determinant_cofactor(),
a.determinant_cofactor() * b.determinant_cofactor()
));
}
#[test]
#[should_panic(expected = "square")]
fn test_non_square_elimination_panics() {
Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).determinant();
}
#[test]
#[should_panic(expected = "square")]
fn test_non_square_cofactor_panics() {
Matrix::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).determinant_cofactor();
}
}
Run the tests:
cd code && cargo test -p ch07-determinants
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Walkthrough
-
determinant_cofactor(&self) -> f64β The definitional algorithm. For $n = 1$ it returns the single entry; otherwise it walks the first row, multiplying each $a_{0j}$ by its cofactor $(-1)^j \det(M_{0j})$ and summing. Thej % 2check implements the checkerboard sign. Recursion bottoms out at $1 \times 1$, so an $n \times n$ matrix costs $n!$ recursive calls β fine for the worked examples above, hopeless past $4 \times 4$. -
minor(row, col) -> Matrixβ Deletes one row and one column by skipping them while copying into a freshVec. Note it's a copy, not a view β the cofactor recursion needs each minor as an independent matrix. -
determinant(&self) -> f64β The practical algorithm, and the heart of the chapter. It clones the matrix and runs one elimination pass per column:- Find the pivot β the row at or below the diagonal with the largest $|$value$|$ in this column (same partial pivoting as ch06).
- Check for singularity β if every candidate pivot is ~0, the matrix collapses a direction: return
0.0immediately. This is the elimination view of "det = 0 β singular". - Swap, track sign β if the pivot row isn't already in place, swap and flip
sign. No row scaling happens, ever β scaling by $c$ would multiply the determinant by $c$ and corrupt the answer. - Eliminate below β subtract a multiple of the pivot row from each lower row. This operation provably leaves the determinant unchanged.
When the matrix is upper-triangular, the determinant is
signtimes the product of the diagonal. Each pass is O(nΒ²) work over n columns β O(nΒ³) total. -
Why
f64, notOption? β ch06'sinversereturnsOption<Matrix>because a singular matrix has no inverse. A determinant, by contrast, always exists for a square matrix β a singular matrix simply has determinant 0. Returning0.0is the honest, total answer, and it makesdet != 0the clean one-line invertibility test:a.determinant() != 0.0iffa.inverse().is_some(). -
The 1e-10 threshold β The same absolute threshold as ch06's inverse: it assumes a well-scaled matrix (entries of order 1). A matrix with all entries below ~1e-10 would be falsely reported singular β acceptable for a teaching implementation, and the kind of thing production
LAPACKhandles with relative thresholds. -
Two algorithms, one number β
determinant_cofactoris slow but definitional, which makes it the perfect test oracle: the 4Γ4 tests assert that elimination and cofactor expansion agree (det = 16 and det = 5), so either implementation bugging out is caught by the other.
Verification
| Test | What it checks | Mathematical invariant |
|---|---|---|
test_1x1_det_is_the_entry | 1Γ1 base case, both algorithms | $\det([a]) = a$ |
test_2x2_ad_minus_bc | Known 2Γ2 value | $\det\begin{pmatrix} a & b \\ c & d \end{pmatrix} = ad - bc$ |
test_2x2_row_swap_flips_sign | Swap flips sign | Row-swap property |
test_3x3_known_value | Hand-traced 3Γ3 cofactor expansion | Cofactor expansion correctness |
test_3x3_zero_pivot_requires_swap | Zero pivot rescued by row swap | Partial pivoting + sign tracking |
test_triangular_det_is_diagonal_product | Upper-triangular matrix | $\det$ = diagonal product |
test_identity_det_is_one | $\mathbf{I}_n$ for n = 1..5 | $\det(\mathbf{I}_n) = 1$ |
test_singular_det_is_zero | Duplicate row β 0 | $\det = 0 \iff$ singular |
test_4x4_cofactor_matches_elimination | Both algorithms agree; det = 16 (exact rational check) | Cofactor β‘ elimination |
test_4x4_band_matrix_matches_elimination | Tridiagonal 4Γ4; det = 5 | Cofactor β‘ elimination |
test_det_of_product_equals_product_of_dets | $\det(\mathbf{A}\mathbf{B})$ vs $\det(\mathbf{A})\det(\mathbf{B})$ | Multiplicativity |
test_non_square_elimination_panics | Non-square input panics | Determinants require square matrices |
test_non_square_cofactor_panics | Non-square input panics | Determinants require square matrices |
Key Takeaways
- The determinant is the signed area/volume-scaling factor of a matrix's transformation: $|\det(\mathbf{A})|$ is the factor, the sign is the orientation.
- $\det(\mathbf{A}) = 0$ exactly when $\mathbf{A}$ is singular β the determinant is a direct singularity test and the denominator in the ch06 inverse formulas.
- Cofactor expansion is the definitional algorithm: $\det(\mathbf{A}) = \sum_j a_{1j}C_{1j}$ with $C_{ij} = (-1)^{i+j}\det(M_{ij})$ β O(n!), only practical for tiny matrices.
- Gaussian elimination computes determinants in O(nΒ³): only two row ops are allowed (add-multiple: no change; swap: flip sign), then $\det$ = sign Γ diagonal product.
- Partial pivoting matters here exactly as in ch06 β a zero pivot in a perfectly invertible matrix (like $\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$, det = β1) is rescued by a row swap, with the sign flip tracked.
- Next: determinants are the doorway to rank and vector spaces (ch08) β a matrix's rows are linearly dependent exactly when its determinant is zero, which is the same collapse we saw geometrically in ch05βch07.