<p class="hint">
Choose an array size, then run both approaches. Sequential visits elements one by one — <b>n steps</b>. Parallel prefix-sum uses a tree of doublings — <b>⌈log₂ n⌉ rounds</b>.
</p>
<div class="controls">
<label>Array size: <select id="sizeSelect">
<option value="4">4</option>
<option value="8" selected>8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
</select></label>
<button id="btnSeq" type="button">▶ Run sequential</button>
<button id="btnPar" type="button">▶ Run parallel</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div id="arrayViz" class="array-viz"></div>
<div id="treeViz" class="tree-viz"></div>
<div id="statsBox" class="stats-box">
<span id="statSeq" class="stat">Sequential steps: —</span>
<span id="statPar" class="stat">Parallel rounds: —</span>
</div>
<div class="status" id="status">Pick a size and run an algorithm.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { font-size: .9rem; }
select { font-size: .9rem; border: 1px solid #adb1b8; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.array-viz { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: .5rem; }
.cell { min-width: 36px; 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 .3s; }
.cell.active { background: #f4a261; border-color: #e07b3c; color: #fff; }
.cell.done { background: #2a9d8f; border-color: #1e7b6d; color: #fff; }
.tree-viz { font-size: .78rem; color: #555; margin-bottom: .5rem; min-height: 1.2em; }
.tree-viz .round { margin: .15rem 0; }
.tree-viz .round-label { font-weight: 700; color: #1d3557; margin-right: .4rem; }
.stats-box { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .4rem; }
.stat { font-size: .9rem; font-weight: 600; }
#statSeq { color: #e63946; }
#statPar { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// NC vs P demo: compare sequential summation (n steps) vs
// parallel prefix-sum (ceil(log2 n) rounds, each touching n/2 pairs).
// This illustrates why prefix-sum is in NC: O(log n) depth, O(n) work.
let arr = [], n = 8;
let animTimer = null;
function randomArr(size) {
return Array.from({ length: size }, () => Math.floor(Math.random() * 9) + 1);
}
function resetAll() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
n = parseInt(document.getElementById('sizeSelect').value, 10);
arr = randomArr(n);
document.getElementById('statSeq').textContent = 'Sequential steps: —';
document.getElementById('statPar').textContent = 'Parallel rounds: —';
document.getElementById('status').textContent = 'Pick a size and run an algorithm.';
document.getElementById('status').className = 'status';
document.getElementById('treeViz').innerHTML = '';
renderArray(arr, []);
}
function renderArray(values, highlighted) {
const viz = document.getElementById('arrayViz');
viz.innerHTML = '';
values.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'cell' + (highlighted.includes(i) ? ' active' : '');
d.textContent = v;
viz.appendChild(d);
});
}
function markDone(indices) {
const cells = document.getElementById('arrayViz').querySelectorAll('.cell');
indices.forEach(i => { if (cells[i]) cells[i].className = 'cell done'; });
}
// Sequential: animate visiting each index, accumulating sum
function runSequential() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
resetAll();
let step = 0, acc = arr[0];
renderArray(arr, [0]);
markDone([0]);
document.getElementById('statSeq').textContent = fmt('Sequential steps: 1 / %n%', { n: n });
document.getElementById('status').textContent = fmt('Running sequential… step 1 of %n%, running sum = %acc%', { n: n, acc: acc });
function tick() {
step++;
if (step >= n) {
document.getElementById('statSeq').textContent = fmt('Sequential steps: %n%', { n: n });
document.getElementById('status').textContent = fmt('Sequential done: sum = %acc% in %n% steps.', { acc: acc, n: n });
document.getElementById('status').className = 'status ok';
renderArray(arr, []);
markDone(arr.map((_, i) => i));
return;
}
acc += arr[step];
renderArray(arr, [step]);
// Re-mark done
const cells = document.getElementById('arrayViz').querySelectorAll('.cell');
for (let i = 0; i < step; i++) { if (cells[i]) cells[i].className = 'cell done'; }
document.getElementById('statSeq').textContent = fmt('Sequential steps: %v% / %n%', { v: step + 1, n: n });
document.getElementById('status').textContent = fmt('Running sequential… step %v% of %n%, running sum = %acc%', { v: step + 1, n: n, acc: acc });
animTimer = setTimeout(tick, 400);
}
animTimer = setTimeout(tick, 400);
}
// Parallel prefix-sum (Blelloch up-sweep) animated by rounds.
// In round r (stride = 2^r), the right child of each pair is at
// index i = stride*2 - 1, stride*4 - 1, … (i.e. i starts at stride*2-1,
// step stride*2). work[i] += work[i - stride].
function runParallel() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
resetAll();
const rounds = Math.ceil(Math.log2(n));
const treeDiv = document.getElementById('treeViz');
treeDiv.innerHTML = '';
// Build rounds list using the correct Blelloch up-sweep indices.
const roundPairs = [];
for (let r = 0; r < rounds; r++) {
const stride = 1 << r;
const pairs = [];
for (let i = stride * 2 - 1; i < n; i += stride * 2) {
pairs.push([i - stride, i]);
}
roundPairs.push(pairs);
}
// Build working array (we show prefix sums building up)
const work = arr.slice();
let roundIdx = 0;
function doRound() {
if (roundIdx >= rounds) {
document.getElementById('statPar').textContent = fmt('Parallel rounds: %rounds% (log₂ %n% = %rounds%)', { rounds: rounds, n: n });
document.getElementById('status').textContent = fmt('Parallel done: %rounds% rounds — total sum = %v%.', { rounds: rounds, v: work[n - 1] });
document.getElementById('status').className = 'status ok';
renderArray(work, []);
markDone(work.map((_, i) => i));
return;
}
const stride = 1 << roundIdx;
const pairs = roundPairs[roundIdx];
// Show active cells
const active = pairs.flatMap(([a, b]) => [a, b]);
renderArray(work, active);
// Apply sums
pairs.forEach(([a, b]) => { work[b] = work[a] + work[b]; });
// Log round
const rowEl = document.createElement('div');
rowEl.className = 'round';
rowEl.innerHTML = fmt('<span class="round-label">Round %v% (stride %stride%):</span>%map%', { v: roundIdx + 1, stride: stride, map: pairs.map(([a, b]) => `[${a}]+[${b}]`).join(' ') });
treeDiv.appendChild(rowEl);
document.getElementById('statPar').textContent = fmt('Parallel rounds: %v% / %rounds%', { v: roundIdx + 1, rounds: rounds });
document.getElementById('status').textContent = fmt('Round %v% of %rounds% — combining %length% pair(s) simultaneously.', { v: roundIdx + 1, rounds: rounds, length: pairs.length });
roundIdx++;
animTimer = setTimeout(doRound, 700);
}
renderArray(arr, []);
document.getElementById('status').textContent = fmt('Starting parallel prefix-sum on %n% elements…', { n: n });
animTimer = setTimeout(doRound, 500);
}
document.getElementById('sizeSelect').addEventListener('change', resetAll);
document.getElementById('btnSeq').addEventListener('click', runSequential);
document.getElementById('btnPar').addEventListener('click', runParallel);
document.getElementById('btnReset').addEventListener('click', resetAll);
// Verify: parallel prefix-sum (Blelloch up-sweep) must yield same total as sequential.
(function selfTest() {
const testArr = [3, 1, 4, 1, 5, 9, 2, 6];
const seqSum = testArr.reduce((a, b) => a + b, 0);
const work = testArr.slice();
const n2 = testArr.length;
const rounds2 = Math.ceil(Math.log2(n2));
for (let r = 0; r < rounds2; r++) {
const stride = 1 << r;
// Right child is at stride*2-1, stride*4-1, … (Blelloch up-sweep)
for (let i = stride * 2 - 1; i < n2; i += stride * 2) {
work[i] = work[i - stride] + work[i];
}
}
if (work[n2 - 1] !== seqSum) {
console.error('Self-test FAILED: expected', seqSum, 'got', work[n2 - 1]);
} else {
console.log('Self-test passed: parallel prefix-sum total =', work[n2 - 1]);
}
})();
resetAll();
<p class="hint">
Choose an array size, then run both approaches. Sequential visits elements one by one — <b>n steps</b>. Parallel prefix-sum uses a tree of doublings — <b>⌈log₂ n⌉ rounds</b>.
</p>
<div class="controls">
<label>Array size: <select id="sizeSelect">
<option value="4">4</option>
<option value="8" selected>8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
</select></label>
<button id="btnSeq" type="button">▶ Run sequential</button>
<button id="btnPar" type="button">▶ Run parallel</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div id="arrayViz" class="array-viz"></div>
<div id="treeViz" class="tree-viz"></div>
<div id="statsBox" class="stats-box">
<span id="statSeq" class="stat">Sequential steps: —</span>
<span id="statPar" class="stat">Parallel rounds: —</span>
</div>
<div class="status" id="status">Pick a size and run an algorithm.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { font-size: .9rem; }
select { font-size: .9rem; border: 1px solid #adb1b8; border-radius: 6px; padding: .2rem .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.array-viz { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: .5rem; }
.cell { min-width: 36px; 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 .3s; }
.cell.active { background: #f4a261; border-color: #e07b3c; color: #fff; }
.cell.done { background: #2a9d8f; border-color: #1e7b6d; color: #fff; }
.tree-viz { font-size: .78rem; color: #555; margin-bottom: .5rem; min-height: 1.2em; }
.tree-viz .round { margin: .15rem 0; }
.tree-viz .round-label { font-weight: 700; color: #1d3557; margin-right: .4rem; }
.stats-box { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .4rem; }
.stat { font-size: .9rem; font-weight: 600; }
#statSeq { color: #e63946; }
#statPar { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// NC vs P demo: compare sequential summation (n steps) vs
// parallel prefix-sum (ceil(log2 n) rounds, each touching n/2 pairs).
// This illustrates why prefix-sum is in NC: O(log n) depth, O(n) work.
let arr = [], n = 8;
let animTimer = null;
function randomArr(size) {
return Array.from({ length: size }, () => Math.floor(Math.random() * 9) + 1);
}
function resetAll() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
n = parseInt(document.getElementById('sizeSelect').value, 10);
arr = randomArr(n);
document.getElementById('statSeq').textContent = 'Sequential steps: —';
document.getElementById('statPar').textContent = 'Parallel rounds: —';
document.getElementById('status').textContent = 'Pick a size and run an algorithm.';
document.getElementById('status').className = 'status';
document.getElementById('treeViz').innerHTML = '';
renderArray(arr, []);
}
function renderArray(values, highlighted) {
const viz = document.getElementById('arrayViz');
viz.innerHTML = '';
values.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'cell' + (highlighted.includes(i) ? ' active' : '');
d.textContent = v;
viz.appendChild(d);
});
}
function markDone(indices) {
const cells = document.getElementById('arrayViz').querySelectorAll('.cell');
indices.forEach(i => { if (cells[i]) cells[i].className = 'cell done'; });
}
// Sequential: animate visiting each index, accumulating sum
function runSequential() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
resetAll();
let step = 0, acc = arr[0];
renderArray(arr, [0]);
markDone([0]);
document.getElementById('statSeq').textContent = fmt('Sequential steps: 1 / %n%', { n: n });
document.getElementById('status').textContent = fmt('Running sequential… step 1 of %n%, running sum = %acc%', { n: n, acc: acc });
function tick() {
step++;
if (step >= n) {
document.getElementById('statSeq').textContent = fmt('Sequential steps: %n%', { n: n });
document.getElementById('status').textContent = fmt('Sequential done: sum = %acc% in %n% steps.', { acc: acc, n: n });
document.getElementById('status').className = 'status ok';
renderArray(arr, []);
markDone(arr.map((_, i) => i));
return;
}
acc += arr[step];
renderArray(arr, [step]);
// Re-mark done
const cells = document.getElementById('arrayViz').querySelectorAll('.cell');
for (let i = 0; i < step; i++) { if (cells[i]) cells[i].className = 'cell done'; }
document.getElementById('statSeq').textContent = fmt('Sequential steps: %v% / %n%', { v: step + 1, n: n });
document.getElementById('status').textContent = fmt('Running sequential… step %v% of %n%, running sum = %acc%', { v: step + 1, n: n, acc: acc });
animTimer = setTimeout(tick, 400);
}
animTimer = setTimeout(tick, 400);
}
// Parallel prefix-sum (Blelloch up-sweep) animated by rounds.
// In round r (stride = 2^r), the right child of each pair is at
// index i = stride*2 - 1, stride*4 - 1, … (i.e. i starts at stride*2-1,
// step stride*2). work[i] += work[i - stride].
function runParallel() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
resetAll();
const rounds = Math.ceil(Math.log2(n));
const treeDiv = document.getElementById('treeViz');
treeDiv.innerHTML = '';
// Build rounds list using the correct Blelloch up-sweep indices.
const roundPairs = [];
for (let r = 0; r < rounds; r++) {
const stride = 1 << r;
const pairs = [];
for (let i = stride * 2 - 1; i < n; i += stride * 2) {
pairs.push([i - stride, i]);
}
roundPairs.push(pairs);
}
// Build working array (we show prefix sums building up)
const work = arr.slice();
let roundIdx = 0;
function doRound() {
if (roundIdx >= rounds) {
document.getElementById('statPar').textContent = fmt('Parallel rounds: %rounds% (log₂ %n% = %rounds%)', { rounds: rounds, n: n });
document.getElementById('status').textContent = fmt('Parallel done: %rounds% rounds — total sum = %v%.', { rounds: rounds, v: work[n - 1] });
document.getElementById('status').className = 'status ok';
renderArray(work, []);
markDone(work.map((_, i) => i));
return;
}
const stride = 1 << roundIdx;
const pairs = roundPairs[roundIdx];
// Show active cells
const active = pairs.flatMap(([a, b]) => [a, b]);
renderArray(work, active);
// Apply sums
pairs.forEach(([a, b]) => { work[b] = work[a] + work[b]; });
// Log round
const rowEl = document.createElement('div');
rowEl.className = 'round';
rowEl.innerHTML = fmt('<span class="round-label">Round %v% (stride %stride%):</span>%map%', { v: roundIdx + 1, stride: stride, map: pairs.map(([a, b]) => `[${a}]+[${b}]`).join(' ') });
treeDiv.appendChild(rowEl);
document.getElementById('statPar').textContent = fmt('Parallel rounds: %v% / %rounds%', { v: roundIdx + 1, rounds: rounds });
document.getElementById('status').textContent = fmt('Round %v% of %rounds% — combining %length% pair(s) simultaneously.', { v: roundIdx + 1, rounds: rounds, length: pairs.length });
roundIdx++;
animTimer = setTimeout(doRound, 700);
}
renderArray(arr, []);
document.getElementById('status').textContent = fmt('Starting parallel prefix-sum on %n% elements…', { n: n });
animTimer = setTimeout(doRound, 500);
}
document.getElementById('sizeSelect').addEventListener('change', resetAll);
document.getElementById('btnSeq').addEventListener('click', runSequential);
document.getElementById('btnPar').addEventListener('click', runParallel);
document.getElementById('btnReset').addEventListener('click', resetAll);
// Verify: parallel prefix-sum (Blelloch up-sweep) must yield same total as sequential.
(function selfTest() {
const testArr = [3, 1, 4, 1, 5, 9, 2, 6];
const seqSum = testArr.reduce((a, b) => a + b, 0);
const work = testArr.slice();
const n2 = testArr.length;
const rounds2 = Math.ceil(Math.log2(n2));
for (let r = 0; r < rounds2; r++) {
const stride = 1 << r;
// Right child is at stride*2-1, stride*4-1, … (Blelloch up-sweep)
for (let i = stride * 2 - 1; i < n2; i += stride * 2) {
work[i] = work[i - stride] + work[i];
}
}
if (work[n2 - 1] !== seqSum) {
console.error('Self-test FAILED: expected', seqSum, 'got', work[n2 - 1]);
} else {
console.log('Self-test passed: parallel prefix-sum total =', work[n2 - 1]);
}
})();
resetAll();
Comments
Loading comments...