|
| 1 | +// Minimum Spanning Tree — presets and Kruskal/Prim planners. |
| 2 | +// |
| 3 | +// Reuses the action-log pattern and the weighted graph workspace. MST is |
| 4 | +// undirected; planners treat the graph as undirected. Edge/node states reused: |
| 5 | +// relax -> edge being considered |
| 6 | +// path -> edge/node in the spanning tree |
| 7 | +// reject -> edge skipped (would form a cycle) |
| 8 | +// plus { type: 'status', text } for the running/total weight. |
| 9 | + |
| 10 | +export const MST_PRESETS = [ |
| 11 | + { |
| 12 | + name: 'Mesh', |
| 13 | + nodes: [ |
| 14 | + { id: 'n1', x: 120, y: 70, label: 'A' }, |
| 15 | + { id: 'n2', x: 290, y: 50, label: 'B' }, |
| 16 | + { id: 'n3', x: 450, y: 100, label: 'C' }, |
| 17 | + { id: 'n4', x: 160, y: 240, label: 'D' }, |
| 18 | + { id: 'n5', x: 320, y: 250, label: 'E' }, |
| 19 | + { id: 'n6', x: 470, y: 240, label: 'F' }, |
| 20 | + ], |
| 21 | + edges: [ |
| 22 | + ['n1', 'n2', 4], ['n2', 'n3', 3], ['n1', 'n4', 2], ['n2', 'n4', 5], |
| 23 | + ['n2', 'n5', 10], ['n3', 'n5', 6], ['n3', 'n6', 1], ['n4', 'n5', 4], ['n5', 'n6', 2], |
| 24 | + ], |
| 25 | + }, |
| 26 | + { |
| 27 | + name: 'Ring', |
| 28 | + nodes: [ |
| 29 | + { id: 'n1', x: 250, y: 40, label: 'A' }, |
| 30 | + { id: 'n2', x: 360, y: 110, label: 'B' }, |
| 31 | + { id: 'n3', x: 360, y: 240, label: 'C' }, |
| 32 | + { id: 'n4', x: 250, y: 300, label: 'D' }, |
| 33 | + { id: 'n5', x: 140, y: 240, label: 'E' }, |
| 34 | + { id: 'n6', x: 140, y: 110, label: 'F' }, |
| 35 | + ], |
| 36 | + edges: [ |
| 37 | + ['n1', 'n2', 3], ['n2', 'n3', 5], ['n3', 'n4', 2], ['n4', 'n5', 6], |
| 38 | + ['n5', 'n6', 4], ['n6', 'n1', 1], ['n1', 'n4', 7], ['n2', 'n5', 8], |
| 39 | + ], |
| 40 | + }, |
| 41 | +]; |
| 42 | + |
| 43 | +// each undirected edge once |
| 44 | +function weightedEdges(edges) { |
| 45 | + return edges.map((e) => ({ u: e.source, v: e.target, w: e.data?.weight ?? 1, id: e.id })); |
| 46 | +} |
| 47 | + |
| 48 | +export function kruskalActions(nodes, edges) { |
| 49 | + const actions = []; |
| 50 | + const V = nodes.length; |
| 51 | + if (V === 0) return actions; |
| 52 | + |
| 53 | + const sorted = weightedEdges(edges).sort((a, b) => (a.w - b.w) || (a.id < b.id ? -1 : 1)); |
| 54 | + |
| 55 | + const parent = {}; |
| 56 | + for (const n of nodes) parent[n.id] = n.id; |
| 57 | + const find = (x) => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }; |
| 58 | + |
| 59 | + let total = 0; |
| 60 | + let accepted = 0; |
| 61 | + for (const { u, v, w, id } of sorted) { |
| 62 | + actions.push({ type: 'markEdge', id, state: 'relax' }); |
| 63 | + const ru = find(u); |
| 64 | + const rv = find(v); |
| 65 | + if (ru !== rv) { |
| 66 | + parent[ru] = rv; |
| 67 | + total += w; |
| 68 | + accepted += 1; |
| 69 | + actions.push({ type: 'markEdge', id, state: 'path' }); |
| 70 | + actions.push({ type: 'markNode', id: u, state: 'path' }); |
| 71 | + actions.push({ type: 'markNode', id: v, state: 'path' }); |
| 72 | + actions.push({ type: 'status', text: `MST weight: ${total}` }); |
| 73 | + } else { |
| 74 | + actions.push({ type: 'markEdge', id, state: 'reject' }); |
| 75 | + } |
| 76 | + } |
| 77 | + actions.push({ |
| 78 | + type: 'status', |
| 79 | + text: accepted === V - 1 ? `MST complete · weight ${total}` : `Not connected · forest weight ${total}`, |
| 80 | + }); |
| 81 | + return actions; |
| 82 | +} |
| 83 | + |
| 84 | +export function primActions(adj, startId, nodes) { |
| 85 | + const actions = []; |
| 86 | + const ids = nodes.map((n) => n.id); |
| 87 | + const V = ids.length; |
| 88 | + const start = startId ?? ids[0]; |
| 89 | + if (start == null) return actions; |
| 90 | + |
| 91 | + const visited = new Set([start]); |
| 92 | + actions.push({ type: 'markNode', id: start, state: 'path' }); |
| 93 | + let total = 0; |
| 94 | + |
| 95 | + while (visited.size < V) { |
| 96 | + // best crossing edge from the visited set to an unvisited node |
| 97 | + let best = null; |
| 98 | + for (const u of visited) { |
| 99 | + for (const { node: v, edge, weight } of adj[u] || []) { |
| 100 | + if (visited.has(v)) continue; |
| 101 | + if (best == null || weight < best.weight) best = { u, v, edge, weight }; |
| 102 | + } |
| 103 | + } |
| 104 | + if (best == null) break; // disconnected |
| 105 | + |
| 106 | + actions.push({ type: 'markEdge', id: best.edge, state: 'relax' }); |
| 107 | + actions.push({ type: 'markEdge', id: best.edge, state: 'path' }); |
| 108 | + actions.push({ type: 'markNode', id: best.v, state: 'path' }); |
| 109 | + visited.add(best.v); |
| 110 | + total += best.weight; |
| 111 | + actions.push({ type: 'status', text: `MST weight: ${total}` }); |
| 112 | + } |
| 113 | + |
| 114 | + actions.push({ |
| 115 | + type: 'status', |
| 116 | + text: visited.size === V ? `MST complete · weight ${total}` : `Not connected · weight ${total}`, |
| 117 | + }); |
| 118 | + return actions; |
| 119 | +} |
0 commit comments