The demo below shows a 5-equation tridiagonal system. The three diagonals are labeled a (lower), b (main), c (upper), and d is the right-hand side.
<!-- Thomas algorithm demo — forward sweep + back substitution on a tridiagonal system -->
<p class="hint">A 5-equation tridiagonal system. Columns <b>a</b>, <b>b</b>, <b>c</b> are the lower, main, and upper diagonals; <b>d</b> is the right-hand side. Click <b>Forward sweep</b> once per row to eliminate the lower diagonal. Then use <b>Back-substitute</b> to recover the solution.</p>
<div id="matrix-display" class="matrix-display"></div>
<div class="status" id="status">Ready — click Forward sweep to start.</div>
<div class="btns">
<button id="btn-sweep" type="button">Forward sweep</button>
<button id="btn-back" type="button" disabled>Back-substitute</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the tridiagonal system display */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.matrix-display { display: grid; grid-template-columns: auto auto auto auto auto; gap: 3px 6px; margin: .5rem 0 .6rem; width: fit-content; }
.col-label { font: 700 11px ui-monospace, monospace; color: #888; text-align: center; padding-bottom: 2px; }
.row-label { font: 700 11px ui-monospace, monospace; color: #888; display: flex; align-items: center; padding-right: 2px; }
.cell { width: 54px; height: 36px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 6px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; transition: background .25s, color .25s; }
.cell.zero { color: #bbb; background: #f4f6f8; }
.cell.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.cell.changed { background: #d4edda; color: #0a7d33; border-color: #a8d5b5; }
.cell.solution { background: #fff3cd; color: #856404; border-color: #ffc107; }
.cell.rhs { background: #f0e8f8; color: #5a1d9e; border-color: #c4a5e8; }
.cell.rhs.changed { background: #e8d5f8; }
.cell.rhs.solution { background: #fff3cd; color: #856404; border-color: #ffc107; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.5em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.status.done { color: #856404; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Thomas algorithm: solve Ax=d where A is tridiagonal in O(n) time.
// Forward sweep: eliminate the lower diagonal, row by row.
// Back substitution: recover the solution from bottom to top.
// Original tridiagonal system (4,-1 diagonals). Solution is x=[1,1,1,1,1].
const ORIG = {
a: [0, -1, -1, -1, -1],
b: [4, 4, 4, 4, 4],
c: [-1, -1, -1, -1, 0],
d: [3, 2, 2, 2, 3]
};
// Verify: 4*1 + (-1)*1 = 3, (-1)*1 + 4*1 + (-1)*1 = 2, etc.
// x = [1, 1, 1, 1, 1]
const N = 5;
let a, b, c, d, x;
let step = 0; // 0 = not started, k = rows 1..k-1 have been eliminated
function clone(orig) {
return { a: [...orig.a], b: [...orig.b], c: [...orig.c], d: [...orig.d] };
}
function reset() {
({ a, b, c, d } = clone(ORIG));
x = new Array(N).fill(null);
step = 0;
document.getElementById('btn-sweep').disabled = false;
document.getElementById('btn-back').disabled = true;
setStatus('Ready — click Forward sweep to start.', '');
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
// Render the current state of the coefficient matrix and right-hand side.
function render() {
const el = document.getElementById('matrix-display');
el.innerHTML = '';
const labels = ['', 'a', 'b', 'c', 'd'];
labels.forEach(lbl => {
const h = document.createElement('div');
h.className = 'col-label'; h.textContent = lbl; el.appendChild(h);
});
for (let i = 0; i < N; i++) {
const rowLabel = document.createElement('div');
rowLabel.className = 'row-label';
rowLabel.textContent = i18nFmt('Row %i%', { i: i + 1 });
el.appendChild(rowLabel);
const vals = [a[i], b[i], c[i], d[i]];
const changed = step > 0 && i === step - 1 && step <= N - 1;
const solved = x[i] !== null;
vals.forEach((v, col) => {
const cell = document.createElement('div');
const isRhs = col === 3;
let cls = 'cell' + (isRhs ? ' rhs' : '');
if (v === 0 && !isRhs) cls += ' zero';
if (changed) cls += ' changed';
if (solved) cls = 'cell' + (isRhs ? ' rhs' : '') + ' solution';
if (step === i && step < N - 1 && !solved) cls += ' active';
cell.className = cls;
cell.textContent = solved && isRhs ? x[i].toFixed(2) : v.toFixed(2);
el.appendChild(cell);
});
}
}
// Forward sweep — one step per click.
function forwardSweep() {
if (step >= N - 1) return;
const i = step + 1; // eliminate row i using row i-1
const w = a[i] / b[i - 1];
b[i] = b[i] - w * c[i - 1];
d[i] = d[i] - w * d[i - 1];
a[i] = 0;
step++;
render();
if (step === N - 1) {
document.getElementById('btn-sweep').disabled = true;
document.getElementById('btn-back').disabled = false;
setStatus('Forward sweep done. Now click Back-substitute.', 'info');
} else {
setStatus(i18nFmt('Eliminated row %i% — keep sweeping.', { i: step }), 'info');
}
}
// Back substitution — one unknown per click.
function backSubstitute() {
if (x[N - 1] === null) {
x[N - 1] = d[N - 1] / b[N - 1];
}
let solved = 0;
for (let i = N - 2; i >= 0; i--) {
if (x[i] === null) { x[i] = (d[i] - c[i] * x[i + 1]) / b[i]; solved++; break; }
}
const allDone = x.every(v => v !== null);
render();
if (allDone) {
document.getElementById('btn-back').disabled = true;
const sol = x.map(v => v.toFixed(2)).join(', ');
setStatus(i18nFmt('Solution: x = [%sol%]', { sol: sol }), 'done');
} else {
const filled = x.filter(v => v !== null).length;
setStatus(i18nFmt('Back-substituted %filled% of %n% unknowns.', { filled: filled, n: N }), 'ok');
}
}
document.getElementById('btn-sweep').addEventListener('click', forwardSweep);
document.getElementById('btn-back').addEventListener('click', backSubstitute);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Thomas algorithm demo — forward sweep + back substitution on a tridiagonal system -->
<p class="hint">A 5-equation tridiagonal system. Columns <b>a</b>, <b>b</b>, <b>c</b> are the lower, main, and upper diagonals; <b>d</b> is the right-hand side. Click <b>Forward sweep</b> once per row to eliminate the lower diagonal. Then use <b>Back-substitute</b> to recover the solution.</p>
<div id="matrix-display" class="matrix-display"></div>
<div class="status" id="status">Ready — click Forward sweep to start.</div>
<div class="btns">
<button id="btn-sweep" type="button">Forward sweep</button>
<button id="btn-back" type="button" disabled>Back-substitute</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the tridiagonal system display */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.matrix-display { display: grid; grid-template-columns: auto auto auto auto auto; gap: 3px 6px; margin: .5rem 0 .6rem; width: fit-content; }
.col-label { font: 700 11px ui-monospace, monospace; color: #888; text-align: center; padding-bottom: 2px; }
.row-label { font: 700 11px ui-monospace, monospace; color: #888; display: flex; align-items: center; padding-right: 2px; }
.cell { width: 54px; height: 36px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 6px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; transition: background .25s, color .25s; }
.cell.zero { color: #bbb; background: #f4f6f8; }
.cell.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.cell.changed { background: #d4edda; color: #0a7d33; border-color: #a8d5b5; }
.cell.solution { background: #fff3cd; color: #856404; border-color: #ffc107; }
.cell.rhs { background: #f0e8f8; color: #5a1d9e; border-color: #c4a5e8; }
.cell.rhs.changed { background: #e8d5f8; }
.cell.rhs.solution { background: #fff3cd; color: #856404; border-color: #ffc107; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.5em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.status.done { color: #856404; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Thomas algorithm: solve Ax=d where A is tridiagonal in O(n) time.
// Forward sweep: eliminate the lower diagonal, row by row.
// Back substitution: recover the solution from bottom to top.
// Original tridiagonal system (4,-1 diagonals). Solution is x=[1,1,1,1,1].
const ORIG = {
a: [0, -1, -1, -1, -1],
b: [4, 4, 4, 4, 4],
c: [-1, -1, -1, -1, 0],
d: [3, 2, 2, 2, 3]
};
// Verify: 4*1 + (-1)*1 = 3, (-1)*1 + 4*1 + (-1)*1 = 2, etc.
// x = [1, 1, 1, 1, 1]
const N = 5;
let a, b, c, d, x;
let step = 0; // 0 = not started, k = rows 1..k-1 have been eliminated
function clone(orig) {
return { a: [...orig.a], b: [...orig.b], c: [...orig.c], d: [...orig.d] };
}
function reset() {
({ a, b, c, d } = clone(ORIG));
x = new Array(N).fill(null);
step = 0;
document.getElementById('btn-sweep').disabled = false;
document.getElementById('btn-back').disabled = true;
setStatus('Ready — click Forward sweep to start.', '');
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
// Render the current state of the coefficient matrix and right-hand side.
function render() {
const el = document.getElementById('matrix-display');
el.innerHTML = '';
const labels = ['', 'a', 'b', 'c', 'd'];
labels.forEach(lbl => {
const h = document.createElement('div');
h.className = 'col-label'; h.textContent = lbl; el.appendChild(h);
});
for (let i = 0; i < N; i++) {
const rowLabel = document.createElement('div');
rowLabel.className = 'row-label';
rowLabel.textContent = i18nFmt('Row %i%', { i: i + 1 });
el.appendChild(rowLabel);
const vals = [a[i], b[i], c[i], d[i]];
const changed = step > 0 && i === step - 1 && step <= N - 1;
const solved = x[i] !== null;
vals.forEach((v, col) => {
const cell = document.createElement('div');
const isRhs = col === 3;
let cls = 'cell' + (isRhs ? ' rhs' : '');
if (v === 0 && !isRhs) cls += ' zero';
if (changed) cls += ' changed';
if (solved) cls = 'cell' + (isRhs ? ' rhs' : '') + ' solution';
if (step === i && step < N - 1 && !solved) cls += ' active';
cell.className = cls;
cell.textContent = solved && isRhs ? x[i].toFixed(2) : v.toFixed(2);
el.appendChild(cell);
});
}
}
// Forward sweep — one step per click.
function forwardSweep() {
if (step >= N - 1) return;
const i = step + 1; // eliminate row i using row i-1
const w = a[i] / b[i - 1];
b[i] = b[i] - w * c[i - 1];
d[i] = d[i] - w * d[i - 1];
a[i] = 0;
step++;
render();
if (step === N - 1) {
document.getElementById('btn-sweep').disabled = true;
document.getElementById('btn-back').disabled = false;
setStatus('Forward sweep done. Now click Back-substitute.', 'info');
} else {
setStatus(i18nFmt('Eliminated row %i% — keep sweeping.', { i: step }), 'info');
}
}
// Back substitution — one unknown per click.
function backSubstitute() {
if (x[N - 1] === null) {
x[N - 1] = d[N - 1] / b[N - 1];
}
let solved = 0;
for (let i = N - 2; i >= 0; i--) {
if (x[i] === null) { x[i] = (d[i] - c[i] * x[i + 1]) / b[i]; solved++; break; }
}
const allDone = x.every(v => v !== null);
render();
if (allDone) {
document.getElementById('btn-back').disabled = true;
const sol = x.map(v => v.toFixed(2)).join(', ');
setStatus(i18nFmt('Solution: x = [%sol%]', { sol: sol }), 'done');
} else {
const filled = x.filter(v => v !== null).length;
setStatus(i18nFmt('Back-substituted %filled% of %n% unknowns.', { filled: filled, n: N }), 'ok');
}
}
document.getElementById('btn-sweep').addEventListener('click', forwardSweep);
document.getElementById('btn-back').addEventListener('click', backSubstitute);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Notice that no row ever touches anything outside its immediate neighbors — that is the structural miracle that makes the whole thing O(n).
Comments
Loading comments...