A DP table often holds far more than it needs. If each cell only reads the last row (or the last one or two cells), every older row is dead weight once the next one is done. Keep only what the recurrence still reads, and an O(n · k) table becomes O(k) — or an O(n) array becomes two variables. Same answer, same time, a fraction of the memory.
When dp[i] reads only dp[i−1] and dp[i−2], the array can shrink to two variables that slide forward:
function fib(n) { if (n === 0) return 0; let prev = 0, cur = 1; // dp[i - 2] and dp[i - 1] for (let i = 2; i <= n; i++) [prev, cur] = [cur, prev + cur]; return cur;}Pascal's rule reads dp[i−1][j−1] and dp[i−1][j] — only the previous row. Keep a single row and update it in place: row[j] += row[j − 1]. The catch is the direction: row[j] needs the old row[j − 1]. Going right to left, row[j − 1] hasn't been updated yet when row[j] reads it. Going left to right would read a value from the new row and get the wrong answer.
One row is updated from right to left, so every cell still reads the previous row's values.
function binomial(n, k) { const row = new Array(k + 1).fill(0); row[0] = 1; // row 0: C(0, 0) = 1 for (let i = 1; i <= n; i++) { for (let j = Math.min(i, k); j >= 1; j--) { // right to left: row[j - 1] is still the old row row[j] += row[j - 1]; } } return row[k];}Time O(n · k), space O(k). The same right-to-left rule shows up in the 0/1 knapsack (each item used once) — and the opposite, left to right, in the unbounded knapsack (items reused), where reading the new row is exactly what you want. Both appear in Pattern 3.
prev and cur, swapped after each row. One row needs the right update direction.