Try it. Six donor-patient pairs are stuck — each donor is incompatible with their own patient. Below are the possible exchanges their compatibilities allow (2-way swaps and one 3-way cycle). Click to add an exchange to your plan; you can't reuse a pair, so choosing one swap may rule out another.
<p class="hint">Six donor–patient pairs, each internally incompatible. Their compatibilities allow the <b>exchanges</b> below. Click to add one to your plan — but a pair can only be in <b>one</b> exchange. Maximize <span class="g">transplants</span>, then Optimize.</p>
<div id="pairs" class="pairs"></div>
<h4 class="h">Possible exchanges</h4>
<div id="cands" class="cands"></div>
<div class="meter">
<div>Transplants: <b id="count" class="g">0</b> / 6</div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="opt" type="button">Optimize</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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 .8rem; line-height: 1.45; }
.hint .g { color: #0a7d33; font-weight: 700; }
.pairs { display: grid; grid-template-columns: repeat(6, 1fr); gap: .4rem; margin-bottom: .9rem; }
.pair { text-align: center; border: 2px solid #e2e6eb; border-radius: 8px; padding: .4rem .2rem; font: 700 13px system-ui; color: #555; background: #fff; }
.pair.on { border-color: #0a7d33; background: #e6f6ec; color: #0a7d33; }
.pair small { display: block; font-weight: 500; font-size: .8em; color: #999; }
.h { font: 700 13px system-ui; color: #1d3557; margin: .2rem 0 .5rem; }
.cands { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .9rem; }
.cand { border: 2px solid #457b9d; border-radius: 8px; padding: .5rem .7rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font: 600 14px system-ui; color: #1d3557; background: #fff; }
.cand:hover { background: #f0f4f8; }
.cand.sel { background: #457b9d; color: #fff; border-color: #457b9d; }
.cand.blocked { opacity: .4; cursor: not-allowed; border-color: #ccc; color: #999; }
.cand .badge { font: 700 12px system-ui; background: rgba(0,0,0,.08); border-radius: 99px; padding: .1rem .5rem; }
.cand.sel .badge { background: rgba(255,255,255,.25); }
.meter { display: flex; gap: 1.4rem; align-items: center; font-size: 1rem; margin-bottom: .7rem; flex-wrap: wrap; }
.meter .g { color: #0a7d33; }
.msg { font-weight: 700; color: #0a7d33; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kidney exchange: pick disjoint compatible cycles to maximize transplants.
// NP-hard with cycle-length limits. Greedy "biggest cycle first" is a trap:
// grabbing the 3-way here blocks a pair and yields only 5 of 6.
const PAIRS = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'];
// each exchange: label, member pair indices (its size = transplants it gives)
const CANDS = [
{ label: 'P1 ⇄ P2', members: [0, 1] },
{ label: 'P3 ⇄ P4', members: [2, 3] },
{ label: 'P5 ⇄ P6', members: [4, 5] },
{ label: 'P3 → P4 → P5 (3-way)', members: [2, 3, 4] }
];
const OPT = 6; // P1⇄P2 + P3⇄P4 + P5⇄P6 covers everyone
let chosen;
function reset() { chosen = []; render(); }
function covered(set) { const s = new Set(); set.forEach(ci => CANDS[ci].members.forEach(m => s.add(m))); return s; }
function conflicts(ci, set) {
const cov = covered(set);
return CANDS[ci].members.some(m => cov.has(m));
}
function toggle(ci) {
const i = chosen.indexOf(ci);
if (i >= 0) { chosen.splice(i, 1); render(); return; }
if (conflicts(ci, chosen)) return; // blocked
chosen.push(ci); render();
}
function render() {
const cov = covered(chosen);
document.getElementById('pairs').innerHTML = PAIRS.map((p, i) =>
'<div class="pair' + (cov.has(i) ? ' on' : '') + '">' + p + '<small>' + (cov.has(i) ? '✓' : '—') + '</small></div>').join('');
document.getElementById('cands').innerHTML = CANDS.map((c, ci) => {
const sel = chosen.includes(ci);
const blocked = !sel && conflicts(ci, chosen);
const cls = sel ? ' sel' : (blocked ? ' blocked' : '');
return '<div class="cand' + cls + '" data-ci="' + ci + '"><span>' + c.label +
'</span><span class="badge">+' + c.members.length + '</span></div>';
}).join('');
document.querySelectorAll('.cand').forEach(el => el.addEventListener('click', () => toggle(+el.dataset.ci)));
const n = cov.size;
document.getElementById('count').textContent = n;
const m = document.getElementById('msg');
if (n === OPT) { m.style.color = '#0a7d33'; m.textContent = '✓ Everyone transplanted — optimal!'; }
else if (chosen.includes(3)) { m.style.color = '#b9770e'; m.textContent = 'The 3-way looks big, but it strands a pair. Try three 2-way swaps.'; }
else { m.style.color = '#777'; m.textContent = fmt('%n% of 6 so far.', { n: n }); }
}
// Optimize: brute-force the best disjoint set of exchanges (max transplants).
function optimize() {
let best = [], bestN = 0;
for (let mask = 0; mask < (1 << CANDS.length); mask++) {
const set = [], seen = new Set(); let ok = true;
for (let ci = 0; ci < CANDS.length; ci++) if (mask & (1 << ci)) {
for (const m of CANDS[ci].members) { if (seen.has(m)) { ok = false; break; } seen.add(m); }
if (!ok) break; set.push(ci);
}
if (ok && seen.size > bestN) { bestN = seen.size; best = set; }
}
chosen = best; render();
}
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">Six donor–patient pairs, each internally incompatible. Their compatibilities allow the <b>exchanges</b> below. Click to add one to your plan — but a pair can only be in <b>one</b> exchange. Maximize <span class="g">transplants</span>, then Optimize.</p>
<div id="pairs" class="pairs"></div>
<h4 class="h">Possible exchanges</h4>
<div id="cands" class="cands"></div>
<div class="meter">
<div>Transplants: <b id="count" class="g">0</b> / 6</div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="opt" type="button">Optimize</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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 .8rem; line-height: 1.45; }
.hint .g { color: #0a7d33; font-weight: 700; }
.pairs { display: grid; grid-template-columns: repeat(6, 1fr); gap: .4rem; margin-bottom: .9rem; }
.pair { text-align: center; border: 2px solid #e2e6eb; border-radius: 8px; padding: .4rem .2rem; font: 700 13px system-ui; color: #555; background: #fff; }
.pair.on { border-color: #0a7d33; background: #e6f6ec; color: #0a7d33; }
.pair small { display: block; font-weight: 500; font-size: .8em; color: #999; }
.h { font: 700 13px system-ui; color: #1d3557; margin: .2rem 0 .5rem; }
.cands { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .9rem; }
.cand { border: 2px solid #457b9d; border-radius: 8px; padding: .5rem .7rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font: 600 14px system-ui; color: #1d3557; background: #fff; }
.cand:hover { background: #f0f4f8; }
.cand.sel { background: #457b9d; color: #fff; border-color: #457b9d; }
.cand.blocked { opacity: .4; cursor: not-allowed; border-color: #ccc; color: #999; }
.cand .badge { font: 700 12px system-ui; background: rgba(0,0,0,.08); border-radius: 99px; padding: .1rem .5rem; }
.cand.sel .badge { background: rgba(255,255,255,.25); }
.meter { display: flex; gap: 1.4rem; align-items: center; font-size: 1rem; margin-bottom: .7rem; flex-wrap: wrap; }
.meter .g { color: #0a7d33; }
.msg { font-weight: 700; color: #0a7d33; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kidney exchange: pick disjoint compatible cycles to maximize transplants.
// NP-hard with cycle-length limits. Greedy "biggest cycle first" is a trap:
// grabbing the 3-way here blocks a pair and yields only 5 of 6.
const PAIRS = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'];
// each exchange: label, member pair indices (its size = transplants it gives)
const CANDS = [
{ label: 'P1 ⇄ P2', members: [0, 1] },
{ label: 'P3 ⇄ P4', members: [2, 3] },
{ label: 'P5 ⇄ P6', members: [4, 5] },
{ label: 'P3 → P4 → P5 (3-way)', members: [2, 3, 4] }
];
const OPT = 6; // P1⇄P2 + P3⇄P4 + P5⇄P6 covers everyone
let chosen;
function reset() { chosen = []; render(); }
function covered(set) { const s = new Set(); set.forEach(ci => CANDS[ci].members.forEach(m => s.add(m))); return s; }
function conflicts(ci, set) {
const cov = covered(set);
return CANDS[ci].members.some(m => cov.has(m));
}
function toggle(ci) {
const i = chosen.indexOf(ci);
if (i >= 0) { chosen.splice(i, 1); render(); return; }
if (conflicts(ci, chosen)) return; // blocked
chosen.push(ci); render();
}
function render() {
const cov = covered(chosen);
document.getElementById('pairs').innerHTML = PAIRS.map((p, i) =>
'<div class="pair' + (cov.has(i) ? ' on' : '') + '">' + p + '<small>' + (cov.has(i) ? '✓' : '—') + '</small></div>').join('');
document.getElementById('cands').innerHTML = CANDS.map((c, ci) => {
const sel = chosen.includes(ci);
const blocked = !sel && conflicts(ci, chosen);
const cls = sel ? ' sel' : (blocked ? ' blocked' : '');
return '<div class="cand' + cls + '" data-ci="' + ci + '"><span>' + c.label +
'</span><span class="badge">+' + c.members.length + '</span></div>';
}).join('');
document.querySelectorAll('.cand').forEach(el => el.addEventListener('click', () => toggle(+el.dataset.ci)));
const n = cov.size;
document.getElementById('count').textContent = n;
const m = document.getElementById('msg');
if (n === OPT) { m.style.color = '#0a7d33'; m.textContent = '✓ Everyone transplanted — optimal!'; }
else if (chosen.includes(3)) { m.style.color = '#b9770e'; m.textContent = 'The 3-way looks big, but it strands a pair. Try three 2-way swaps.'; }
else { m.style.color = '#777'; m.textContent = fmt('%n% of 6 so far.', { n: n }); }
}
// Optimize: brute-force the best disjoint set of exchanges (max transplants).
function optimize() {
let best = [], bestN = 0;
for (let mask = 0; mask < (1 << CANDS.length); mask++) {
const set = [], seen = new Set(); let ok = true;
for (let ci = 0; ci < CANDS.length; ci++) if (mask & (1 << ci)) {
for (const m of CANDS[ci].members) { if (seen.has(m)) { ok = false; break; } seen.add(m); }
if (!ok) break; set.push(ci);
}
if (ok && seen.size > bestN) { bestN = seen.size; best = set; }
}
chosen = best; render();
}
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', reset);
reset();
Maximize the transplants. Then hit Optimize. Watch out: grabbing the biggest cycle first — the tempting 3-way — can actually leave fewer people transplanted than three careful 2-way swaps. That trap is exactly why this needs real optimization.
Comments
Loading comments...