<div class="cg-wrap">
<div class="cg-top">
<div class="cg-left">
<div class="legend"><strong>Items to cover:</strong><span id="items-display"></span></div>
<div class="col-list-wrap">
<div class="col-list-label">Active columns (restricted LP):</div>
<div id="active-cols" class="col-list"></div>
</div>
<div class="col-list-wrap">
<div class="col-list-label">Pool (not yet added):</div>
<div id="pool-cols" class="col-list pool"></div>
</div>
</div>
<div class="cg-right">
<div class="result-box">
<div class="rb-title">LP solution</div>
<div id="rb-body">Press "Next iteration" to begin.</div>
</div>
<div class="pricing-box" id="pricing-box" style="display:none">
<div class="pb-title">Pricing step</div>
<div id="pb-body"></div>
</div>
</div>
</div>
<div class="btns">
<button id="btn-next" type="button">Next iteration</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="log" class="log"></div>
</div>
*{box-sizing:border-box;}
body{font-family:system-ui,sans-serif;color:#1a2233;margin:0;font-size:14px;}
.cg-wrap{padding:4px 0;}
.cg-top{display:flex;gap:14px;flex-wrap:wrap;}
.cg-left,.cg-right{flex:1;min-width:190px;}
.legend{margin-bottom:8px;font-size:13px;}
#items-display{display:inline-flex;gap:5px;flex-wrap:wrap;margin-left:6px;}
.item-chip{width:26px;height:26px;border-radius:50%;display:inline-flex;align-items:center;
justify-content:center;font:700 12px monospace;border:2px solid #1d3557;color:#1d3557;}
.item-chip.covered{background:#1d3557;color:#fff;}
.col-list-wrap{margin-bottom:9px;}
.col-list-label{font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.04em;margin-bottom:5px;}
.col-list{display:flex;flex-wrap:wrap;gap:5px;}
.col-chip{border-radius:6px;padding:4px 9px;font:600 12px monospace;cursor:default;
background:#e8eef3;border:1.5px solid #b0bec5;color:#1a2233;display:flex;align-items:center;gap:5px;}
.col-chip .cc-sub{font-size:10px;color:#546e7a;}
.col-chip.entering{background:#fff3cd;border-color:#f59e0b;animation:pulse .5s ease 2;}
.col-chip.in-basis{background:#d1fae5;border-color:#10b981;color:#064e3b;}
.col-chip.dropped{background:#fee2e2;border-color:#ef4444;color:#7f1d1d;text-decoration:line-through;}
@keyframes pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.1);}}
.pool .col-chip{background:#f5f5f5;border-color:#d1d5db;color:#9ca3af;}
.result-box,.pricing-box{border-radius:8px;padding:10px 13px;margin-bottom:10px;font-size:13px;line-height:1.7;}
.result-box{background:#eff6ff;border:1.5px solid #93c5fd;}
.pricing-box{background:#fffbeb;border:1.5px solid #fcd34d;}
.rb-title,.pb-title{font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;}
.rb-title{color:#1e40af;}.pb-title{color:#b45309;}
.row{margin:1px 0;}
.hi{font-weight:700;color:#1e40af;}
.hi-neg{font-weight:700;color:#b91c1c;}
.hi-ok{font-weight:700;color:#065f46;}
.btns{display:flex;gap:8px;margin:10px 0;}
button{font:600 13px system-ui;padding:.4rem 1rem;border:1.5px solid #1d3557;
background:#1d3557;color:#fff;border-radius:7px;cursor:pointer;}
button.ghost{background:#fff;color:#1d3557;}
button:disabled{opacity:.35;cursor:not-allowed;}
.log{font-size:11.5px;color:#4b5563;border-top:1px solid #e5e7eb;padding-top:6px;
max-height:86px;overflow-y:auto;line-height:1.8;}
.log-entry{padding:0;}
.done-banner{background:#d1fae5;border:1.5px solid #10b981;border-radius:8px;
padding:10px 14px;font-weight:700;color:#064e3b;margin-bottom:8px;font-size:13px;}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Column generation demo — set-cover LP on 4 items with 6 columns.
//
// Instance (verified by hand):
// Items: {1,2,3,4}
// Columns: P={1,2} cost 4 Q={3,4} cost 4
// R={1,3} cost 3 S={2,4} cost 3
// T={1,4} cost 2 U={2,3} cost 2
// Optimal: T+U = cost 4 (covers all 4 items)
// Initial basis: P+Q (expensive, cost 8)
//
// LP dual prices (verified):
// Iter 0: duals all 2 → RC(T)=-2, RC(U)=-2 (most negative), RC(R)=RC(S)=-1
// Iter 1: basis P,Q,T; duals y1=0,y2=4,y3=4,y4=0 → RC(U)=-6 (most negative)
// Iter 2: basis P,Q,T,U; solution xT=1,xU=1,xP=0,xQ=0; duals all 1
// RC(P)=2, RC(Q)=2, RC(R)=1, RC(S)=1 — all non-negative → OPTIMAL
const ITEMS = [1,2,3,4];
const ALL_COLS = [
{id:'P', items:[1,2], cost:4},
{id:'Q', items:[3,4], cost:4},
{id:'R', items:[1,3], cost:3},
{id:'S', items:[2,4], cost:3},
{id:'T', items:[1,4], cost:2},
{id:'U', items:[2,3], cost:2},
];
// Pre-verified LP states at each iteration.
// Each state is shown AFTER that iteration's LP solve and BEFORE/AT the pricing step.
const STATES = [
{
// Iteration 0: initial restricted LP with P,Q only.
active: ['P','Q'],
x: {P:1, Q:1},
obj: 8,
duals: {1:2, 2:2, 3:2, 4:2},
allRC: {P:0, Q:0, R:-1, S:-1, T:-2, U:-2},
enter: 'T', // most negative reduced cost
enterRC: -2,
note: 'Iter 0: basis {P,Q}, obj=8. Dual prices: all 2. Most negative RC: T and U both −2. Enter T.'
},
{
// Iteration 1: LP with P,Q,T. T has zero value; xP=1,xQ=1 still forced.
active: ['P','Q','T'],
x: {P:1, Q:1, T:0},
obj: 8,
duals: {1:0, 2:4, 3:4, 4:0},
allRC: {P:0, Q:0, R:-1, S:-1, T:2, U:-6},
enter: 'U', // RC=-6
enterRC: -6,
note: 'Iter 1: T entered but obj still 8 (items 2,3 force P,Q). RC(U)=−6 — most negative. Enter U.'
},
{
// Iteration 2: LP with P,Q,T,U. Optimal: xT=1,xU=1,xP=0,xQ=0 → cost 4.
active: ['P','Q','T','U'],
x: {P:0, Q:0, T:1, U:1},
obj: 4,
duals: {1:1, 2:1, 3:1, 4:1},
allRC: {P:2, Q:2, R:1, S:1, T:0, U:0},
enter: null, // no negative RC → optimal
enterRC: null,
note: 'Iter 2: U entered. Now xT=1,xU=1,xP=0,xQ=0 → obj=4. All reduced costs ≥ 0. OPTIMAL!'
}
];
function getCol(id){ return ALL_COLS.find(c=>c.id===id); }
function coveredItems(activeIds, xMap){
const cov = new Set();
for(const id of activeIds){
if((xMap[id]||0) > 1e-9) getCol(id).items.forEach(i=>cov.add(i));
}
return cov;
}
function renderItems(covSet){
document.getElementById('items-display').innerHTML =
ITEMS.map(i=>`<span class="item-chip${covSet.has(i)?' covered':''}">${i}</span>`).join('');
}
function renderActiveCols(active, xMap, entering){
document.getElementById('active-cols').innerHTML = active.map(id=>{
const col=getCol(id), xv=xMap[id]||0;
const cls = id===entering ? 'entering' : (xv>1e-9 ? 'in-basis' : 'dropped');
return fmt('<span class="col-chip %cls%" title="cost %cost%">%id%<span class="cc-sub">{%items%} $%cost%</span></span>', { cls: cls, cost: col.cost, id: id, items: col.items.join(',') });
}).join('');
}
function renderPool(active){
const el=document.getElementById('pool-cols');
const rest=ALL_COLS.filter(c=>!active.includes(c.id));
if(!rest.length){el.innerHTML='<em style="color:#bbb;font-size:11px">all columns considered</em>';return;}
el.innerHTML=rest.map(c=>fmt('<span class="col-chip" title="cost %cost%">%id%<span class="cc-sub">{%items%} $%cost%</span></span>', { cost: c.cost, id: c.id, items: c.items.join(',') })).join('');
}
function renderResult(st){
renderItems(coveredItems(st.active, st.x));
const xStr=st.active.filter(id=>(st.x[id]||0)>1e-9).map(id=>`x<sub>${id}</sub>=${st.x[id]}`).join(', ');
const dStr=Object.entries(st.duals).map(([i,v])=>`y<sub>${i}</sub>=${v}`).join(', ');
document.getElementById('rb-body').innerHTML=
fmt('<div class="row">Solution: %xStr%</div><div class="row">Objective: <span class="hi">%obj%</span></div><div class="row">Duals: %dStr%</div>', { xStr: xStr, obj: st.obj, dStr: dStr });
}
function renderPricing(st){
const box=document.getElementById('pricing-box');
const body=document.getElementById('pb-body');
box.style.display='block';
if(st.enter===null){
const rcLines=st.active.concat(ALL_COLS.filter(c=>!st.active.includes(c.id)).map(c=>c.id))
.map(id=>{const rc=st.allRC[id]; return `RC(${id})=${rc}`;}).join(', ');
body.innerHTML=fmt('<div class="row">%rcLines%</div><div class="row"><span class="hi-ok">All reduced costs ≥ 0. LP is optimal!</span></div>', { rcLines: rcLines });
return;
}
const col=getCol(st.enter);
const dsum=col.items.reduce((a,i)=>a+st.duals[i],0);
const rcLines=ALL_COLS.filter(c=>!st.active.includes(c.id))
.map(c=>{const rc=st.allRC[c.id];return `RC(${c.id})=<b>${rc}</b>`;}).join(' | ');
body.innerHTML=
fmt('<div class="row">Pool RCs: %rcLines%</div><div class="row">Best: <strong>%enter%</strong> = %cost%−(%map%) = %cost%−%dsum% = <span class="hi-neg">%enterRC%</span> → Add!</div>', { rcLines: rcLines, enter: st.enter, cost: col.cost, map: col.items.map(i=>`y<sub>${i}</sub>`).join('+'), dsum: dsum, enterRC: st.enterRC });
}
function addLog(msg){
const log=document.getElementById('log');
const d=document.createElement('div');d.className='log-entry';d.textContent=msg;
log.appendChild(d);log.scrollTop=log.scrollHeight;
}
let iter=0, done=false;
function step(){
if(done)return;
const st=STATES[iter];
renderActiveCols(st.active, st.x, st.enter);
renderPool(st.active);
renderResult(st);
renderPricing(st);
addLog(st.note);
if(st.enter===null){
done=true;
document.getElementById('btn-next').disabled=true;
const b=document.createElement('div');b.className='done-banner';
b.textContent=fmt('Optimal! No improving column exists. Total cost: %obj%.', { obj: st.obj });
const wrap=document.querySelector('.cg-wrap');
wrap.insertBefore(b,document.querySelector('.btns'));
} else {
iter++;
}
}
function reset(){
iter=0;done=false;
document.getElementById('btn-next').disabled=false;
document.getElementById('pricing-box').style.display='none';
document.getElementById('rb-body').innerHTML='Press "Next iteration" to begin.';
document.getElementById('log').innerHTML='';
const banner=document.querySelector('.done-banner');if(banner)banner.remove();
renderItems(new Set());
renderActiveCols(STATES[0].active,{},null);
renderPool(STATES[0].active);
}
document.getElementById('btn-next').addEventListener('click',step);
document.getElementById('btn-reset').addEventListener('click',reset);
reset();
<div class="cg-wrap">
<div class="cg-top">
<div class="cg-left">
<div class="legend"><strong>Items to cover:</strong><span id="items-display"></span></div>
<div class="col-list-wrap">
<div class="col-list-label">Active columns (restricted LP):</div>
<div id="active-cols" class="col-list"></div>
</div>
<div class="col-list-wrap">
<div class="col-list-label">Pool (not yet added):</div>
<div id="pool-cols" class="col-list pool"></div>
</div>
</div>
<div class="cg-right">
<div class="result-box">
<div class="rb-title">LP solution</div>
<div id="rb-body">Press "Next iteration" to begin.</div>
</div>
<div class="pricing-box" id="pricing-box" style="display:none">
<div class="pb-title">Pricing step</div>
<div id="pb-body"></div>
</div>
</div>
</div>
<div class="btns">
<button id="btn-next" type="button">Next iteration</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="log" class="log"></div>
</div>
*{box-sizing:border-box;}
body{font-family:system-ui,sans-serif;color:#1a2233;margin:0;font-size:14px;}
.cg-wrap{padding:4px 0;}
.cg-top{display:flex;gap:14px;flex-wrap:wrap;}
.cg-left,.cg-right{flex:1;min-width:190px;}
.legend{margin-bottom:8px;font-size:13px;}
#items-display{display:inline-flex;gap:5px;flex-wrap:wrap;margin-left:6px;}
.item-chip{width:26px;height:26px;border-radius:50%;display:inline-flex;align-items:center;
justify-content:center;font:700 12px monospace;border:2px solid #1d3557;color:#1d3557;}
.item-chip.covered{background:#1d3557;color:#fff;}
.col-list-wrap{margin-bottom:9px;}
.col-list-label{font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.04em;margin-bottom:5px;}
.col-list{display:flex;flex-wrap:wrap;gap:5px;}
.col-chip{border-radius:6px;padding:4px 9px;font:600 12px monospace;cursor:default;
background:#e8eef3;border:1.5px solid #b0bec5;color:#1a2233;display:flex;align-items:center;gap:5px;}
.col-chip .cc-sub{font-size:10px;color:#546e7a;}
.col-chip.entering{background:#fff3cd;border-color:#f59e0b;animation:pulse .5s ease 2;}
.col-chip.in-basis{background:#d1fae5;border-color:#10b981;color:#064e3b;}
.col-chip.dropped{background:#fee2e2;border-color:#ef4444;color:#7f1d1d;text-decoration:line-through;}
@keyframes pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.1);}}
.pool .col-chip{background:#f5f5f5;border-color:#d1d5db;color:#9ca3af;}
.result-box,.pricing-box{border-radius:8px;padding:10px 13px;margin-bottom:10px;font-size:13px;line-height:1.7;}
.result-box{background:#eff6ff;border:1.5px solid #93c5fd;}
.pricing-box{background:#fffbeb;border:1.5px solid #fcd34d;}
.rb-title,.pb-title{font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;}
.rb-title{color:#1e40af;}.pb-title{color:#b45309;}
.row{margin:1px 0;}
.hi{font-weight:700;color:#1e40af;}
.hi-neg{font-weight:700;color:#b91c1c;}
.hi-ok{font-weight:700;color:#065f46;}
.btns{display:flex;gap:8px;margin:10px 0;}
button{font:600 13px system-ui;padding:.4rem 1rem;border:1.5px solid #1d3557;
background:#1d3557;color:#fff;border-radius:7px;cursor:pointer;}
button.ghost{background:#fff;color:#1d3557;}
button:disabled{opacity:.35;cursor:not-allowed;}
.log{font-size:11.5px;color:#4b5563;border-top:1px solid #e5e7eb;padding-top:6px;
max-height:86px;overflow-y:auto;line-height:1.8;}
.log-entry{padding:0;}
.done-banner{background:#d1fae5;border:1.5px solid #10b981;border-radius:8px;
padding:10px 14px;font-weight:700;color:#064e3b;margin-bottom:8px;font-size:13px;}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Column generation demo — set-cover LP on 4 items with 6 columns.
//
// Instance (verified by hand):
// Items: {1,2,3,4}
// Columns: P={1,2} cost 4 Q={3,4} cost 4
// R={1,3} cost 3 S={2,4} cost 3
// T={1,4} cost 2 U={2,3} cost 2
// Optimal: T+U = cost 4 (covers all 4 items)
// Initial basis: P+Q (expensive, cost 8)
//
// LP dual prices (verified):
// Iter 0: duals all 2 → RC(T)=-2, RC(U)=-2 (most negative), RC(R)=RC(S)=-1
// Iter 1: basis P,Q,T; duals y1=0,y2=4,y3=4,y4=0 → RC(U)=-6 (most negative)
// Iter 2: basis P,Q,T,U; solution xT=1,xU=1,xP=0,xQ=0; duals all 1
// RC(P)=2, RC(Q)=2, RC(R)=1, RC(S)=1 — all non-negative → OPTIMAL
const ITEMS = [1,2,3,4];
const ALL_COLS = [
{id:'P', items:[1,2], cost:4},
{id:'Q', items:[3,4], cost:4},
{id:'R', items:[1,3], cost:3},
{id:'S', items:[2,4], cost:3},
{id:'T', items:[1,4], cost:2},
{id:'U', items:[2,3], cost:2},
];
// Pre-verified LP states at each iteration.
// Each state is shown AFTER that iteration's LP solve and BEFORE/AT the pricing step.
const STATES = [
{
// Iteration 0: initial restricted LP with P,Q only.
active: ['P','Q'],
x: {P:1, Q:1},
obj: 8,
duals: {1:2, 2:2, 3:2, 4:2},
allRC: {P:0, Q:0, R:-1, S:-1, T:-2, U:-2},
enter: 'T', // most negative reduced cost
enterRC: -2,
note: 'Iter 0: basis {P,Q}, obj=8. Dual prices: all 2. Most negative RC: T and U both −2. Enter T.'
},
{
// Iteration 1: LP with P,Q,T. T has zero value; xP=1,xQ=1 still forced.
active: ['P','Q','T'],
x: {P:1, Q:1, T:0},
obj: 8,
duals: {1:0, 2:4, 3:4, 4:0},
allRC: {P:0, Q:0, R:-1, S:-1, T:2, U:-6},
enter: 'U', // RC=-6
enterRC: -6,
note: 'Iter 1: T entered but obj still 8 (items 2,3 force P,Q). RC(U)=−6 — most negative. Enter U.'
},
{
// Iteration 2: LP with P,Q,T,U. Optimal: xT=1,xU=1,xP=0,xQ=0 → cost 4.
active: ['P','Q','T','U'],
x: {P:0, Q:0, T:1, U:1},
obj: 4,
duals: {1:1, 2:1, 3:1, 4:1},
allRC: {P:2, Q:2, R:1, S:1, T:0, U:0},
enter: null, // no negative RC → optimal
enterRC: null,
note: 'Iter 2: U entered. Now xT=1,xU=1,xP=0,xQ=0 → obj=4. All reduced costs ≥ 0. OPTIMAL!'
}
];
function getCol(id){ return ALL_COLS.find(c=>c.id===id); }
function coveredItems(activeIds, xMap){
const cov = new Set();
for(const id of activeIds){
if((xMap[id]||0) > 1e-9) getCol(id).items.forEach(i=>cov.add(i));
}
return cov;
}
function renderItems(covSet){
document.getElementById('items-display').innerHTML =
ITEMS.map(i=>`<span class="item-chip${covSet.has(i)?' covered':''}">${i}</span>`).join('');
}
function renderActiveCols(active, xMap, entering){
document.getElementById('active-cols').innerHTML = active.map(id=>{
const col=getCol(id), xv=xMap[id]||0;
const cls = id===entering ? 'entering' : (xv>1e-9 ? 'in-basis' : 'dropped');
return fmt('<span class="col-chip %cls%" title="cost %cost%">%id%<span class="cc-sub">{%items%} $%cost%</span></span>', { cls: cls, cost: col.cost, id: id, items: col.items.join(',') });
}).join('');
}
function renderPool(active){
const el=document.getElementById('pool-cols');
const rest=ALL_COLS.filter(c=>!active.includes(c.id));
if(!rest.length){el.innerHTML='<em style="color:#bbb;font-size:11px">all columns considered</em>';return;}
el.innerHTML=rest.map(c=>fmt('<span class="col-chip" title="cost %cost%">%id%<span class="cc-sub">{%items%} $%cost%</span></span>', { cost: c.cost, id: c.id, items: c.items.join(',') })).join('');
}
function renderResult(st){
renderItems(coveredItems(st.active, st.x));
const xStr=st.active.filter(id=>(st.x[id]||0)>1e-9).map(id=>`x<sub>${id}</sub>=${st.x[id]}`).join(', ');
const dStr=Object.entries(st.duals).map(([i,v])=>`y<sub>${i}</sub>=${v}`).join(', ');
document.getElementById('rb-body').innerHTML=
fmt('<div class="row">Solution: %xStr%</div><div class="row">Objective: <span class="hi">%obj%</span></div><div class="row">Duals: %dStr%</div>', { xStr: xStr, obj: st.obj, dStr: dStr });
}
function renderPricing(st){
const box=document.getElementById('pricing-box');
const body=document.getElementById('pb-body');
box.style.display='block';
if(st.enter===null){
const rcLines=st.active.concat(ALL_COLS.filter(c=>!st.active.includes(c.id)).map(c=>c.id))
.map(id=>{const rc=st.allRC[id]; return `RC(${id})=${rc}`;}).join(', ');
body.innerHTML=fmt('<div class="row">%rcLines%</div><div class="row"><span class="hi-ok">All reduced costs ≥ 0. LP is optimal!</span></div>', { rcLines: rcLines });
return;
}
const col=getCol(st.enter);
const dsum=col.items.reduce((a,i)=>a+st.duals[i],0);
const rcLines=ALL_COLS.filter(c=>!st.active.includes(c.id))
.map(c=>{const rc=st.allRC[c.id];return `RC(${c.id})=<b>${rc}</b>`;}).join(' | ');
body.innerHTML=
fmt('<div class="row">Pool RCs: %rcLines%</div><div class="row">Best: <strong>%enter%</strong> = %cost%−(%map%) = %cost%−%dsum% = <span class="hi-neg">%enterRC%</span> → Add!</div>', { rcLines: rcLines, enter: st.enter, cost: col.cost, map: col.items.map(i=>`y<sub>${i}</sub>`).join('+'), dsum: dsum, enterRC: st.enterRC });
}
function addLog(msg){
const log=document.getElementById('log');
const d=document.createElement('div');d.className='log-entry';d.textContent=msg;
log.appendChild(d);log.scrollTop=log.scrollHeight;
}
let iter=0, done=false;
function step(){
if(done)return;
const st=STATES[iter];
renderActiveCols(st.active, st.x, st.enter);
renderPool(st.active);
renderResult(st);
renderPricing(st);
addLog(st.note);
if(st.enter===null){
done=true;
document.getElementById('btn-next').disabled=true;
const b=document.createElement('div');b.className='done-banner';
b.textContent=fmt('Optimal! No improving column exists. Total cost: %obj%.', { obj: st.obj });
const wrap=document.querySelector('.cg-wrap');
wrap.insertBefore(b,document.querySelector('.btns'));
} else {
iter++;
}
}
function reset(){
iter=0;done=false;
document.getElementById('btn-next').disabled=false;
document.getElementById('pricing-box').style.display='none';
document.getElementById('rb-body').innerHTML='Press "Next iteration" to begin.';
document.getElementById('log').innerHTML='';
const banner=document.querySelector('.done-banner');if(banner)banner.remove();
renderItems(new Set());
renderActiveCols(STATES[0].active,{},null);
renderPool(STATES[0].active);
}
document.getElementById('btn-next').addEventListener('click',step);
document.getElementById('btn-reset').addEventListener('click',reset);
reset();
Comments
Loading comments...