feat(viz): backtracking, greedy, intervals, matrix

This commit is contained in:
2025-09-03 21:47:33 +01:00
parent 31c74f177b
commit 67c8932c14
15 changed files with 3063 additions and 1 deletions

View File

@@ -14,7 +14,7 @@ import {
RelatedPatterns,
} from "@/components/patterns";
import { PatternVisualization } from "@/components/visualization";
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization, BacktrackingVisualization, HeapVisualization } from "@/components/visualizations-new";
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization, BacktrackingVisualization, HeapVisualization, GreedyVisualization, IntervalsVisualization, MatrixTraversalVisualization } from "@/components/visualizations-new";
import { twoSumAlgorithm } from "@/content/algorithms/two-sum";
import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window";
import { binarySearchAlgorithm } from "@/content/algorithms/binary-search";
@@ -28,6 +28,9 @@ import { dfsAlgorithm } from "@/content/algorithms/dfs";
import { coinChangeAlgorithm } from "@/content/algorithms/coin-change";
import { subsetsAlgorithm } from "@/content/algorithms/subsets";
import { kthLargestAlgorithm } from "@/content/algorithms/kth-largest";
import { jumpGameAlgorithm } from "@/content/algorithms/jump-game";
import { mergeIntervalsAlgorithm } from "@/content/algorithms/merge-intervals";
import { numberOfIslandsAlgorithm } from "@/content/algorithms/number-of-islands";
interface PageProps {
params: Promise<{ slug: string }>;
@@ -146,6 +149,12 @@ export default async function PatternDetailPage({ params }: PageProps) {
<BacktrackingVisualization algorithm={subsetsAlgorithm} />
) : slug === "heap" ? (
<HeapVisualization algorithm={kthLargestAlgorithm} />
) : slug === "greedy" ? (
<GreedyVisualization algorithm={jumpGameAlgorithm} />
) : slug === "intervals" ? (
<IntervalsVisualization algorithm={mergeIntervalsAlgorithm} />
) : slug === "matrix-traversal" ? (
<MatrixTraversalVisualization algorithm={numberOfIslandsAlgorithm} />
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
<Card>
<CardHeader>

View File

@@ -0,0 +1,68 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { DecisionTreeView } from '../data-structures/decision-tree-view';
import { ArrayView } from '../data-structures/array-view';
interface BacktrackingVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function BacktrackingVisualization({
algorithm,
className,
}: BacktrackingVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
const decisionTree = dataState.decisionTrees?.[0] ?? null;
const inputArray = dataState.arrays?.[0] ?? null;
return (
<VisualizationContainer
title={algorithm.title}
pattern={algorithm.pattern}
code={algorithm.code}
currentLine={currentStep.codeLine}
highlightLines={currentStep.codeHighlightLines}
explanation={currentStep.explanation}
decision={currentStep.decision}
phase={currentPhase}
variables={dataState.variables}
currentStepIndex={currentStepIndex}
totalSteps={totalSteps}
isPlaying={playback.isPlaying}
speed={playback.speed}
controls={controls}
progress={progress}
className={className}
>
<div className="flex flex-col items-center gap-6">
{/* Input array (if present) */}
{inputArray && (
<ArrayView
array={inputArray}
pointers={dataState.pointers}
elementSize="sm"
/>
)}
{/* Decision tree */}
{decisionTree && (
<DecisionTreeView tree={decisionTree} />
)}
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,69 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { ArrayView } from '../data-structures/array-view';
import { CalculationBubble } from '../primitives/calculation-bubble';
interface GreedyVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function GreedyVisualization({
algorithm,
className,
}: GreedyVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
const mainArray = dataState.arrays[0];
const calculation = dataState.calculations[0] ?? null;
return (
<VisualizationContainer
title={algorithm.title}
pattern={algorithm.pattern}
code={algorithm.code}
currentLine={currentStep.codeLine}
highlightLines={currentStep.codeHighlightLines}
explanation={currentStep.explanation}
decision={currentStep.decision}
phase={currentPhase}
variables={dataState.variables}
currentStepIndex={currentStepIndex}
totalSteps={totalSteps}
isPlaying={playback.isPlaying}
speed={playback.speed}
controls={controls}
progress={progress}
className={className}
>
<div className="flex flex-col items-center">
{/* Fixed height area for calculation bubble */}
<div className="flex h-8 items-center justify-center">
<CalculationBubble calculation={calculation} />
</div>
{/* Array visualization with pointers */}
<div className="mt-2">
{mainArray && (
<ArrayView
array={mainArray}
pointers={dataState.pointers}
elementSize="sm"
/>
)}
</div>
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,63 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { IntervalView } from '../data-structures/interval-view';
interface IntervalsVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function IntervalsVisualization({
algorithm,
className,
}: IntervalsVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
const inputIntervals = dataState.intervals?.[0] ?? null;
const resultIntervals = dataState.intervals?.[1] ?? null;
return (
<VisualizationContainer
title={algorithm.title}
pattern={algorithm.pattern}
code={algorithm.code}
currentLine={currentStep.codeLine}
highlightLines={currentStep.codeHighlightLines}
explanation={currentStep.explanation}
decision={currentStep.decision}
phase={currentPhase}
variables={dataState.variables}
currentStepIndex={currentStepIndex}
totalSteps={totalSteps}
isPlaying={playback.isPlaying}
speed={playback.speed}
controls={controls}
progress={progress}
className={className}
>
<div className="flex flex-col items-center gap-6">
{/* Input intervals */}
{inputIntervals && (
<IntervalView intervalList={inputIntervals} />
)}
{/* Result intervals (shown when building merged result) */}
{resultIntervals && resultIntervals.intervals.length > 0 && (
<IntervalView intervalList={resultIntervals} />
)}
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,71 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { GridView } from '../data-structures/grid-view';
import { CalculationBubble } from '../primitives/calculation-bubble';
interface MatrixTraversalVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function MatrixTraversalVisualization({
algorithm,
className,
}: MatrixTraversalVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
const grid = dataState.grids?.[0] ?? null;
const calculation = dataState.calculations[0] ?? null;
const gridPointers = dataState.gridPointers ?? [];
return (
<VisualizationContainer
title={algorithm.title}
pattern={algorithm.pattern}
code={algorithm.code}
currentLine={currentStep.codeLine}
highlightLines={currentStep.codeHighlightLines}
explanation={currentStep.explanation}
decision={currentStep.decision}
phase={currentPhase}
variables={dataState.variables}
currentStepIndex={currentStepIndex}
totalSteps={totalSteps}
isPlaying={playback.isPlaying}
speed={playback.speed}
controls={controls}
progress={progress}
className={className}
>
<div className="flex flex-col items-center gap-4">
{/* Calculation bubble */}
<div className="flex h-8 items-center justify-center">
<CalculationBubble calculation={calculation} />
</div>
{/* Island Grid */}
{grid && (
<GridView
grid={grid}
pointers={gridPointers}
cellSize="md"
showColLabels={true}
showRowLabels={true}
/>
)}
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,170 @@
'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { DecisionTreeState, DecisionNodeState } from '@/lib/visualizations/types';
import { DecisionNode } from '../primitives/decision-node';
interface DecisionTreeViewProps {
tree: DecisionTreeState;
className?: string;
}
const NODE_WIDTH = 64;
const NODE_HEIGHT = 32;
const LEVEL_HEIGHT = 80;
const MIN_NODE_SPACING = 72;
const SVG_PADDING = 40;
interface NodePosition {
node: DecisionNodeState;
x: number;
y: number;
parentX?: number;
parentY?: number;
isLeftChild?: boolean;
}
function buildNodeMap(nodes: DecisionNodeState[]): Map<string, DecisionNodeState> {
const map = new Map<string, DecisionNodeState>();
for (const node of nodes) {
map.set(node.id, node);
}
return map;
}
function calculateTreeDepth(
nodeId: string | null,
nodeMap: Map<string, DecisionNodeState>
): number {
if (!nodeId) return 0;
const node = nodeMap.get(nodeId);
if (!node) return 0;
const leftDepth = calculateTreeDepth(node.left, nodeMap);
const rightDepth = calculateTreeDepth(node.right, nodeMap);
return 1 + Math.max(leftDepth, rightDepth);
}
function calculatePositions(
tree: DecisionTreeState,
totalWidth: number
): NodePosition[] {
const nodeMap = buildNodeMap(tree.nodes);
const positions: NodePosition[] = [];
function traverse(
nodeId: string | null,
level: number,
left: number,
right: number,
parentX?: number,
parentY?: number,
isLeftChild?: boolean
) {
if (!nodeId) return;
const node = nodeMap.get(nodeId);
if (!node) return;
const x = (left + right) / 2;
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_HEIGHT / 2;
positions.push({ node, x, y, parentX, parentY, isLeftChild });
const childWidth = (right - left) / 2;
traverse(node.left, level + 1, left, left + childWidth, x, y, true);
traverse(node.right, level + 1, left + childWidth, right, x, y, false);
}
traverse(tree.rootId, 0, 0, totalWidth);
return positions;
}
export function DecisionTreeView({
tree,
className,
}: DecisionTreeViewProps) {
const { positions, svgWidth, svgHeight } = useMemo(() => {
const nodeMap = buildNodeMap(tree.nodes);
const depth = calculateTreeDepth(tree.rootId, nodeMap);
const maxNodesAtBottom = Math.pow(2, depth - 1);
const width = Math.max(
maxNodesAtBottom * MIN_NODE_SPACING + SVG_PADDING * 2,
300
);
const height = depth * LEVEL_HEIGHT + SVG_PADDING * 2;
const pos = calculatePositions(tree, width);
return { positions: pos, svgWidth: width, svgHeight: height };
}, [tree]);
const currentPath = new Set(tree.currentPath ?? []);
return (
<div className={cn('flex flex-col items-center', className)}>
{tree.label && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{tree.label}
</span>
)}
<svg
width={svgWidth}
height={svgHeight}
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
className="overflow-visible"
>
{/* Draw edges first (behind nodes) */}
{positions.map(({ node, x, y, parentX, parentY, isLeftChild }) =>
parentX !== undefined && parentY !== undefined ? (
<g key={`edge-${node.id}`}>
{/* Edge line */}
<motion.line
x1={parentX}
y1={parentY + NODE_HEIGHT / 2}
x2={x}
y2={y - NODE_HEIGHT / 2}
className={cn(
'stroke-[var(--border)]',
currentPath.has(node.id) && 'stroke-[var(--primary)]'
)}
strokeWidth={currentPath.has(node.id) ? 2.5 : 2}
initial={false}
animate={{
opacity: node.state === 'complete' ? 0.6 : 1,
}}
transition={{ duration: 0.2 }}
/>
{/* Branch label */}
<text
x={(parentX + x) / 2 + (isLeftChild ? -8 : 8)}
y={(parentY + NODE_HEIGHT / 2 + y - NODE_HEIGHT / 2) / 2}
textAnchor="middle"
dominantBaseline="central"
className={cn(
'text-[10px] font-medium',
currentPath.has(node.id)
? 'fill-[var(--primary)]'
: 'fill-[var(--muted-foreground)]'
)}
>
{isLeftChild ? '+' : ''}
</text>
</g>
) : null
)}
{/* Draw nodes */}
{positions.map(({ node, x, y }) => (
<DecisionNode
key={node.id}
node={node}
x={x}
y={y}
width={NODE_WIDTH}
height={NODE_HEIGHT}
isInPath={currentPath.has(node.id)}
/>
))}
</svg>
</div>
);
}

View File

@@ -0,0 +1,115 @@
'use client';
import { cn } from '@/lib/utils';
import type { IntervalListState } from '@/lib/visualizations/types';
import { IntervalBar } from '../primitives/interval-bar';
import { AnimatePresence } from 'framer-motion';
interface IntervalViewProps {
intervalList: IntervalListState;
className?: string;
}
const PIXELS_PER_UNIT = 20;
const BAR_HEIGHT = 28;
const ROW_GAP = 8;
export function IntervalView({
intervalList,
className,
}: IntervalViewProps) {
// Calculate timeline range
const minValue = intervalList.minValue ?? 0;
const maxValue = intervalList.maxValue ?? Math.max(
...intervalList.intervals.map((i) => i.end),
20
);
const timelineWidth = (maxValue - minValue) * PIXELS_PER_UNIT;
// Generate tick marks for the number line
const tickStep = maxValue <= 10 ? 1 : maxValue <= 20 ? 2 : 5;
const ticks: number[] = [];
for (let i = minValue; i <= maxValue; i += tickStep) {
ticks.push(i);
}
// Calculate row assignments to prevent overlapping intervals
const rows: IntervalListState['intervals'][] = [];
for (const interval of intervalList.intervals) {
let placed = false;
for (const row of rows) {
const overlaps = row.some(
(existing) => !(interval.end <= existing.start || interval.start >= existing.end)
);
if (!overlaps) {
row.push(interval);
placed = true;
break;
}
}
if (!placed) {
rows.push([interval]);
}
}
const intervalsHeight = rows.length * (BAR_HEIGHT + ROW_GAP) - ROW_GAP;
return (
<div className={cn('flex flex-col items-center', className)}>
{intervalList.label && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{intervalList.label}
</span>
)}
<div className="relative" style={{ width: `${timelineWidth}px` }}>
{/* Intervals area */}
<div
className="relative mb-2"
style={{ height: `${Math.max(intervalsHeight, BAR_HEIGHT)}px` }}
>
<AnimatePresence mode="popLayout">
{rows.map((row, rowIndex) =>
row.map((interval) => (
<div
key={interval.id}
style={{ position: 'absolute', top: `${rowIndex * (BAR_HEIGHT + ROW_GAP)}px` }}
>
<IntervalBar
interval={interval}
pixelsPerUnit={PIXELS_PER_UNIT}
minValue={minValue}
height={BAR_HEIGHT}
/>
</div>
))
)}
</AnimatePresence>
</div>
{/* Number line */}
<div className="relative h-6">
{/* Horizontal line */}
<div
className="absolute top-0 h-px bg-[var(--border)]"
style={{ width: `${timelineWidth}px` }}
/>
{/* Tick marks and labels */}
{ticks.map((tick) => {
const x = (tick - minValue) * PIXELS_PER_UNIT;
return (
<div key={tick} className="absolute" style={{ left: `${x}px` }}>
<div className="h-2 w-px bg-[var(--border)]" />
<span className="absolute -translate-x-1/2 mt-1 text-xs text-[var(--muted-foreground)]">
{tick}
</span>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -17,6 +17,7 @@ export { TreeNode } from "./primitives/tree-node";
export { GridCell } from "./primitives/grid-cell";
export { DecisionNode } from "./primitives/decision-node";
export { HeapNode } from "./primitives/heap-node";
export { IntervalBar } from "./primitives/interval-bar";
// Data structures
export { ArrayView } from "./data-structures/array-view";
@@ -27,6 +28,7 @@ export { BinaryTreeView } from "./data-structures/binary-tree-view";
export { GridView } from "./data-structures/grid-view";
export { DecisionTreeView } from "./data-structures/decision-tree-view";
export { HeapView } from "./data-structures/heap-view";
export { IntervalView } from "./data-structures/interval-view";
// Algorithm visualizations
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
@@ -39,3 +41,6 @@ export { LinkedListVisualization } from "./algorithms/linked-list";
export { CoinChangeVisualization } from "./algorithms/coin-change";
export { BacktrackingVisualization } from "./algorithms/backtracking";
export { HeapVisualization } from "./algorithms/heap";
export { GreedyVisualization } from "./algorithms/greedy";
export { IntervalsVisualization } from "./algorithms/intervals";
export { MatrixTraversalVisualization } from "./algorithms/matrix-traversal";

View File

@@ -0,0 +1,109 @@
'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { DecisionNodeState } from '@/lib/visualizations/types';
interface DecisionNodeProps {
node: DecisionNodeState;
x: number;
y: number;
width?: number;
height?: number;
isInPath?: boolean;
className?: string;
}
const STATE_CLASSES = {
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
exploring: 'fill-[var(--info)]/20 stroke-[var(--info)]',
complete: 'fill-[var(--success)]/20 stroke-[var(--success)]',
backtracking: 'fill-[var(--warning)]/20 stroke-[var(--warning)]',
current: 'fill-[var(--primary)]/20 stroke-[var(--primary)]',
} as const;
const TEXT_CLASSES = {
normal: 'fill-[var(--foreground)]',
exploring: 'fill-[var(--info)]',
complete: 'fill-[var(--success)]',
backtracking: 'fill-[var(--warning)]',
current: 'fill-[var(--primary)]',
} as const;
export function DecisionNode({
node,
x,
y,
width = 64,
height = 32,
isInPath = false,
className,
}: DecisionNodeProps) {
const isActive = node.state === 'current' || node.state === 'exploring';
const showDecision = node.decision && node.state === 'current';
return (
<motion.g
initial={false}
animate={{
scale: isActive ? 1.05 : 1,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
style={{ transformOrigin: `${x}px ${y}px` }}
className={cn(className)}
>
{/* Decision label above node */}
{showDecision && (
<motion.text
x={x}
y={y - height / 2 - 12}
textAnchor="middle"
className="fill-[var(--primary)] text-xs font-medium"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{node.decision}
</motion.text>
)}
{/* Node rectangle (rounded) */}
<motion.rect
x={x - width / 2}
y={y - height / 2}
width={width}
height={height}
rx={6}
ry={6}
strokeWidth={isInPath ? 2.5 : 2}
className={cn(
'transition-colors duration-200',
STATE_CLASSES[node.state]
)}
initial={false}
animate={{
filter: isActive ? 'drop-shadow(0 0 8px var(--primary))' : 'none',
}}
transition={{ duration: 0.2 }}
/>
{/* Value text */}
<text
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
className={cn(
'pointer-events-none select-none font-mono text-xs font-medium',
TEXT_CLASSES[node.state]
)}
>
{node.value}
</text>
</motion.g>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { IntervalState } from '@/lib/visualizations/types';
interface IntervalBarProps {
interval: IntervalState;
pixelsPerUnit: number;
minValue: number;
height?: number;
className?: string;
}
const STATE_CLASSES = {
normal: 'bg-[var(--muted)] border-[var(--border)] text-[var(--foreground)]',
highlighted: 'bg-[var(--primary)]/30 border-[var(--primary)] text-[var(--primary)]',
merging: 'bg-amber-500/30 border-amber-500 text-amber-500',
merged: 'bg-green-500/30 border-green-500 text-green-500',
dimmed: 'bg-[var(--muted)]/30 border-[var(--border)]/50 text-[var(--muted-foreground)] opacity-40',
} as const;
export function IntervalBar({
interval,
pixelsPerUnit,
minValue,
height = 32,
className,
}: IntervalBarProps) {
const width = (interval.end - interval.start) * pixelsPerUnit;
const left = (interval.start - minValue) * pixelsPerUnit;
const label = interval.label ?? `[${interval.start},${interval.end}]`;
return (
<motion.div
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{
opacity: 1,
scale: interval.state === 'highlighted' || interval.state === 'merging' ? 1.02 : 1,
}}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.2 }}
className={cn(
'absolute flex items-center justify-center rounded border-2 font-mono text-xs font-medium transition-colors duration-200',
STATE_CLASSES[interval.state],
className
)}
style={{
left: `${left}px`,
width: `${Math.max(width, 40)}px`,
height: `${height}px`,
}}
>
{label}
</motion.div>
);
}

View File

@@ -0,0 +1,473 @@
import type { AlgorithmDefinition, ArrayElementState } from '@/lib/visualizations/types';
/**
* Jump Game (LeetCode 55)
*
* Input: nums = [2, 3, 1, 1, 4]
* Output: true (can reach last index)
*
* Greedy approach: Track maximum reachable position.
* At each index i, if i > maxReach we're stuck. Otherwise update maxReach.
*/
// Helper to create array state with reachable range highlighted
function createJumpArray(
values: number[],
currentIndex: number | undefined,
maxReach: number,
showResult?: 'success' | 'failure'
) {
return {
id: 'nums',
label: 'Jump Values',
elements: values.map((value, index) => ({
value,
index,
state: (showResult === 'success' && index <= maxReach
? 'success'
: showResult === 'failure' && index > maxReach
? 'dimmed'
: index === currentIndex
? 'highlighted'
: index <= maxReach
? 'success'
: 'normal') as ArrayElementState['state'],
})),
};
}
export const jumpGameAlgorithm: AlgorithmDefinition = {
id: 'jump-game',
title: 'Jump Game (Greedy)',
slug: 'jump-game',
pattern: {
name: 'Greedy',
description:
'Make locally optimal choices at each step, tracking the farthest reachable position.',
},
problemStatement:
'Given an array of non-negative integers nums, where nums[i] represents the maximum jump length from position i, determine if you can reach the last index starting from index 0.',
intuition:
'We don\'t need to track every possible path—just the farthest position we can reach. If at any point our current position exceeds the farthest reachable, we\'re stuck. Otherwise, we keep extending our reach.',
code: {
language: 'python',
code: `def can_jump(nums: list[int]) -> bool:
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
return True`,
},
initialExample: {
input: { nums: [2, 3, 1, 1, 4] },
expected: true,
},
steps: [
// ==========================================
// Phase 1: Problem (2 steps)
// ==========================================
{
id: 'problem-1',
phase: 'problem',
explanation:
'Given nums = [2, 3, 1, 1, 4], can we reach the last index? Each value tells us the maximum jump length from that position.',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, -1)],
pointers: [],
variables: [
{ id: 'goal', name: 'goal', value: 'reach index 4' },
],
calculations: [],
},
},
{
id: 'problem-2',
phase: 'problem',
explanation:
'Brute force: Try every possible path using BFS/DFS. This is O(2^n) in worst case. Can we do better with a greedy approach?',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 0, -1)],
pointers: [],
variables: [
{ id: 'brute', name: 'brute force', value: 'O(2^n)' },
],
calculations: [],
},
},
// ==========================================
// Phase 2: Intuition (3 steps)
// ==========================================
{
id: 'intuition-1',
phase: 'intuition',
explanation:
'Key insight: We only care about the FARTHEST position we can reach, not HOW we get there. If we can reach index 4, we win—the path doesn\'t matter.',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, -1)],
pointers: [],
variables: [
{ id: 'insight', name: 'insight', value: 'track max reach' },
],
calculations: [],
},
},
{
id: 'intuition-2',
phase: 'intuition',
explanation:
'From index 0, nums[0]=2 means we can reach indices 0, 1, or 2. From index 1, nums[1]=3 extends our reach to index 4. That\'s the greedy choice!',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 0, 2)],
pointers: [
{ id: 'i', name: 'i', index: 0, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 2, color: 'right' },
],
variables: [
{ id: 'reach0', name: 'from 0', value: '0 + 2 = 2' },
],
calculations: [],
},
},
{
id: 'intuition-3',
phase: 'intuition',
explanation:
'At each position, we extend our reach if possible. If we ever find ourselves at a position beyond our max reach, we\'re stuck and return false.',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 1, 4)],
pointers: [
{ id: 'i', name: 'i', index: 1, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 4, color: 'right' },
],
variables: [
{ id: 'reach1', name: 'from 1', value: '1 + 3 = 4' },
],
calculations: [],
},
},
// ==========================================
// Phase 3: Pattern (2 steps)
// ==========================================
{
id: 'pattern-1',
phase: 'pattern',
explanation:
'Greedy property: The locally optimal choice (extending reach as far as possible) leads to a globally optimal solution.',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, -1)],
pointers: [],
variables: [
{ id: 'greedy', name: 'greedy', value: 'local → global optimal' },
],
calculations: [],
},
},
{
id: 'pattern-2',
phase: 'pattern',
explanation:
'One pass solution: Track max_reach. For each index i, check if reachable (i ≤ max_reach), then update max_reach = max(max_reach, i + nums[i]). Return true if we finish the loop.',
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, -1)],
pointers: [],
variables: [
{ id: 'time', name: 'Time', value: 'O(n)' },
{ id: 'space', name: 'Space', value: 'O(1)' },
],
calculations: [],
},
},
// ==========================================
// Phase 4: Code (3 steps)
// ==========================================
{
id: 'code-1',
phase: 'code',
explanation:
'Initialize max_reach = 0. We start at index 0, so our initial reachable range is just index 0.',
codeLine: 2,
codeHighlightLines: [2],
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 0)],
pointers: [
{ id: 'maxReach', name: 'max_reach', index: 0, color: 'right' },
],
variables: [
{ id: 'maxReach', name: 'max_reach', value: 0 },
],
calculations: [],
},
},
{
id: 'code-2',
phase: 'code',
explanation:
'Loop through each index. If i > max_reach, we can\'t reach this position—return False. Otherwise, update max_reach to extend our reachable range.',
codeLine: 4,
codeHighlightLines: [4, 5, 6, 7],
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 0)],
pointers: [],
variables: [
{ id: 'maxReach', name: 'max_reach', value: 0 },
],
calculations: [],
},
},
{
id: 'code-3',
phase: 'code',
explanation:
'If we complete the loop, we were able to reach or pass every index—return True. We can reach the last index!',
codeLine: 9,
codeHighlightLines: [9],
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 4, 'success')],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: 'True' },
],
calculations: [],
},
},
// ==========================================
// Phase 5: Execution (~10 steps)
// ==========================================
// Process index 0
{
id: 'exec-1',
phase: 'execution',
explanation:
'Initialize max_reach = 0. Start processing from index 0.',
codeLine: 2,
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 0)],
pointers: [
{ id: 'maxReach', name: 'max_reach', index: 0, color: 'right' },
],
variables: [
{ id: 'maxReach', name: 'max_reach', value: 0 },
],
calculations: [],
},
},
{
id: 'exec-2',
phase: 'execution',
explanation:
'i = 0: Check if reachable. Is 0 > max_reach (0)? No, we can reach index 0.',
codeLine: 5,
decision: {
question: 'Is i > max_reach?',
answer: '0 > 0 = false',
action: 'Position is reachable, continue',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 0, 0)],
pointers: [
{ id: 'i', name: 'i', index: 0, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 0, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 0 },
{ id: 'maxReach', name: 'max_reach', value: 0 },
],
calculations: [],
},
},
{
id: 'exec-3',
phase: 'execution',
explanation:
'Update max_reach = max(0, 0 + 2) = 2. From index 0 with jump value 2, we can now reach up to index 2.',
codeLine: 7,
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 0, 2)],
pointers: [
{ id: 'i', name: 'i', index: 0, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 2, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 0 },
{ id: 'maxReach', name: 'max_reach', value: 2, previousValue: 0, derivation: 'max(0, 0+2)' },
],
calculations: [
{ id: 'calc-1', expression: 'max(0, 0+2)', result: '2', position: 'above' },
],
},
},
// Process index 1
{
id: 'exec-4',
phase: 'execution',
explanation:
'i = 1: Check if reachable. Is 1 > max_reach (2)? No, we can reach index 1.',
codeLine: 5,
decision: {
question: 'Is i > max_reach?',
answer: '1 > 2 = false',
action: 'Position is reachable, continue',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 1, 2)],
pointers: [
{ id: 'i', name: 'i', index: 1, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 2, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 1, previousValue: 0 },
{ id: 'maxReach', name: 'max_reach', value: 2 },
],
calculations: [],
},
},
{
id: 'exec-5',
phase: 'execution',
explanation:
'Update max_reach = max(2, 1 + 3) = 4. From index 1 with jump value 3, we can now reach the last index!',
codeLine: 7,
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 1, 4)],
pointers: [
{ id: 'i', name: 'i', index: 1, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 4, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 1 },
{ id: 'maxReach', name: 'max_reach', value: 4, previousValue: 2, derivation: 'max(2, 1+3)' },
],
calculations: [
{ id: 'calc-2', expression: 'max(2, 1+3)', result: '4', position: 'above' },
],
},
},
// Process index 2
{
id: 'exec-6',
phase: 'execution',
explanation:
'i = 2: Check if reachable. Is 2 > max_reach (4)? No. Update max_reach = max(4, 2+1) = 4. No change.',
codeLine: 7,
decision: {
question: 'Is i > max_reach?',
answer: '2 > 4 = false',
action: 'Position is reachable, continue',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 2, 4)],
pointers: [
{ id: 'i', name: 'i', index: 2, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 4, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 2, previousValue: 1 },
{ id: 'maxReach', name: 'max_reach', value: 4, derivation: 'max(4, 2+1) = 4' },
],
calculations: [],
},
},
// Process index 3
{
id: 'exec-7',
phase: 'execution',
explanation:
'i = 3: Check if reachable. Is 3 > max_reach (4)? No. Update max_reach = max(4, 3+1) = 4. No change.',
codeLine: 7,
decision: {
question: 'Is i > max_reach?',
answer: '3 > 4 = false',
action: 'Position is reachable, continue',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 3, 4)],
pointers: [
{ id: 'i', name: 'i', index: 3, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 4, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 3, previousValue: 2 },
{ id: 'maxReach', name: 'max_reach', value: 4, derivation: 'max(4, 3+1) = 4' },
],
calculations: [],
},
},
// Process index 4 (last)
{
id: 'exec-8',
phase: 'execution',
explanation:
'i = 4: Check if reachable. Is 4 > max_reach (4)? No. Update max_reach = max(4, 4+4) = 8. We\'re at the last index!',
codeLine: 7,
decision: {
question: 'Is i > max_reach?',
answer: '4 > 4 = false',
action: 'Position is reachable, continue',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], 4, 4)],
pointers: [
{ id: 'i', name: 'i', index: 4, color: 'left' },
{ id: 'maxReach', name: 'max_reach', index: 4, color: 'right' },
],
variables: [
{ id: 'i', name: 'i', value: 4, previousValue: 3 },
{ id: 'maxReach', name: 'max_reach', value: 8, previousValue: 4, derivation: 'max(4, 4+4)' },
],
calculations: [
{ id: 'calc-3', expression: 'max(4, 4+4)', result: '8', position: 'above' },
],
},
},
// Return result
{
id: 'exec-9',
phase: 'execution',
explanation:
'Loop complete! We processed all indices without getting stuck. Return True—we can reach the last index.',
codeLine: 9,
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 4, 'success')],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: 'True' },
],
calculations: [],
},
},
{
id: 'exec-10',
phase: 'execution',
explanation:
'Done! The greedy approach finds the answer in O(n) time and O(1) space by tracking only the maximum reachable position.',
codeLine: 9,
decision: {
question: 'Why does greedy work here?',
answer: 'We only need to know IF we can reach the end, not HOW',
action: 'Tracking max reach is sufficient—no need to explore all paths',
},
dataState: {
arrays: [createJumpArray([2, 3, 1, 1, 4], undefined, 4, 'success')],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: 'True' },
{ id: 'time', name: 'Time', value: 'O(n)' },
{ id: 'space', name: 'Space', value: 'O(1)' },
],
calculations: [],
},
},
],
};

