The network below has 20 nodes. Each directed edge carries a fixed propagation probability. Pick a budget k with the slider, then press Run greedy to let the algorithm pick seeds one at a time, always choosing the node that adds the most expected spread. Press Run random to compare against k randomly chosen seeds.
<!-- Influence Maximization demo: greedy seed selection vs random selection. -->
<div class="controls">
<label>Budget k = <span id="k-val">3</span>
<input type="range" id="k-slider" min="1" max="6" value="3">
</label>
<div class="btns">
<button id="btn-greedy" type="button">Run greedy</button>
<button id="btn-random" type="button">Run random</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="graph-canvas" width="480" height="280"></canvas>
<div class="legend">
<span class="dot seed-g"></span>Greedy seed
<span class="dot seed-r"></span>Random seed
<span class="dot active"></span>Reached
<span class="dot idle"></span>Idle
</div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .5rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 90px; accent-color: #1d6fa8; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 10px; background: #f0f4f8;
max-width: 100%; height: auto; border: 1px solid #d0d8e0; }
.legend { display: flex; flex-wrap: wrap; gap: .5rem 1rem; font-size: .8rem;
align-items: center; margin-top: .4rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%;
border: 1.5px solid #555; margin-right: 3px; }
.dot.seed-g { background: #f4a261; border-color: #c1440e; }
.dot.seed-r { background: #e63946; border-color: #a31f2a; }
.dot.active { background: #4cc9f0; border-color: #1a7fa8; }
.dot.idle { background: #cdd9e3; border-color: #8fa0ae; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.status.ok { color: #0a7d33; }
.status.tie { color: #555; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 20-node directed social network.
// Each node has (id, x, y) for layout.
const NODES = [
{ id: 0, x: 60, y: 60 },
{ id: 1, x: 160, y: 40 },
{ id: 2, x: 260, y: 60 },
{ id: 3, x: 360, y: 50 },
{ id: 4, x: 430, y: 120 },
{ id: 5, x: 100, y: 130 },
{ id: 6, x: 200, y: 120 },
{ id: 7, x: 310, y: 130 },
{ id: 8, x: 420, y: 200 },
{ id: 9, x: 50, y: 200 },
{ id: 10, x: 150, y: 200 },
{ id: 11, x: 250, y: 200 },
{ id: 12, x: 340, y: 210 },
{ id: 13, x: 110, y: 260 },
{ id: 14, x: 220, y: 255 },
{ id: 15, x: 320, y: 260 },
{ id: 16, x: 430, y: 265 },
{ id: 17, x: 60, y: 260 },
{ id: 18, x: 180, y: 140 },
{ id: 19, x: 390, y: 160 }
];
// Directed edges: [source, target, propagation-probability].
const EDGES = [
[0,1,0.7],[0,5,0.5],[1,2,0.6],[1,6,0.5],[2,3,0.6],[2,7,0.5],
[3,4,0.7],[3,7,0.4],[4,8,0.6],[4,19,0.5],[5,9,0.5],[5,10,0.6],
[5,6,0.4],[6,10,0.5],[6,11,0.5],[7,11,0.6],[7,12,0.5],[8,12,0.7],
[8,16,0.5],[9,13,0.6],[9,17,0.5],[10,13,0.5],[10,14,0.6],
[11,14,0.5],[11,15,0.6],[12,15,0.5],[12,16,0.6],[13,17,0.4],
[14,15,0.4],[15,16,0.4],[18,6,0.6],[18,10,0.5],[19,12,0.6],[1,18,0.5]
];
// Build adjacency list.
const ADJ = {};
NODES.forEach(n => ADJ[n.id] = []);
EDGES.forEach(([u,v,p]) => ADJ[u].push({ to: v, p }));
// Estimate expected spread via Monte Carlo simulation.
function estimateSpread(seeds, trials) {
let total = 0;
for (let t = 0; t < trials; t++) {
const reached = new Set(seeds);
const queue = [...seeds];
while (queue.length) {
const u = queue.shift();
for (const { to, p } of ADJ[u]) {
if (!reached.has(to) && Math.random() < p) {
reached.add(to);
queue.push(to);
}
}
}
total += reached.size;
}
return total / trials;
}
// Greedy seed selection: pick the node with largest marginal gain each round.
function greedySeeds(k) {
const chosen = [];
for (let i = 0; i < k; i++) {
let best = -1, bestScore = -1;
for (const n of NODES) {
if (chosen.includes(n.id)) continue;
const score = estimateSpread([...chosen, n.id], 200);
if (score > bestScore) { bestScore = score; best = n.id; }
}
chosen.push(best);
}
return chosen;
}
// Random seed selection: pick k nodes uniformly at random.
function randomSeeds(k) {
const pool = NODES.map(n => n.id).sort(() => Math.random() - 0.5);
return pool.slice(0, k);
}
// Run one cascade simulation to visualize the actual reached set.
function simulateFull(seeds) {
const reached = new Set(seeds);
const queue = [...seeds];
while (queue.length) {
const u = queue.shift();
for (const { to, p } of ADJ[u]) {
if (!reached.has(to) && Math.random() < p) {
reached.add(to);
queue.push(to);
}
}
}
return reached;
}
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
let state = { greedySeeds: [], greedyReached: new Set(), randomSeeds: [], randomReached: new Set(), mode: 'idle' };
// Render the graph on the canvas.
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw edges first (below nodes).
for (const [u, v, p] of EDGES) {
const a = NODES[u], b = NODES[v];
ctx.beginPath();
ctx.strokeStyle = `rgba(100,120,140,${0.25 + p * 0.35})`;
ctx.lineWidth = 1 + p;
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
ctx.stroke();
// Draw arrowhead.
const angle = Math.atan2(b.y - a.y, b.x - a.x);
const ax = b.x - 9 * Math.cos(angle), ay = b.y - 9 * Math.sin(angle);
ctx.beginPath();
ctx.fillStyle = `rgba(100,120,140,${0.4 + p * 0.3})`;
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 6 * Math.cos(angle - 0.4), ay - 6 * Math.sin(angle - 0.4));
ctx.lineTo(ax - 6 * Math.cos(angle + 0.4), ay - 6 * Math.sin(angle + 0.4));
ctx.fill();
}
// Draw nodes on top.
for (const n of NODES) {
const isGSeed = state.greedySeeds.includes(n.id);
const isRSeed = state.randomSeeds.includes(n.id);
const gReached = state.greedyReached.has(n.id);
const rReached = state.randomReached.has(n.id);
let fill = '#cdd9e3', stroke = '#8fa0ae';
if (isGSeed) { fill = '#f4a261'; stroke = '#c1440e'; }
else if (isRSeed) { fill = '#e63946'; stroke = '#a31f2a'; }
else if (gReached || rReached) { fill = '#4cc9f0'; stroke = '#1a7fa8'; }
ctx.beginPath();
ctx.arc(n.x, n.y, 11, 0, 2 * Math.PI);
ctx.fillStyle = fill; ctx.fill();
ctx.strokeStyle = stroke; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#333'; ctx.font = '9px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(n.id, n.x, n.y);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + cls;
}
const kSlider = document.getElementById('k-slider');
const kVal = document.getElementById('k-val');
kSlider.addEventListener('input', () => { kVal.textContent = kSlider.value; });
document.getElementById('btn-greedy').addEventListener('click', () => {
const k = +kSlider.value;
setStatus('Computing greedy seeds...', 'tie');
// Defer so the browser can paint the running message first.
setTimeout(() => {
const seeds = greedySeeds(k);
const reached = simulateFull(seeds);
state = { greedySeeds: seeds, greedyReached: reached, randomSeeds: [], randomReached: new Set(), mode: 'greedy' };
draw();
setStatus(i18nFmt('Greedy k=%k%: reached %n% nodes.', { k, n: reached.size }), 'ok');
}, 10);
});
document.getElementById('btn-random').addEventListener('click', () => {
const k = +kSlider.value;
const seeds = randomSeeds(k);
const reached = simulateFull(seeds);
const prev = state;
state = { greedySeeds: prev.greedySeeds, greedyReached: prev.greedyReached, randomSeeds: seeds, randomReached: reached, mode: 'both' };
draw();
if (prev.greedySeeds.length === 0) {
setStatus(i18nFmt('Random k=%k%: reached %n% nodes. Run greedy to compare.', { k, n: reached.size }), 'tie');
} else {
const gSize = prev.greedyReached.size;
const cls = gSize >= reached.size ? 'ok' : 'bad';
setStatus(i18nFmt('Greedy reached %gn% nodes · Random reached %rn% nodes.', { gn: gSize, rn: reached.size }), cls);
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
state = { greedySeeds: [], greedyReached: new Set(), randomSeeds: [], randomReached: new Set(), mode: 'idle' };
draw();
setStatus('', '');
});
draw();
<!-- Influence Maximization demo: greedy seed selection vs random selection. -->
<div class="controls">
<label>Budget k = <span id="k-val">3</span>
<input type="range" id="k-slider" min="1" max="6" value="3">
</label>
<div class="btns">
<button id="btn-greedy" type="button">Run greedy</button>
<button id="btn-random" type="button">Run random</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="graph-canvas" width="480" height="280"></canvas>
<div class="legend">
<span class="dot seed-g"></span>Greedy seed
<span class="dot seed-r"></span>Random seed
<span class="dot active"></span>Reached
<span class="dot idle"></span>Idle
</div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .5rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 90px; accent-color: #1d6fa8; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 10px; background: #f0f4f8;
max-width: 100%; height: auto; border: 1px solid #d0d8e0; }
.legend { display: flex; flex-wrap: wrap; gap: .5rem 1rem; font-size: .8rem;
align-items: center; margin-top: .4rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%;
border: 1.5px solid #555; margin-right: 3px; }
.dot.seed-g { background: #f4a261; border-color: #c1440e; }
.dot.seed-r { background: #e63946; border-color: #a31f2a; }
.dot.active { background: #4cc9f0; border-color: #1a7fa8; }
.dot.idle { background: #cdd9e3; border-color: #8fa0ae; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.status.ok { color: #0a7d33; }
.status.tie { color: #555; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 20-node directed social network.
// Each node has (id, x, y) for layout.
const NODES = [
{ id: 0, x: 60, y: 60 },
{ id: 1, x: 160, y: 40 },
{ id: 2, x: 260, y: 60 },
{ id: 3, x: 360, y: 50 },
{ id: 4, x: 430, y: 120 },
{ id: 5, x: 100, y: 130 },
{ id: 6, x: 200, y: 120 },
{ id: 7, x: 310, y: 130 },
{ id: 8, x: 420, y: 200 },
{ id: 9, x: 50, y: 200 },
{ id: 10, x: 150, y: 200 },
{ id: 11, x: 250, y: 200 },
{ id: 12, x: 340, y: 210 },
{ id: 13, x: 110, y: 260 },
{ id: 14, x: 220, y: 255 },
{ id: 15, x: 320, y: 260 },
{ id: 16, x: 430, y: 265 },
{ id: 17, x: 60, y: 260 },
{ id: 18, x: 180, y: 140 },
{ id: 19, x: 390, y: 160 }
];
// Directed edges: [source, target, propagation-probability].
const EDGES = [
[0,1,0.7],[0,5,0.5],[1,2,0.6],[1,6,0.5],[2,3,0.6],[2,7,0.5],
[3,4,0.7],[3,7,0.4],[4,8,0.6],[4,19,0.5],[5,9,0.5],[5,10,0.6],
[5,6,0.4],[6,10,0.5],[6,11,0.5],[7,11,0.6],[7,12,0.5],[8,12,0.7],
[8,16,0.5],[9,13,0.6],[9,17,0.5],[10,13,0.5],[10,14,0.6],
[11,14,0.5],[11,15,0.6],[12,15,0.5],[12,16,0.6],[13,17,0.4],
[14,15,0.4],[15,16,0.4],[18,6,0.6],[18,10,0.5],[19,12,0.6],[1,18,0.5]
];
// Build adjacency list.
const ADJ = {};
NODES.forEach(n => ADJ[n.id] = []);
EDGES.forEach(([u,v,p]) => ADJ[u].push({ to: v, p }));
// Estimate expected spread via Monte Carlo simulation.
function estimateSpread(seeds, trials) {
let total = 0;
for (let t = 0; t < trials; t++) {
const reached = new Set(seeds);
const queue = [...seeds];
while (queue.length) {
const u = queue.shift();
for (const { to, p } of ADJ[u]) {
if (!reached.has(to) && Math.random() < p) {
reached.add(to);
queue.push(to);
}
}
}
total += reached.size;
}
return total / trials;
}
// Greedy seed selection: pick the node with largest marginal gain each round.
function greedySeeds(k) {
const chosen = [];
for (let i = 0; i < k; i++) {
let best = -1, bestScore = -1;
for (const n of NODES) {
if (chosen.includes(n.id)) continue;
const score = estimateSpread([...chosen, n.id], 200);
if (score > bestScore) { bestScore = score; best = n.id; }
}
chosen.push(best);
}
return chosen;
}
// Random seed selection: pick k nodes uniformly at random.
function randomSeeds(k) {
const pool = NODES.map(n => n.id).sort(() => Math.random() - 0.5);
return pool.slice(0, k);
}
// Run one cascade simulation to visualize the actual reached set.
function simulateFull(seeds) {
const reached = new Set(seeds);
const queue = [...seeds];
while (queue.length) {
const u = queue.shift();
for (const { to, p } of ADJ[u]) {
if (!reached.has(to) && Math.random() < p) {
reached.add(to);
queue.push(to);
}
}
}
return reached;
}
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
let state = { greedySeeds: [], greedyReached: new Set(), randomSeeds: [], randomReached: new Set(), mode: 'idle' };
// Render the graph on the canvas.
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw edges first (below nodes).
for (const [u, v, p] of EDGES) {
const a = NODES[u], b = NODES[v];
ctx.beginPath();
ctx.strokeStyle = `rgba(100,120,140,${0.25 + p * 0.35})`;
ctx.lineWidth = 1 + p;
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
ctx.stroke();
// Draw arrowhead.
const angle = Math.atan2(b.y - a.y, b.x - a.x);
const ax = b.x - 9 * Math.cos(angle), ay = b.y - 9 * Math.sin(angle);
ctx.beginPath();
ctx.fillStyle = `rgba(100,120,140,${0.4 + p * 0.3})`;
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 6 * Math.cos(angle - 0.4), ay - 6 * Math.sin(angle - 0.4));
ctx.lineTo(ax - 6 * Math.cos(angle + 0.4), ay - 6 * Math.sin(angle + 0.4));
ctx.fill();
}
// Draw nodes on top.
for (const n of NODES) {
const isGSeed = state.greedySeeds.includes(n.id);
const isRSeed = state.randomSeeds.includes(n.id);
const gReached = state.greedyReached.has(n.id);
const rReached = state.randomReached.has(n.id);
let fill = '#cdd9e3', stroke = '#8fa0ae';
if (isGSeed) { fill = '#f4a261'; stroke = '#c1440e'; }
else if (isRSeed) { fill = '#e63946'; stroke = '#a31f2a'; }
else if (gReached || rReached) { fill = '#4cc9f0'; stroke = '#1a7fa8'; }
ctx.beginPath();
ctx.arc(n.x, n.y, 11, 0, 2 * Math.PI);
ctx.fillStyle = fill; ctx.fill();
ctx.strokeStyle = stroke; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#333'; ctx.font = '9px system-ui';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(n.id, n.x, n.y);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + cls;
}
const kSlider = document.getElementById('k-slider');
const kVal = document.getElementById('k-val');
kSlider.addEventListener('input', () => { kVal.textContent = kSlider.value; });
document.getElementById('btn-greedy').addEventListener('click', () => {
const k = +kSlider.value;
setStatus('Computing greedy seeds...', 'tie');
// Defer so the browser can paint the running message first.
setTimeout(() => {
const seeds = greedySeeds(k);
const reached = simulateFull(seeds);
state = { greedySeeds: seeds, greedyReached: reached, randomSeeds: [], randomReached: new Set(), mode: 'greedy' };
draw();
setStatus(i18nFmt('Greedy k=%k%: reached %n% nodes.', { k, n: reached.size }), 'ok');
}, 10);
});
document.getElementById('btn-random').addEventListener('click', () => {
const k = +kSlider.value;
const seeds = randomSeeds(k);
const reached = simulateFull(seeds);
const prev = state;
state = { greedySeeds: prev.greedySeeds, greedyReached: prev.greedyReached, randomSeeds: seeds, randomReached: reached, mode: 'both' };
draw();
if (prev.greedySeeds.length === 0) {
setStatus(i18nFmt('Random k=%k%: reached %n% nodes. Run greedy to compare.', { k, n: reached.size }), 'tie');
} else {
const gSize = prev.greedyReached.size;
const cls = gSize >= reached.size ? 'ok' : 'bad';
setStatus(i18nFmt('Greedy reached %gn% nodes · Random reached %rn% nodes.', { gn: gSize, rn: reached.size }), cls);
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
state = { greedySeeds: [], greedyReached: new Set(), randomSeeds: [], randomReached: new Set(), mode: 'idle' };
draw();
setStatus('', '');
});
draw();
The greedy strategy wins almost every time — and the guarantee is mathematical, not just lucky. Because the spread function is submodular, each additional greedy pick is worth at most as much as the previous one, and the cumulative shortfall stays bounded below (1−1/e) of optimal.
Comments
Loading comments...