The demo below lets you build a small 3-SAT formula over three Boolean variables (x, y, z). Each clause picks three literals; you choose which variables appear and whether each is negated. The engine then counts every satisfying assignment by enumerating all 23 = 8 possible truth-value combinations.
<p class="hint">Build a 3-SAT formula over <b>x, y, z</b>. Each clause is a disjunction of 3 literals. Toggle the <b>¬</b> buttons to negate a variable. The engine counts <em>every</em> satisfying assignment by exhaustive search — 2³ = 8 combinations.</p>
<div id="clauses"></div>
<div class="btns">
<button id="addClause" type="button">+ Add clause</button>
<button id="countBtn" type="button">Count solutions</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
<div id="table" class="truth-table"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
#clauses { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .75rem; }
.clause { display: flex; align-items: center; gap: .4rem; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .4rem .6rem; flex-wrap: wrap; }
.clause-num { font-size: .75rem; color: #666; min-width: 1.5rem; }
.lit { display: flex; align-items: center; gap: .2rem; }
.lit-label { font: 600 14px ui-monospace, monospace; color: #1d3557; }
.neg-btn { font-size: .75rem; padding: .15rem .35rem; border-radius: 5px; border: 1px solid #aaa;
background: #fff; cursor: pointer; color: #555; transition: all .1s; }
.neg-btn.active { background: #e63946; border-color: #c92f3c; color: #fff; }
.var-sel { font: 600 13px ui-monospace, monospace; padding: .2rem .3rem; border: 1px solid #aaa;
border-radius: 5px; background: #fff; color: #1d3557; }
.clause-sep { font-weight: 700; color: #999; }
.del-btn { margin-left: auto; font-size: .8rem; padding: .15rem .45rem; border-radius: 5px;
border: 1px solid #aaa; background: #fff; color: #888; cursor: pointer; }
.del-btn:hover { background: #fce; border-color: #e63946; color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.4em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.zero { color: #c92f3c; }
.result.info { color: #1d3557; }
.truth-table { font-size: .82rem; overflow-x: auto; }
table { border-collapse: collapse; }
th, td { padding: .25rem .55rem; border: 1px solid #d0d8e4; text-align: center; }
th { background: #dbe6f0; font-weight: 700; }
tr.sat { background: #d4f1de; }
tr.unsat { background: #fde8e8; }
td.mono { font-family: ui-monospace, monospace; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// #P / Counting Hierarchy demo
// Formula: conjunction of clauses; each clause is a disjunction of 3 literals.
// Variables: x=0, y=1, z=2.
const VARS = ['x', 'y', 'z'];
// Each clause: array of 3 {varIdx, neg} objects
let clauses = [
[{varIdx:0,neg:false},{varIdx:1,neg:true},{varIdx:2,neg:false}],
[{varIdx:0,neg:true},{varIdx:1,neg:false},{varIdx:2,neg:true}]
];
function renderClauses() {
const container = document.getElementById('clauses');
container.innerHTML = '';
clauses.forEach((clause, ci) => {
const row = document.createElement('div');
row.className = 'clause';
const num = document.createElement('span');
num.className = 'clause-num';
num.textContent = 'C' + (ci + 1);
row.appendChild(num);
clause.forEach((lit, li) => {
if (li > 0) {
const sep = document.createElement('span');
sep.className = 'clause-sep';
sep.textContent = '∨';
row.appendChild(sep);
}
const litDiv = document.createElement('div');
litDiv.className = 'lit';
// negation toggle
const negBtn = document.createElement('button');
negBtn.className = 'neg-btn' + (lit.neg ? ' active' : '');
negBtn.textContent = '¬';
negBtn.title = 'Toggle negation';
negBtn.addEventListener('click', () => {
clauses[ci][li].neg = !clauses[ci][li].neg;
renderClauses();
clearResult();
});
// variable selector
const sel = document.createElement('select');
sel.className = 'var-sel';
VARS.forEach((v, vi) => {
const opt = document.createElement('option');
opt.value = vi;
opt.textContent = v;
if (vi === lit.varIdx) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('change', () => {
clauses[ci][li].varIdx = parseInt(sel.value);
clearResult();
});
const label = document.createElement('span');
label.className = 'lit-label';
label.textContent = VARS[lit.varIdx];
litDiv.appendChild(negBtn);
litDiv.appendChild(sel);
row.appendChild(litDiv);
});
const del = document.createElement('button');
del.className = 'del-btn';
del.textContent = '✕';
del.title = 'Remove clause';
del.addEventListener('click', () => { clauses.splice(ci, 1); renderClauses(); clearResult(); });
row.appendChild(del);
container.appendChild(row);
});
}
function clearResult() {
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
document.getElementById('table').innerHTML = '';
}
function evalClause(clause, vals) {
return clause.some(lit => {
const v = vals[lit.varIdx];
return lit.neg ? !v : v;
});
}
function evalFormula(vals) {
return clauses.every(c => evalClause(c, vals));
}
function countSolutions() {
if (clauses.length === 0) {
showResult('Add at least one clause first.', 'info');
return;
}
const n = VARS.length; // 3
const total = 1 << n; // 8
const rows = [];
let count = 0;
for (let mask = 0; mask < total; mask++) {
// bit i = variable i's value (0=false, 1=true), LSB = x
const vals = VARS.map((_, i) => !!(mask & (1 << i)));
const sat = evalFormula(vals);
if (sat) count++;
rows.push({ vals, sat });
}
// Display summary
const half = total / 2; // 4
const ppAnswer = count > half ? 'YES (majority)' : 'NO (minority)';
const parityAnswer = (count % 2 === 1) ? 'ODD' : 'EVEN';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result ' + (count > 0 ? 'ok' : 'zero');
resultDiv.innerHTML =
fmt('<b>#SAT count: %count% / 8</b> | PP oracle: %ppAnswer% | ⊕P oracle: %parityAnswer%', { count: count, ppAnswer: ppAnswer, parityAnswer: parityAnswer });
// Build truth table
const tableDiv = document.getElementById('table');
const tbl = document.createElement('table');
const header = document.createElement('tr');
['x','y','z','Formula','Sat?'].forEach(h => {
const th = document.createElement('th'); th.textContent = h; header.appendChild(th);
});
tbl.appendChild(header);
rows.forEach(({ vals, sat }) => {
const tr = document.createElement('tr');
tr.className = sat ? 'sat' : 'unsat';
vals.forEach(v => {
const td = document.createElement('td'); td.className = 'mono';
td.textContent = v ? 'T' : 'F'; tr.appendChild(td);
});
// formula text
const fmlaTd = document.createElement('td');
fmlaTd.style.textAlign = 'left';
fmlaTd.style.fontSize = '.78rem';
fmlaTd.textContent = clauses.map((c, ci) =>
'(' + c.map(l => (l.neg ? '¬' : '') + VARS[l.varIdx]).join('∨') + ')'
).join(' ∧ ');
tr.appendChild(fmlaTd);
const resTd = document.createElement('td');
resTd.textContent = sat ? '✓' : '✗';
resTd.style.fontWeight = '700';
resTd.style.color = sat ? '#0a7d33' : '#c92f3c';
tr.appendChild(resTd);
tbl.appendChild(tr);
});
tableDiv.appendChild(tbl);
console.log('Count:', count, '| PP (majority):', count > half, '| parity odd:', count%2===1);
}
function showResult(msg, cls) {
const r = document.getElementById('result');
r.textContent = msg; r.className = 'result ' + cls;
}
document.getElementById('addClause').addEventListener('click', () => {
clauses.push([{varIdx:0,neg:false},{varIdx:1,neg:false},{varIdx:2,neg:false}]);
renderClauses(); clearResult();
});
document.getElementById('countBtn').addEventListener('click', countSolutions);
document.getElementById('resetBtn').addEventListener('click', () => {
clauses = [
[{varIdx:0,neg:false},{varIdx:1,neg:true},{varIdx:2,neg:false}],
[{varIdx:0,neg:true},{varIdx:1,neg:false},{varIdx:2,neg:true}]
];
renderClauses(); clearResult();
});
renderClauses();
<p class="hint">Build a 3-SAT formula over <b>x, y, z</b>. Each clause is a disjunction of 3 literals. Toggle the <b>¬</b> buttons to negate a variable. The engine counts <em>every</em> satisfying assignment by exhaustive search — 2³ = 8 combinations.</p>
<div id="clauses"></div>
<div class="btns">
<button id="addClause" type="button">+ Add clause</button>
<button id="countBtn" type="button">Count solutions</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
<div id="table" class="truth-table"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
#clauses { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .75rem; }
.clause { display: flex; align-items: center; gap: .4rem; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .4rem .6rem; flex-wrap: wrap; }
.clause-num { font-size: .75rem; color: #666; min-width: 1.5rem; }
.lit { display: flex; align-items: center; gap: .2rem; }
.lit-label { font: 600 14px ui-monospace, monospace; color: #1d3557; }
.neg-btn { font-size: .75rem; padding: .15rem .35rem; border-radius: 5px; border: 1px solid #aaa;
background: #fff; cursor: pointer; color: #555; transition: all .1s; }
.neg-btn.active { background: #e63946; border-color: #c92f3c; color: #fff; }
.var-sel { font: 600 13px ui-monospace, monospace; padding: .2rem .3rem; border: 1px solid #aaa;
border-radius: 5px; background: #fff; color: #1d3557; }
.clause-sep { font-weight: 700; color: #999; }
.del-btn { margin-left: auto; font-size: .8rem; padding: .15rem .45rem; border-radius: 5px;
border: 1px solid #aaa; background: #fff; color: #888; cursor: pointer; }
.del-btn:hover { background: #fce; border-color: #e63946; color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.4em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.zero { color: #c92f3c; }
.result.info { color: #1d3557; }
.truth-table { font-size: .82rem; overflow-x: auto; }
table { border-collapse: collapse; }
th, td { padding: .25rem .55rem; border: 1px solid #d0d8e4; text-align: center; }
th { background: #dbe6f0; font-weight: 700; }
tr.sat { background: #d4f1de; }
tr.unsat { background: #fde8e8; }
td.mono { font-family: ui-monospace, monospace; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// #P / Counting Hierarchy demo
// Formula: conjunction of clauses; each clause is a disjunction of 3 literals.
// Variables: x=0, y=1, z=2.
const VARS = ['x', 'y', 'z'];
// Each clause: array of 3 {varIdx, neg} objects
let clauses = [
[{varIdx:0,neg:false},{varIdx:1,neg:true},{varIdx:2,neg:false}],
[{varIdx:0,neg:true},{varIdx:1,neg:false},{varIdx:2,neg:true}]
];
function renderClauses() {
const container = document.getElementById('clauses');
container.innerHTML = '';
clauses.forEach((clause, ci) => {
const row = document.createElement('div');
row.className = 'clause';
const num = document.createElement('span');
num.className = 'clause-num';
num.textContent = 'C' + (ci + 1);
row.appendChild(num);
clause.forEach((lit, li) => {
if (li > 0) {
const sep = document.createElement('span');
sep.className = 'clause-sep';
sep.textContent = '∨';
row.appendChild(sep);
}
const litDiv = document.createElement('div');
litDiv.className = 'lit';
// negation toggle
const negBtn = document.createElement('button');
negBtn.className = 'neg-btn' + (lit.neg ? ' active' : '');
negBtn.textContent = '¬';
negBtn.title = 'Toggle negation';
negBtn.addEventListener('click', () => {
clauses[ci][li].neg = !clauses[ci][li].neg;
renderClauses();
clearResult();
});
// variable selector
const sel = document.createElement('select');
sel.className = 'var-sel';
VARS.forEach((v, vi) => {
const opt = document.createElement('option');
opt.value = vi;
opt.textContent = v;
if (vi === lit.varIdx) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('change', () => {
clauses[ci][li].varIdx = parseInt(sel.value);
clearResult();
});
const label = document.createElement('span');
label.className = 'lit-label';
label.textContent = VARS[lit.varIdx];
litDiv.appendChild(negBtn);
litDiv.appendChild(sel);
row.appendChild(litDiv);
});
const del = document.createElement('button');
del.className = 'del-btn';
del.textContent = '✕';
del.title = 'Remove clause';
del.addEventListener('click', () => { clauses.splice(ci, 1); renderClauses(); clearResult(); });
row.appendChild(del);
container.appendChild(row);
});
}
function clearResult() {
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
document.getElementById('table').innerHTML = '';
}
function evalClause(clause, vals) {
return clause.some(lit => {
const v = vals[lit.varIdx];
return lit.neg ? !v : v;
});
}
function evalFormula(vals) {
return clauses.every(c => evalClause(c, vals));
}
function countSolutions() {
if (clauses.length === 0) {
showResult('Add at least one clause first.', 'info');
return;
}
const n = VARS.length; // 3
const total = 1 << n; // 8
const rows = [];
let count = 0;
for (let mask = 0; mask < total; mask++) {
// bit i = variable i's value (0=false, 1=true), LSB = x
const vals = VARS.map((_, i) => !!(mask & (1 << i)));
const sat = evalFormula(vals);
if (sat) count++;
rows.push({ vals, sat });
}
// Display summary
const half = total / 2; // 4
const ppAnswer = count > half ? 'YES (majority)' : 'NO (minority)';
const parityAnswer = (count % 2 === 1) ? 'ODD' : 'EVEN';
const resultDiv = document.getElementById('result');
resultDiv.className = 'result ' + (count > 0 ? 'ok' : 'zero');
resultDiv.innerHTML =
fmt('<b>#SAT count: %count% / 8</b> | PP oracle: %ppAnswer% | ⊕P oracle: %parityAnswer%', { count: count, ppAnswer: ppAnswer, parityAnswer: parityAnswer });
// Build truth table
const tableDiv = document.getElementById('table');
const tbl = document.createElement('table');
const header = document.createElement('tr');
['x','y','z','Formula','Sat?'].forEach(h => {
const th = document.createElement('th'); th.textContent = h; header.appendChild(th);
});
tbl.appendChild(header);
rows.forEach(({ vals, sat }) => {
const tr = document.createElement('tr');
tr.className = sat ? 'sat' : 'unsat';
vals.forEach(v => {
const td = document.createElement('td'); td.className = 'mono';
td.textContent = v ? 'T' : 'F'; tr.appendChild(td);
});
// formula text
const fmlaTd = document.createElement('td');
fmlaTd.style.textAlign = 'left';
fmlaTd.style.fontSize = '.78rem';
fmlaTd.textContent = clauses.map((c, ci) =>
'(' + c.map(l => (l.neg ? '¬' : '') + VARS[l.varIdx]).join('∨') + ')'
).join(' ∧ ');
tr.appendChild(fmlaTd);
const resTd = document.createElement('td');
resTd.textContent = sat ? '✓' : '✗';
resTd.style.fontWeight = '700';
resTd.style.color = sat ? '#0a7d33' : '#c92f3c';
tr.appendChild(resTd);
tbl.appendChild(tr);
});
tableDiv.appendChild(tbl);
console.log('Count:', count, '| PP (majority):', count > half, '| parity odd:', count%2===1);
}
function showResult(msg, cls) {
const r = document.getElementById('result');
r.textContent = msg; r.className = 'result ' + cls;
}
document.getElementById('addClause').addEventListener('click', () => {
clauses.push([{varIdx:0,neg:false},{varIdx:1,neg:false},{varIdx:2,neg:false}]);
renderClauses(); clearResult();
});
document.getElementById('countBtn').addEventListener('click', countSolutions);
document.getElementById('resetBtn').addEventListener('click', () => {
clauses = [
[{varIdx:0,neg:false},{varIdx:1,neg:true},{varIdx:2,neg:false}],
[{varIdx:0,neg:true},{varIdx:1,neg:false},{varIdx:2,neg:true}]
];
renderClauses(); clearResult();
});
renderClauses();
Comments
Loading comments...