View File

@@ -0,0 +1,490 @@
import type { AlgorithmDefinition, IntervalState, IntervalListState } from '@/lib/visualizations/types';
/**
* Merge Intervals (LeetCode 56)
*
* Input: intervals = [[1,3], [2,6], [8,10], [15,18]]
* Output: [[1,6], [8,10], [15,18]]
*
* Sort by start time, then merge overlapping intervals in one pass.
*/
// Helper to create input intervals state
function createInputIntervals(
intervals: [number, number][],
currentIndex?: number,
processedIndices: number[] = []
): IntervalListState {
return {
id: 'input',
label: 'Input Intervals (sorted)',
minValue: 0,
maxValue: 20,
intervals: intervals.map(([start, end], index) => ({
id: `input-${index}`,
start,
end,
label: `[${start},${end}]`,
state: (index === currentIndex
? 'highlighted'
: processedIndices.includes(index)
? 'dimmed'
: 'normal') as IntervalState['state'],
})),
};
}
// Helper to create result intervals state
function createResultIntervals(
intervals: [number, number][],
highlightLast = false,
mergingLast = false
): IntervalListState {
return {
id: 'result',
label: 'Merged Result',
minValue: 0,
maxValue: 20,
intervals: intervals.map(([start, end], index) => ({
id: `result-${index}`,
start,
end,
label: `[${start},${end}]`,
state: (index === intervals.length - 1
? mergingLast
? 'merging'
: highlightLast
? 'highlighted'
: 'merged'
: 'merged') as IntervalState['state'],
})),
};
}
export const mergeIntervalsAlgorithm: AlgorithmDefinition = {
id: 'merge-intervals',
title: 'Merge Intervals',
slug: 'merge-intervals',
pattern: {
name: 'Intervals',
description:
'Sort intervals by start time, then process them linearly to merge overlapping ranges.',
},
problemStatement:
'Given an array of intervals where intervals[i] = [start_i, end_i], merge all overlapping intervals and return an array of non-overlapping intervals.',
intuition:
'After sorting by start time, overlapping intervals become adjacent. Two sorted intervals [a,b] and [c,d] overlap if c <= b. When they overlap, merge them by extending the end to max(b, d).',
code: {
language: 'python',
code: `def merge(intervals: list[list[int]]) -> list[list[int]]:
if len(intervals) <= 1:
return intervals
# Sort by start time
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
last = merged[-1]
if current[0] <= last[1]: # Overlap
last[1] = max(last[1], current[1])
else:
merged.append(current)
return merged`,
},
initialExample: {
input: { intervals: [[1, 3], [2, 6], [8, 10], [15, 18]] },
expected: [[1, 6], [8, 10], [15, 18]],
},
steps: [
// ==========================================
// Phase 1: Problem (2 steps)
// ==========================================
{
id: 'problem-1',
phase: 'problem',
explanation:
'Given intervals [[1,3], [2,6], [8,10], [15,18]], merge all overlapping intervals. Notice [1,3] and [2,6] share the range [2,3].',
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
{
id: 'problem-2',
phase: 'problem',
explanation:
'Brute force: Compare every pair of intervals, O(n^2). But if intervals are sorted, we can detect overlaps in a single pass!',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'brute', name: 'brute force', value: 'O(n^2)' },
{ id: 'optimal', name: 'optimal', value: 'O(n log n)' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
// ==========================================
// Phase 2: Intuition (3 steps)
// ==========================================
{
id: 'intuition-1',
phase: 'intuition',
explanation:
'Key insight: When sorted by start time, overlapping intervals are always adjacent. No need to compare distant intervals!',
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
{
id: 'intuition-2',
phase: 'intuition',
explanation:
'Two intervals [a,b] and [c,d] (with a <= c) overlap when c <= b. In our example, [1,3] and [2,6]: 2 <= 3, so they overlap!',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'overlap', name: 'overlap condition', value: 'c <= b' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 0),
],
},
},
{
id: 'intuition-3',
phase: 'intuition',
explanation:
'To merge, extend the end to max(b, d). [1,3] + [2,6] = [1, max(3,6)] = [1,6]. This covers both original ranges.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'merge', name: 'merge formula', value: '[a, max(b,d)]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 1),
],
},
},
// ==========================================
// Phase 3: Pattern (2 steps)
// ==========================================
{
id: 'pattern-1',
phase: 'pattern',
explanation:
'Intervals pattern: Sort by start time first. This transforms O(n^2) pair comparisons into O(n) sequential checks.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'pattern', name: 'pattern', value: 'sort + linear scan' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
{
id: 'pattern-2',
phase: 'pattern',
explanation:
'Build result incrementally: For each interval, either merge with the last result interval (if overlapping) or add as new.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'time', name: 'Time', value: 'O(n log n)' },
{ id: 'space', name: 'Space', value: 'O(n)' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
// ==========================================
// Phase 4: Code (3 steps)
// ==========================================
{
id: 'code-1',
phase: 'code',
explanation:
'Sort intervals by start time. Our input is already sorted: [[1,3], [2,6], [8,10], [15,18]].',
codeLine: 6,
codeHighlightLines: [5, 6],
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
],
},
},
{
id: 'code-2',
phase: 'code',
explanation:
'Initialize merged list with first interval. This becomes our "current" merged interval to potentially extend.',
codeLine: 8,
codeHighlightLines: [8],
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 0),
createResultIntervals([[1, 3]], true),
],
},
},
{
id: 'code-3',
phase: 'code',
explanation:
'For each remaining interval: if it overlaps with the last merged (current[0] <= last[1]), extend. Otherwise, add as new interval.',
codeLine: 13,
codeHighlightLines: [10, 11, 13, 14, 15, 16],
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]]),
createResultIntervals([[1, 3]]),
],
},
},
// ==========================================
// Phase 5: Execution (~10 steps)
// ==========================================
{
id: 'exec-1',
phase: 'execution',
explanation:
'Initialize: Add first interval [1,3] to merged result.',
codeLine: 8,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'merged', name: 'merged', value: '[[1,3]]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 0, []),
createResultIntervals([[1, 3]], true),
],
},
},
{
id: 'exec-2',
phase: 'execution',
explanation:
'Process [2,6]: Check overlap with last merged [1,3]. Is 2 <= 3? Yes! They overlap.',
codeLine: 13,
decision: {
question: 'Does current[0] <= last[1]?',
answer: '2 <= 3 = true',
action: 'Intervals overlap, merge them',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'current', name: 'current', value: '[2,6]' },
{ id: 'last', name: 'last', value: '[1,3]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 1, [0]),
createResultIntervals([[1, 3]], false, true),
],
},
},
{
id: 'exec-3',
phase: 'execution',
explanation:
'Merge: Extend last interval\'s end to max(3, 6) = 6. Result: [1,6].',
codeLine: 14,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'merged', name: 'merged', value: '[[1,6]]' },
{ id: 'calc', name: 'new end', value: 'max(3,6) = 6' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 1, [0]),
createResultIntervals([[1, 6]], true),
],
},
},
{
id: 'exec-4',
phase: 'execution',
explanation:
'Process [8,10]: Check overlap with last merged [1,6]. Is 8 <= 6? No! No overlap.',
codeLine: 13,
decision: {
question: 'Does current[0] <= last[1]?',
answer: '8 <= 6 = false',
action: 'No overlap, add as new interval',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'current', name: 'current', value: '[8,10]' },
{ id: 'last', name: 'last', value: '[1,6]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 2, [0, 1]),
createResultIntervals([[1, 6]]),
],
},
},
{
id: 'exec-5',
phase: 'execution',
explanation:
'Add [8,10] as a new interval in the result. Gap between [1,6] and [8,10] is preserved.',
codeLine: 16,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'merged', name: 'merged', value: '[[1,6], [8,10]]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 2, [0, 1]),
createResultIntervals([[1, 6], [8, 10]], true),
],
},
},
{
id: 'exec-6',
phase: 'execution',
explanation:
'Process [15,18]: Check overlap with last merged [8,10]. Is 15 <= 10? No! No overlap.',
codeLine: 13,
decision: {
question: 'Does current[0] <= last[1]?',
answer: '15 <= 10 = false',
action: 'No overlap, add as new interval',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'current', name: 'current', value: '[15,18]' },
{ id: 'last', name: 'last', value: '[8,10]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 3, [0, 1, 2]),
createResultIntervals([[1, 6], [8, 10]]),
],
},
},
{
id: 'exec-7',
phase: 'execution',
explanation:
'Add [15,18] as a new interval. Result now has 3 non-overlapping intervals.',
codeLine: 16,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'merged', name: 'merged', value: '[[1,6], [8,10], [15,18]]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], 3, [0, 1, 2]),
createResultIntervals([[1, 6], [8, 10], [15, 18]], true),
],
},
},
{
id: 'exec-8',
phase: 'execution',
explanation:
'Done! Return merged = [[1,6], [8,10], [15,18]]. We merged 4 intervals into 3.',
codeLine: 18,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[1,6], [8,10], [15,18]]' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], undefined, [0, 1, 2, 3]),
createResultIntervals([[1, 6], [8, 10], [15, 18]]),
],
},
},
{
id: 'exec-9',
phase: 'execution',
explanation:
'Time: O(n log n) for sorting + O(n) for merging. Space: O(n) for result. Sorting is the bottleneck.',
codeLine: 18,
decision: {
question: 'Why sort first?',
answer: 'Sorted intervals make overlap detection O(1) per pair',
action: 'Only need to compare adjacent intervals after sorting',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[1,6], [8,10], [15,18]]' },
{ id: 'time', name: 'Time', value: 'O(n log n)' },
{ id: 'space', name: 'Space', value: 'O(n)' },
],
calculations: [],
intervals: [
createInputIntervals([[1, 3], [2, 6], [8, 10], [15, 18]], undefined, [0, 1, 2, 3]),
createResultIntervals([[1, 6], [8, 10], [15, 18]]),
],
},
},
],
};

View File

@@ -0,0 +1,567 @@
import type { AlgorithmDefinition, GridCellState } from '@/lib/visualizations/types';
/**
* Number of Islands (LeetCode 200)
*
* Grid:
* ["1","1","0","0","0"]
* ["1","1","0","0","0"]
* ["0","0","1","0","0"]
* ["0","0","0","1","1"]
*
* Output: 3 islands
* Island 1: (0,0), (0,1), (1,0), (1,1)
* Island 2: (2,2)
* Island 3: (3,3), (3,4)
*/
// Grid data (4 rows x 5 cols)
const GRID = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1'],
];
// Helper to create grid state with cell highlighting
function createIslandGrid(
visitedCells: Set<string>,
currentCell?: string,
exploringCells?: Set<string>,
label = 'Island Grid'
) {
const cells: GridCellState[][] = GRID.map((row, r) =>
row.map((val, c) => {
const key = `${r}-${c}`;
let state: GridCellState['state'] = val === '0' ? 'dimmed' : 'normal';
if (visitedCells.has(key)) state = 'success';
if (exploringCells?.has(key)) state = 'computing';
if (key === currentCell) state = 'highlighted';
return { id: `cell-${r}-${c}`, value: val, row: r, col: c, state };
})
);
return {
id: 'island-grid',
cells,
rowLabels: [0, 1, 2, 3],
colLabels: [0, 1, 2, 3, 4],
label,
};
}
// Helper to create initial grid showing all land/water
function createInitialGrid() {
const cells: GridCellState[][] = GRID.map((row, r) =>
row.map((val, c) => ({
id: `cell-${r}-${c}`,
value: val,
row: r,
col: c,
state: val === '0' ? 'dimmed' : 'normal',
}))
);
return {
id: 'island-grid',
cells,
rowLabels: [0, 1, 2, 3],
colLabels: [0, 1, 2, 3, 4],
label: 'Island Grid',
};
}
export const numberOfIslandsAlgorithm: AlgorithmDefinition = {
id: 'number-of-islands',
title: 'Number of Islands - Matrix Traversal',
slug: 'number-of-islands',
pattern: {
name: 'Matrix Traversal',
description:
'Explore a 2D grid using DFS or BFS to find connected components, paths, or regions.',
},
problemStatement:
'Given a 2D grid of \'1\'s (land) and \'0\'s (water), count the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically.',
intuition:
'Think of the grid as a map. Each unvisited land cell could be the start of a new island. Use DFS to explore all connected land cells from a starting point, marking them visited. Each time we start a new DFS from an unvisited land cell, we\'ve found a new island.',
code: {
language: 'python',
code: `def numIslands(grid: list[list[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r: int, c: int):
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] == '0':
return
grid[r][c] = '0' # Mark visited
dfs(r + 1, c) # Down
dfs(r - 1, c) # Up
dfs(r, c + 1) # Right
dfs(r, c - 1) # Left
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count`,
},
initialExample: {
input: {
grid: [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1'],
],
},
expected: 3,
},
steps: [
// ==========================================
// Phase 1: Problem (2 steps)
// ==========================================
{
id: 'problem-1',
phase: 'problem',
explanation:
'We have a 4x5 grid where \'1\' represents land and \'0\' represents water. Our goal is to count distinct islands (connected land regions).',
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
grids: [createInitialGrid()],
},
},
{
id: 'problem-2',
phase: 'problem',
explanation:
'Land cells are connected horizontally or vertically (not diagonally). Looking at this grid, we can visually spot 3 separate islands.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'islands', name: 'Expected', value: '3 islands' },
],
calculations: [],
grids: [createInitialGrid()],
},
},
// ==========================================
// Phase 2: Intuition (3 steps)
// ==========================================
{
id: 'intuition-1',
phase: 'intuition',
explanation:
'Strategy: Scan the grid cell by cell. When we find an unvisited land cell (\'1\'), we\'ve discovered a new island. Increment count and explore all connected land using DFS.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'islandCount', value: 0 },
],
calculations: [],
grids: [createIslandGrid(new Set(), '0-0')],
},
},
{
id: 'intuition-2',
phase: 'intuition',
explanation:
'DFS explores in 4 directions (up, down, left, right). As we visit each land cell, we mark it as visited by changing \'1\' to \'0\'. This prevents counting the same island twice.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'islandCount', value: 1 },
],
calculations: [
{ id: 'calc-1', expression: 'DFS: (0,0) → neighbors', result: '4 directions', position: 'above' },
],
grids: [createIslandGrid(new Set(), '0-0', new Set(['0-1', '1-0']))],
},
},
{
id: 'intuition-3',
phase: 'intuition',
explanation:
'Key insight: Each time we start DFS from a new unvisited \'1\', we\'ve found a complete new island. The DFS call explores the entire island before returning.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'islandCount', value: 1 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1']))],
},
},
// ==========================================
// Phase 3: Pattern (2 steps)
// ==========================================
{
id: 'pattern-1',
phase: 'pattern',
explanation:
'Matrix Traversal pattern: Treat the grid as an implicit graph where each cell is a node, and adjacent cells are edges. DFS/BFS finds connected components.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'pattern', name: 'Pattern', value: 'Connected Components' },
],
calculations: [],
grids: [createInitialGrid()],
},
},
{
id: 'pattern-2',
phase: 'pattern',
explanation:
'Time: O(rows × cols) - we visit each cell at most once. Space: O(rows × cols) in worst case for DFS recursion stack (all land).',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'time', name: 'Time', value: 'O(m × n)' },
{ id: 'space', name: 'Space', value: 'O(m × n)' },
],
calculations: [],
grids: [createInitialGrid()],
},
},
// ==========================================
// Phase 4: Code (4 steps)
// ==========================================
{
id: 'code-1',
phase: 'code',
explanation:
'Initialize: Get grid dimensions, set island count to 0. We\'ll modify the grid in-place to track visited cells.',
codeLine: 5,
codeHighlightLines: [2, 3, 4, 5, 6],
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'rows', name: 'rows', value: 4 },
{ id: 'cols', name: 'cols', value: 5 },
{ id: 'count', name: 'count', value: 0 },
],
calculations: [],
grids: [createInitialGrid()],
},
},
{
id: 'code-2',
phase: 'code',
explanation:
'DFS function: Base cases return early if out of bounds or cell is water. Otherwise, mark cell visited and recurse in all 4 directions.',
codeLine: 12,
codeHighlightLines: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [
{ id: 'calc-1', expression: 'dfs(r, c)', result: 'explore & mark', position: 'above' },
],
grids: [createInitialGrid()],
},
},
{
id: 'code-3',
phase: 'code',
explanation:
'Main loop: Scan every cell. When we find \'1\' (unvisited land), increment count and run DFS to mark entire island as visited.',
codeLine: 22,
codeHighlightLines: [19, 20, 21, 22, 23],
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
grids: [createIslandGrid(new Set(), '0-0')],
},
},
{
id: 'code-4',
phase: 'code',
explanation:
'Return the final count. Each DFS call discovers one complete island, so count equals the number of islands.',
codeLine: 25,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: '?' },
],
calculations: [],
grids: [createInitialGrid()],
},
},
// ==========================================
// Phase 5: Execution (~12 steps)
// ==========================================
{
id: 'exec-1',
phase: 'execution',
explanation:
'Start scanning at (0,0). Found \'1\' - this is unvisited land! Increment count to 1 and start DFS to explore this island.',
codeLine: 21,
decision: {
question: 'Is cell (0,0) land?',
answer: 'Yes, grid[0][0] = \'1\'',
action: 'New island found! count++, start DFS',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 0 },
{ id: 'c', name: 'c', value: 0 },
{ id: 'count', name: 'count', value: 1 },
],
calculations: [],
grids: [createIslandGrid(new Set(), '0-0')],
gridPointers: [
{ id: 'ptr-curr', name: 'current', row: 0, col: 0, color: 'current' },
],
},
},
{
id: 'exec-2',
phase: 'execution',
explanation:
'DFS from (0,0): Mark as visited, explore neighbors. Cell (0,1) is land - recurse there. Cell (1,0) is also land.',
codeLine: 13,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 1 },
],
calculations: [
{ id: 'calc-1', expression: 'dfs(0,0) → dfs(0,1), dfs(1,0)', result: 'exploring', position: 'above' },
],
grids: [createIslandGrid(new Set(['0-0']), undefined, new Set(['0-1', '1-0']))],
gridPointers: [
{ id: 'ptr-curr', name: 'visited', row: 0, col: 0, color: 'result' },
],
},
},
{
id: 'exec-3',
phase: 'execution',
explanation:
'DFS explores (0,1), then (1,0), then (1,1). All 4 cells of island 1 are now marked as visited (green).',
codeLine: 13,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 1 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1']))],
},
},
{
id: 'exec-4',
phase: 'execution',
explanation:
'DFS complete for island 1. Continue scanning: (0,2), (0,3), (0,4) are all water (\'0\'). Skip them.',
codeLine: 20,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 0 },
{ id: 'c', name: 'c', value: 2 },
{ id: 'count', name: 'count', value: 1 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1']), '0-2')],
gridPointers: [
{ id: 'ptr-scan', name: 'scan', row: 0, col: 2, color: 'current' },
],
},
},
{
id: 'exec-5',
phase: 'execution',
explanation:
'Scanning row 1: (1,0) and (1,1) already visited. (1,2), (1,3), (1,4) are water. Continue to row 2.',
codeLine: 20,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 1 },
{ id: 'count', name: 'count', value: 1 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1']))],
},
},
{
id: 'exec-6',
phase: 'execution',
explanation:
'Scan (2,0), (2,1) - water. At (2,2): Found \'1\'! This is a new unvisited land cell. Increment count to 2.',
codeLine: 21,
decision: {
question: 'Is cell (2,2) land?',
answer: 'Yes, grid[2][2] = \'1\'',
action: 'New island found! count++',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 2 },
{ id: 'c', name: 'c', value: 2 },
{ id: 'count', name: 'count', value: 2 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1']), '2-2')],
gridPointers: [
{ id: 'ptr-curr', name: 'current', row: 2, col: 2, color: 'current' },
],
},
},
{
id: 'exec-7',
phase: 'execution',
explanation:
'DFS from (2,2): Check all 4 neighbors - all are water or out of bounds. This is a single-cell island. Mark visited.',
codeLine: 13,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 2 },
],
calculations: [
{ id: 'calc-1', expression: 'dfs(2,2) neighbors', result: 'all water', position: 'above' },
],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2']))],
},
},
{
id: 'exec-8',
phase: 'execution',
explanation:
'Continue scanning row 2 and row 3: (2,3), (2,4), (3,0), (3,1), (3,2) are all water. Skip them.',
codeLine: 20,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 3 },
{ id: 'c', name: 'c', value: 2 },
{ id: 'count', name: 'count', value: 2 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2']), '3-2')],
gridPointers: [
{ id: 'ptr-scan', name: 'scan', row: 3, col: 2, color: 'current' },
],
},
},
{
id: 'exec-9',
phase: 'execution',
explanation:
'At (3,3): Found \'1\'! This is another new island. Increment count to 3 and start DFS.',
codeLine: 21,
decision: {
question: 'Is cell (3,3) land?',
answer: 'Yes, grid[3][3] = \'1\'',
action: 'New island found! count++',
},
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'r', name: 'r', value: 3 },
{ id: 'c', name: 'c', value: 3 },
{ id: 'count', name: 'count', value: 3 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2']), '3-3')],
gridPointers: [
{ id: 'ptr-curr', name: 'current', row: 3, col: 3, color: 'current' },
],
},
},
{
id: 'exec-10',
phase: 'execution',
explanation:
'DFS from (3,3): Mark visited, explore neighbors. Found (3,4) is also land! Recurse to explore it.',
codeLine: 13,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 3 },
],
calculations: [
{ id: 'calc-1', expression: 'dfs(3,3) → dfs(3,4)', result: 'found neighbor', position: 'above' },
],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2', '3-3']), undefined, new Set(['3-4']))],
gridPointers: [
{ id: 'ptr-curr', name: 'exploring', row: 3, col: 4, color: 'current' },
],
},
},
{
id: 'exec-11',
phase: 'execution',
explanation:
'DFS visits (3,4): Mark visited. No more land neighbors. Island 3 complete with cells (3,3) and (3,4).',
codeLine: 13,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 3 },
],
calculations: [],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2', '3-3', '3-4']))],
},
},
{
id: 'exec-12',
phase: 'execution',
explanation:
'Scan complete! All cells visited. We found 3 islands: (1) 4 cells top-left, (2) 1 cell middle, (3) 2 cells bottom-right.',
codeLine: 25,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'count', name: 'count', value: 3, derivation: '3 connected components' },
],
calculations: [
{ id: 'calc-1', expression: 'return count', result: '3', position: 'above' },
],
grids: [createIslandGrid(new Set(['0-0', '0-1', '1-0', '1-1', '2-2', '3-3', '3-4']), undefined, undefined, 'Result: 3 Islands')],
},
},
],
};

View File

@@ -0,0 +1,771 @@
import type { AlgorithmDefinition, DecisionNodeState, DecisionTreeState } from '@/lib/visualizations/types';
/**
* Decision tree for Subsets problem with nums = [1, 2, 3]:
*
* []
* / \
* [1] []
* / \ / \
* [1,2] [1] [2] []
* / \ / \ / \ / \
* [1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] []
*
* Each level represents: Include element i?
* Left branch = include, Right branch = exclude
* Leaf nodes are the 8 subsets (2^3)
*/
// Node IDs for the decision tree
const ROOT = 'root';
const L1_INC = 'l1-inc'; // [1]
const L1_EXC = 'l1-exc'; // []
const L2_INC_INC = 'l2-inc-inc'; // [1,2]
const L2_INC_EXC = 'l2-inc-exc'; // [1]
const L2_EXC_INC = 'l2-exc-inc'; // [2]
const L2_EXC_EXC = 'l2-exc-exc'; // []
const L3_123 = 'l3-123'; // [1,2,3]
const L3_12 = 'l3-12'; // [1,2]
const L3_13 = 'l3-13'; // [1,3]
const L3_1 = 'l3-1'; // [1]
const L3_23 = 'l3-23'; // [2,3]
const L3_2 = 'l3-2'; // [2]
const L3_3 = 'l3-3'; // [3]
const L3_EMPTY = 'l3-empty'; // []
type NodeStateType = DecisionNodeState['state'];
// Helper to create decision tree state
function createDecisionTree(
nodeStates: Record<string, NodeStateType>,
decisions: Record<string, string>,
currentPath: string[] = []
): DecisionTreeState {
const getState = (id: string): NodeStateType => nodeStates[id] ?? 'normal';
const getDecision = (id: string): string | undefined => decisions[id];
const nodes: DecisionNodeState[] = [
{ id: ROOT, value: '[]', state: getState(ROOT), left: L1_INC, right: L1_EXC, decision: getDecision(ROOT), depth: 0 },
{ id: L1_INC, value: '[1]', state: getState(L1_INC), left: L2_INC_INC, right: L2_INC_EXC, decision: getDecision(L1_INC), depth: 1 },
{ id: L1_EXC, value: '[]', state: getState(L1_EXC), left: L2_EXC_INC, right: L2_EXC_EXC, decision: getDecision(L1_EXC), depth: 1 },
{ id: L2_INC_INC, value: '[1,2]', state: getState(L2_INC_INC), left: L3_123, right: L3_12, decision: getDecision(L2_INC_INC), depth: 2 },
{ id: L2_INC_EXC, value: '[1]', state: getState(L2_INC_EXC), left: L3_13, right: L3_1, decision: getDecision(L2_INC_EXC), depth: 2 },
{ id: L2_EXC_INC, value: '[2]', state: getState(L2_EXC_INC), left: L3_23, right: L3_2, decision: getDecision(L2_EXC_INC), depth: 2 },
{ id: L2_EXC_EXC, value: '[]', state: getState(L2_EXC_EXC), left: L3_3, right: L3_EMPTY, decision: getDecision(L2_EXC_EXC), depth: 2 },
{ id: L3_123, value: '[1,2,3]', state: getState(L3_123), left: null, right: null, depth: 3 },
{ id: L3_12, value: '[1,2]', state: getState(L3_12), left: null, right: null, depth: 3 },
{ id: L3_13, value: '[1,3]', state: getState(L3_13), left: null, right: null, depth: 3 },
{ id: L3_1, value: '[1]', state: getState(L3_1), left: null, right: null, depth: 3 },
{ id: L3_23, value: '[2,3]', state: getState(L3_23), left: null, right: null, depth: 3 },
{ id: L3_2, value: '[2]', state: getState(L3_2), left: null, right: null, depth: 3 },
{ id: L3_3, value: '[3]', state: getState(L3_3), left: null, right: null, depth: 3 },
{ id: L3_EMPTY, value: '[]', state: getState(L3_EMPTY), left: null, right: null, depth: 3 },
];
return {
id: 'decision-tree',
label: 'Decision Tree',
rootId: ROOT,
nodes,
currentPath,
};
}
export const subsetsAlgorithm: AlgorithmDefinition = {
id: 'subsets',
title: 'Subsets (Backtracking)',
slug: 'subsets',
pattern: {
name: 'Backtracking',
description:
'Build solutions incrementally using Choose-Explore-Unchoose pattern, exploring all possibilities through a decision tree.',
},
problemStatement:
'Given an array nums of unique integers, return all possible subsets (the power set). The solution set must not contain duplicate subsets.',
intuition:
'Every subset is built by making a binary decision for each element: include it or exclude it. This creates a decision tree with 2^n leaves, each representing one unique subset.',
code: {
language: 'python',
code: `def subsets(nums: list[int]) -> list[list[int]]:
result = []
def backtrack(start: int, current: list[int]):
# Add current subset to result
result.append(current[:])
# Try including each remaining element
for i in range(start, len(nums)):
current.append(nums[i]) # Choose
backtrack(i + 1, current) # Explore
current.pop() # Unchoose
backtrack(0, [])
return result`,
},
initialExample: {
input: { nums: [1, 2, 3] },
expected: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]],
},
steps: [
// ==========================================
// Phase 1: Problem (2 steps)
// ==========================================
{
id: 'problem-1',
phase: 'problem',
explanation:
'Given nums = [1, 2, 3], find all possible subsets. A subset can include any combination of elements, including the empty set.',
dataState: {
arrays: [{
id: 'input',
label: 'Input Array',
elements: [
{ value: 1, index: 0, state: 'normal' },
{ value: 2, index: 1, state: 'normal' },
{ value: 3, index: 2, state: 'normal' },
],
}],
pointers: [],
variables: [],
calculations: [],
decisionTrees: [createDecisionTree({}, {})],
},
},
{
id: 'problem-2',
phase: 'problem',
explanation:
'With 3 elements, there are 2^3 = 8 possible subsets. Each element can be included or excluded independently.',
dataState: {
arrays: [{
id: 'input',
label: 'Input Array',
elements: [
{ value: 1, index: 0, state: 'highlighted' },
{ value: 2, index: 1, state: 'highlighted' },
{ value: 3, index: 2, state: 'highlighted' },
],
}],
pointers: [],
variables: [
{ id: 'total', name: 'Total subsets', value: '2³ = 8' },
],
calculations: [],
decisionTrees: [createDecisionTree({}, {})],
},
},
// ==========================================
// Phase 2: Intuition (3 steps)
// ==========================================
{
id: 'intuition-1',
phase: 'intuition',
explanation:
'Think of building each subset as a series of binary decisions: for each element, we ask "Include it?" Yes (left branch) or No (right branch).',
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'current' },
{ [ROOT]: 'Include 1?' }
)],
},
},
{
id: 'intuition-2',
phase: 'intuition',
explanation:
'This creates a binary decision tree. Each path from root to leaf represents one subset. Left = include the element, Right = exclude it.',
dataState: {
arrays: [],
pointers: [],
variables: [],
calculations: [],
decisionTrees: [createDecisionTree(
{
[ROOT]: 'exploring',
[L1_INC]: 'exploring',
[L2_INC_INC]: 'exploring',
[L3_123]: 'current',
},
{},
[ROOT, L1_INC, L2_INC_INC, L3_123]
)],
},
},
{
id: 'intuition-3',
phase: 'intuition',
explanation:
'The tree has depth n (3 levels of decisions) and 2^n = 8 leaf nodes. Each leaf is a complete subset we add to our result.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'depth', name: 'Tree depth', value: 'n = 3' },
{ id: 'leaves', name: 'Leaf nodes', value: '2ⁿ = 8' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{
[L3_123]: 'complete',
[L3_12]: 'complete',
[L3_13]: 'complete',
[L3_1]: 'complete',
[L3_23]: 'complete',
[L3_2]: 'complete',
[L3_3]: 'complete',
[L3_EMPTY]: 'complete',
},
{}
)],
},
},
// ==========================================
// Phase 3: Pattern (2 steps)
// ==========================================
{
id: 'pattern-1',
phase: 'pattern',
explanation:
'Backtracking follows the Choose-Explore-Unchoose pattern: (1) Choose an element, (2) Explore with it, (3) Unchoose to try other paths.',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'step1', name: 'Choose', value: 'current.append(x)' },
{ id: 'step2', name: 'Explore', value: 'backtrack(...)' },
{ id: 'step3', name: 'Unchoose', value: 'current.pop()' },
],
calculations: [],
decisionTrees: [createDecisionTree({}, {})],
},
},
{
id: 'pattern-2',
phase: 'pattern',
explanation:
'We add each partial result at every node (not just leaves). This captures all subsets: [], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3].',
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'key', name: 'Key insight', value: 'Add result at each node' },
],
calculations: [],
decisionTrees: [createDecisionTree({}, {})],
},
},
// ==========================================
// Phase 4: Code (3 steps)
// ==========================================
{
id: 'code-1',
phase: 'code',
explanation:
'Initialize: empty result list. The backtrack function takes start index (which elements to consider) and current subset being built.',
codeLine: 2,
codeHighlightLines: [2, 4],
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[]' },
{ id: 'start', name: 'start', value: '0' },
{ id: 'current', name: 'current', value: '[]' },
],
calculations: [],
decisionTrees: [createDecisionTree({ [ROOT]: 'current' }, {})],
},
},
{
id: 'code-2',
phase: 'code',
explanation:
'First action: always add current subset to result (using a copy). This captures the subset at this point in the decision tree.',
codeLine: 6,
codeHighlightLines: [6],
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[]]' },
{ id: 'current', name: 'current', value: '[]' },
],
calculations: [],
decisionTrees: [createDecisionTree({ [ROOT]: 'current' }, {})],
},
},
{
id: 'code-3',
phase: 'code',
explanation:
'For each remaining element: Choose (append), Explore (recurse with start + 1), Unchoose (pop). This builds all combinations.',
codeLine: 9,
codeHighlightLines: [9, 10, 11, 12],
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'loop', name: 'Loop', value: 'for i in range(start, len(nums))' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'current' },
{ [L1_INC]: 'Include 2?' },
[ROOT, L1_INC]
)],
},
},
// ==========================================
// Phase 5: Execution (~20 steps)
// ==========================================
{
id: 'exec-1',
phase: 'execution',
explanation:
'Start: backtrack(0, []). At the root node, first add [] to result.',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[]]' },
{ id: 'current', name: 'current', value: '[]' },
{ id: 'start', name: 'start', value: '0' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'current' },
{ [ROOT]: 'Include 1?' },
[ROOT]
)],
},
},
{
id: 'exec-2',
phase: 'execution',
explanation:
'Choose: append 1 to current. Now current = [1]. This is the "include 1" branch.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[]]' },
{ id: 'current', name: 'current', value: '[1]', previousValue: '[]' },
{ id: 'i', name: 'i', value: '0 (element: 1)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'current' },
{ [L1_INC]: 'Include 2?' },
[ROOT, L1_INC]
)],
},
},
{
id: 'exec-3',
phase: 'execution',
explanation:
'Explore: backtrack(1, [1]). At this node, add [1] to result. Result: [[], [1]].',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1]]' },
{ id: 'current', name: 'current', value: '[1]' },
{ id: 'start', name: 'start', value: '1' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'current' },
{ [L1_INC]: 'Include 2?' },
[ROOT, L1_INC]
)],
},
},
{
id: 'exec-4',
phase: 'execution',
explanation:
'Choose: append 2 to current. Now current = [1, 2]. Taking the "include 2" branch.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1]]' },
{ id: 'current', name: 'current', value: '[1, 2]', previousValue: '[1]' },
{ id: 'i', name: 'i', value: '1 (element: 2)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'current' },
{ [L2_INC_INC]: 'Include 3?' },
[ROOT, L1_INC, L2_INC_INC]
)],
},
},
{
id: 'exec-5',
phase: 'execution',
explanation:
'Explore: backtrack(2, [1, 2]). Add [1, 2] to result. Result: [[], [1], [1, 2]].',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2]]' },
{ id: 'current', name: 'current', value: '[1, 2]' },
{ id: 'start', name: 'start', value: '2' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'current' },
{ [L2_INC_INC]: 'Include 3?' },
[ROOT, L1_INC, L2_INC_INC]
)],
},
},
{
id: 'exec-6',
phase: 'execution',
explanation:
'Choose: append 3. current = [1, 2, 3]. We\'ve reached the deepest left path.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2]]' },
{ id: 'current', name: 'current', value: '[1, 2, 3]', previousValue: '[1, 2]' },
{ id: 'i', name: 'i', value: '2 (element: 3)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'exploring', [L3_123]: 'current' },
{},
[ROOT, L1_INC, L2_INC_INC, L3_123]
)],
},
},
{
id: 'exec-7',
phase: 'execution',
explanation:
'Explore: backtrack(3, [1, 2, 3]). Add [1, 2, 3] to result. No more elements (start=3 >= len=3).',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3]]' },
{ id: 'current', name: 'current', value: '[1, 2, 3]' },
{ id: 'start', name: 'start', value: '3' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'exploring', [L3_123]: 'complete' },
{},
[ROOT, L1_INC, L2_INC_INC, L3_123]
)],
},
},
{
id: 'exec-8',
phase: 'execution',
explanation:
'Backtrack: pop 3 from current. current = [1, 2]. Return to [1, 2] node. Loop ends (no more elements).',
codeLine: 12,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3]]' },
{ id: 'current', name: 'current', value: '[1, 2]', previousValue: '[1, 2, 3]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'backtracking', [L3_123]: 'complete', [L3_12]: 'complete' },
{},
[ROOT, L1_INC, L2_INC_INC]
)],
},
},
{
id: 'exec-9',
phase: 'execution',
explanation:
'Backtrack: pop 2 from current. current = [1]. Back at [1] node. Continue loop with i=2 (element 3).',
codeLine: 12,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3]]' },
{ id: 'current', name: 'current', value: '[1]', previousValue: '[1, 2]' },
{ id: 'i', name: 'i', value: '2 (element: 3)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'backtracking', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'current', [L3_123]: 'complete', [L3_12]: 'complete' },
{ [L2_INC_EXC]: 'Include 3?' },
[ROOT, L1_INC, L2_INC_EXC]
)],
},
},
{
id: 'exec-10',
phase: 'execution',
explanation:
'Choose: append 3. current = [1, 3]. This is the path: include 1, skip 2, include 3.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3]]' },
{ id: 'current', name: 'current', value: '[1, 3]', previousValue: '[1]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'exploring', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'current' },
{},
[ROOT, L1_INC, L2_INC_EXC, L3_13]
)],
},
},
{
id: 'exec-11',
phase: 'execution',
explanation:
'Explore: backtrack(3, [1, 3]). Add [1, 3] to result. Result: [[], [1], [1, 2], [1, 2, 3], [1, 3]].',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3]]' },
{ id: 'current', name: 'current', value: '[1, 3]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'exploring', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete' },
{},
[ROOT, L1_INC, L2_INC_EXC, L3_13]
)],
},
},
{
id: 'exec-12',
phase: 'execution',
explanation:
'Backtrack: pop 3, then pop 1. Back to root. Now try i=1 (element 2), skipping 1.',
codeLine: 12,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3]]' },
{ id: 'current', name: 'current', value: '[]', previousValue: '[1]' },
{ id: 'i', name: 'i', value: '1 (element: 2)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'backtracking', [L1_INC]: 'complete', [L1_EXC]: 'current', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete' },
{ [L1_EXC]: 'Include 2?' },
[ROOT, L1_EXC]
)],
},
},
{
id: 'exec-13',
phase: 'execution',
explanation:
'Choose: append 2. current = [2]. Path: skip 1, include 2.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3]]' },
{ id: 'current', name: 'current', value: '[2]', previousValue: '[]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'complete', [L1_EXC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'current', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete' },
{ [L2_EXC_INC]: 'Include 3?' },
[ROOT, L1_EXC, L2_EXC_INC]
)],
},
},
{
id: 'exec-14',
phase: 'execution',
explanation:
'Explore: backtrack(2, [2]). Add [2] to result. Result: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2]].',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2]]' },
{ id: 'current', name: 'current', value: '[2]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'complete', [L1_EXC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'current', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete' },
{ [L2_EXC_INC]: 'Include 3?' },
[ROOT, L1_EXC, L2_EXC_INC]
)],
},
},
{
id: 'exec-15',
phase: 'execution',
explanation:
'Choose: append 3. current = [2, 3]. Explore and add to result. Result: [..., [2, 3]].',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]]' },
{ id: 'current', name: 'current', value: '[2, 3]', previousValue: '[2]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'complete', [L1_EXC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'exploring', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete', [L3_23]: 'complete', [L3_2]: 'complete' },
{},
[ROOT, L1_EXC, L2_EXC_INC, L3_23]
)],
},
},
{
id: 'exec-16',
phase: 'execution',
explanation:
'Backtrack to root. Now try i=2 (element 3), skipping both 1 and 2.',
codeLine: 12,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]]' },
{ id: 'current', name: 'current', value: '[]', previousValue: '[2]' },
{ id: 'i', name: 'i', value: '2 (element: 3)' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'backtracking', [L1_INC]: 'complete', [L1_EXC]: 'complete', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'complete', [L2_EXC_EXC]: 'current', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete', [L3_23]: 'complete', [L3_2]: 'complete' },
{ [L2_EXC_EXC]: 'Include 3?' },
[ROOT, L1_EXC, L2_EXC_EXC]
)],
},
},
{
id: 'exec-17',
phase: 'execution',
explanation:
'Choose: append 3. current = [3]. Path: skip 1, skip 2, include 3.',
codeLine: 10,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]]' },
{ id: 'current', name: 'current', value: '[3]', previousValue: '[]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'complete', [L1_EXC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'complete', [L2_EXC_EXC]: 'exploring', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete', [L3_23]: 'complete', [L3_2]: 'complete', [L3_3]: 'current' },
{},
[ROOT, L1_EXC, L2_EXC_EXC, L3_3]
)],
},
},
{
id: 'exec-18',
phase: 'execution',
explanation:
'Explore: backtrack(3, [3]). Add [3] to result. Result: [..., [3]].',
codeLine: 6,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]' },
{ id: 'current', name: 'current', value: '[3]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{ [ROOT]: 'exploring', [L1_INC]: 'complete', [L1_EXC]: 'exploring', [L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'complete', [L2_EXC_EXC]: 'exploring', [L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete', [L3_23]: 'complete', [L3_2]: 'complete', [L3_3]: 'complete', [L3_EMPTY]: 'complete' },
{},
[ROOT, L1_EXC, L2_EXC_EXC, L3_3]
)],
},
},
{
id: 'exec-19',
phase: 'execution',
explanation:
'Backtrack all the way to root. Loop at root ends (i=3 >= len=3). All paths explored.',
codeLine: 12,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]' },
{ id: 'current', name: 'current', value: '[]', previousValue: '[3]' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{
[ROOT]: 'complete',
[L1_INC]: 'complete', [L1_EXC]: 'complete',
[L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'complete', [L2_EXC_EXC]: 'complete',
[L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete',
[L3_23]: 'complete', [L3_2]: 'complete', [L3_3]: 'complete', [L3_EMPTY]: 'complete',
},
{},
[]
)],
},
},
{
id: 'exec-20',
phase: 'execution',
explanation:
'Done! Return result with all 8 subsets: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]].',
codeLine: 15,
dataState: {
arrays: [],
pointers: [],
variables: [
{ id: 'result', name: 'result', value: '8 subsets found' },
],
calculations: [],
decisionTrees: [createDecisionTree(
{
[ROOT]: 'complete',
[L1_INC]: 'complete', [L1_EXC]: 'complete',
[L2_INC_INC]: 'complete', [L2_INC_EXC]: 'complete', [L2_EXC_INC]: 'complete', [L2_EXC_EXC]: 'complete',
[L3_123]: 'complete', [L3_12]: 'complete', [L3_13]: 'complete', [L3_1]: 'complete',
[L3_23]: 'complete', [L3_2]: 'complete', [L3_3]: 'complete', [L3_EMPTY]: 'complete',
},
{},
[]
)],
},
},
],
};

View File

@@ -93,6 +93,8 @@ export interface DataState {
decisionTrees?: DecisionTreeState[];
// Heap support
heaps?: HeapState[];
// Interval support
intervals?: IntervalListState[];
}
/** Single step in the visualization */
@@ -329,3 +331,25 @@ export interface HeapState {
heapType: 'min' | 'max';
maxSize?: number; // For bounded heaps (like top-k)
}
// ============================================
// Interval Types
// ============================================
/** State of a single interval */
export interface IntervalState {
id: string;
start: number;
end: number;
state: 'normal' | 'highlighted' | 'merging' | 'merged' | 'dimmed';
label?: string; // Optional label like "[1,3]"
}
/** Complete interval list state */
export interface IntervalListState {
id: string;
intervals: IntervalState[];
label?: string;
minValue?: number; // Timeline range
maxValue?: number;
}