feat(viz): dp coin change
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
RelatedPatterns,
|
RelatedPatterns,
|
||||||
} from "@/components/patterns";
|
} from "@/components/patterns";
|
||||||
import { PatternVisualization } from "@/components/visualization";
|
import { PatternVisualization } from "@/components/visualization";
|
||||||
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization } from "@/components/visualizations-new";
|
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization } from "@/components/visualizations-new";
|
||||||
import { twoSumAlgorithm } from "@/content/algorithms/two-sum";
|
import { twoSumAlgorithm } from "@/content/algorithms/two-sum";
|
||||||
import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window";
|
import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window";
|
||||||
import { binarySearchAlgorithm } from "@/content/algorithms/binary-search";
|
import { binarySearchAlgorithm } from "@/content/algorithms/binary-search";
|
||||||
@@ -25,6 +25,7 @@ import { monotonicStackAlgorithm } from "@/content/algorithms/monotonic-stack";
|
|||||||
import { treeTraversalAlgorithm } from "@/content/algorithms/tree-traversal";
|
import { treeTraversalAlgorithm } from "@/content/algorithms/tree-traversal";
|
||||||
import { bfsAlgorithm } from "@/content/algorithms/bfs";
|
import { bfsAlgorithm } from "@/content/algorithms/bfs";
|
||||||
import { dfsAlgorithm } from "@/content/algorithms/dfs";
|
import { dfsAlgorithm } from "@/content/algorithms/dfs";
|
||||||
|
import { coinChangeAlgorithm } from "@/content/algorithms/coin-change";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
@@ -137,6 +138,8 @@ export default async function PatternDetailPage({ params }: PageProps) {
|
|||||||
<BFSVisualization algorithm={bfsAlgorithm} />
|
<BFSVisualization algorithm={bfsAlgorithm} />
|
||||||
) : slug === "dfs" ? (
|
) : slug === "dfs" ? (
|
||||||
<DFSVisualization algorithm={dfsAlgorithm} />
|
<DFSVisualization algorithm={dfsAlgorithm} />
|
||||||
|
) : slug === "dynamic-programming" ? (
|
||||||
|
<CoinChangeVisualization algorithm={coinChangeAlgorithm} />
|
||||||
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
|
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
'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 { GridView } from '../data-structures/grid-view';
|
||||||
|
import { CalculationBubble } from '../primitives/calculation-bubble';
|
||||||
|
|
||||||
|
interface CoinChangeVisualizationProps {
|
||||||
|
algorithm: AlgorithmDefinition;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CoinChangeVisualization({
|
||||||
|
algorithm,
|
||||||
|
className,
|
||||||
|
}: CoinChangeVisualizationProps) {
|
||||||
|
const {
|
||||||
|
currentStep,
|
||||||
|
currentStepIndex,
|
||||||
|
totalSteps,
|
||||||
|
playback,
|
||||||
|
controls,
|
||||||
|
currentPhase,
|
||||||
|
progress,
|
||||||
|
} = useVisualization(algorithm);
|
||||||
|
|
||||||
|
const { dataState } = currentStep;
|
||||||
|
const coinsArray = dataState.arrays.find((a) => a.id === 'coins') ?? null;
|
||||||
|
const dpGrid = 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>
|
||||||
|
|
||||||
|
{/* Coins array */}
|
||||||
|
{coinsArray && (
|
||||||
|
<ArrayView
|
||||||
|
array={coinsArray}
|
||||||
|
pointers={[]}
|
||||||
|
elementSize="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* DP Table grid */}
|
||||||
|
{dpGrid && (
|
||||||
|
<GridView
|
||||||
|
grid={dpGrid}
|
||||||
|
pointers={gridPointers}
|
||||||
|
cellSize="sm"
|
||||||
|
showColLabels={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</VisualizationContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { GridState, GridPointerState } from "@/lib/visualizations/types";
|
||||||
|
import { GridCell } from "../primitives/grid-cell";
|
||||||
|
|
||||||
|
interface GridViewProps {
|
||||||
|
grid: GridState;
|
||||||
|
pointers?: GridPointerState[];
|
||||||
|
cellSize?: "sm" | "md" | "lg";
|
||||||
|
showColLabels?: boolean;
|
||||||
|
showRowLabels?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CELL_WIDTHS = {
|
||||||
|
sm: 40,
|
||||||
|
md: 56,
|
||||||
|
lg: 72,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const CELL_GAPS = {
|
||||||
|
sm: 2,
|
||||||
|
md: 4,
|
||||||
|
lg: 6,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const POINTER_COLORS = {
|
||||||
|
current: "border-blue-500 text-blue-500",
|
||||||
|
dependency: "border-amber-500 text-amber-500",
|
||||||
|
result: "border-green-500 text-green-500",
|
||||||
|
default: "border-[var(--primary)] text-[var(--primary)]",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function GridView({
|
||||||
|
grid,
|
||||||
|
pointers = [],
|
||||||
|
cellSize = "sm",
|
||||||
|
showColLabels = true,
|
||||||
|
showRowLabels = false,
|
||||||
|
className,
|
||||||
|
}: GridViewProps) {
|
||||||
|
const cellWidth = CELL_WIDTHS[cellSize];
|
||||||
|
const gap = CELL_GAPS[cellSize];
|
||||||
|
|
||||||
|
// For 1D grids (single row), flatten for easier rendering
|
||||||
|
const is1D = grid.cells.length === 1;
|
||||||
|
|
||||||
|
// Calculate pointer positions for highlighting
|
||||||
|
const pointerPositions = new Map<string, GridPointerState>();
|
||||||
|
for (const pointer of pointers) {
|
||||||
|
const key = `${pointer.row}-${pointer.col}`;
|
||||||
|
pointerPositions.set(key, pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col items-center", className)}>
|
||||||
|
{grid.label && (
|
||||||
|
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
|
||||||
|
{grid.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Column labels */}
|
||||||
|
{showColLabels && grid.colLabels && (
|
||||||
|
<div
|
||||||
|
className="flex mb-1"
|
||||||
|
style={{
|
||||||
|
gap: `${gap}px`,
|
||||||
|
marginLeft: showRowLabels ? `${cellWidth + gap}px` : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{grid.colLabels.map((label, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center justify-center text-xs text-[var(--muted-foreground)]"
|
||||||
|
style={{ width: `${cellWidth}px` }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Grid rows */}
|
||||||
|
{is1D ? (
|
||||||
|
// 1D grid (single row)
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex" style={{ gap: `${gap}px` }}>
|
||||||
|
{grid.cells[0].map((cell, colIdx) => {
|
||||||
|
const pointer = pointerPositions.get(`0-${colIdx}`);
|
||||||
|
return (
|
||||||
|
<div key={cell.id} className="relative">
|
||||||
|
{pointer && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-medium whitespace-nowrap",
|
||||||
|
POINTER_COLORS[pointer.color]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{pointer.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<GridCell cell={cell} size={cellSize} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 2D grid
|
||||||
|
<div className="flex flex-col" style={{ gap: `${gap}px` }}>
|
||||||
|
{grid.cells.map((row, rowIdx) => (
|
||||||
|
<div key={rowIdx} className="flex items-center" style={{ gap: `${gap}px` }}>
|
||||||
|
{/* Row label */}
|
||||||
|
{showRowLabels && grid.rowLabels && (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center text-xs text-[var(--muted-foreground)]"
|
||||||
|
style={{ width: `${cellWidth}px` }}
|
||||||
|
>
|
||||||
|
{grid.rowLabels[rowIdx]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Row cells */}
|
||||||
|
{row.map((cell, colIdx) => {
|
||||||
|
const pointer = pointerPositions.get(`${rowIdx}-${colIdx}`);
|
||||||
|
return (
|
||||||
|
<div key={cell.id} className="relative">
|
||||||
|
{pointer && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-5 left-1/2 -translate-x-1/2 text-xs font-medium whitespace-nowrap",
|
||||||
|
POINTER_COLORS[pointer.color]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{pointer.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<GridCell cell={cell} size={cellSize} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ export { LinkedListPointer } from "./primitives/linked-list-pointer";
|
|||||||
export { StackElement } from "./primitives/stack-element";
|
export { StackElement } from "./primitives/stack-element";
|
||||||
export { QueueElement } from "./primitives/queue-element";
|
export { QueueElement } from "./primitives/queue-element";
|
||||||
export { TreeNode } from "./primitives/tree-node";
|
export { TreeNode } from "./primitives/tree-node";
|
||||||
|
export { GridCell } from "./primitives/grid-cell";
|
||||||
|
|
||||||
// Data structures
|
// Data structures
|
||||||
export { ArrayView } from "./data-structures/array-view";
|
export { ArrayView } from "./data-structures/array-view";
|
||||||
@@ -21,6 +22,7 @@ export { LinkedListView } from "./data-structures/linked-list-view";
|
|||||||
export { StackView } from "./data-structures/stack-view";
|
export { StackView } from "./data-structures/stack-view";
|
||||||
export { QueueView } from "./data-structures/queue-view";
|
export { QueueView } from "./data-structures/queue-view";
|
||||||
export { BinaryTreeView } from "./data-structures/binary-tree-view";
|
export { BinaryTreeView } from "./data-structures/binary-tree-view";
|
||||||
|
export { GridView } from "./data-structures/grid-view";
|
||||||
|
|
||||||
// Algorithm visualizations
|
// Algorithm visualizations
|
||||||
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
||||||
@@ -30,3 +32,4 @@ export { BFSVisualization } from "./algorithms/bfs";
|
|||||||
export { DFSVisualization } from "./algorithms/dfs";
|
export { DFSVisualization } from "./algorithms/dfs";
|
||||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||||
export { LinkedListVisualization } from "./algorithms/linked-list";
|
export { LinkedListVisualization } from "./algorithms/linked-list";
|
||||||
|
export { CoinChangeVisualization } from "./algorithms/coin-change";
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { GridCellState } from "@/lib/visualizations/types";
|
||||||
|
|
||||||
|
interface GridCellProps {
|
||||||
|
cell: GridCellState;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZE_CLASSES = {
|
||||||
|
sm: "w-10 h-10 text-sm",
|
||||||
|
md: "w-14 h-14 text-base",
|
||||||
|
lg: "w-18 h-18 text-lg",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const STATE_CLASSES = {
|
||||||
|
normal: "bg-[var(--muted)] border-[var(--border)] text-[var(--foreground)]",
|
||||||
|
highlighted: "bg-[var(--primary)]/20 border-[var(--primary)] text-[var(--primary)]",
|
||||||
|
dimmed: "bg-[var(--muted)]/50 border-[var(--border)]/50 text-[var(--muted-foreground)] opacity-30",
|
||||||
|
success: "bg-green-500/20 border-green-500 text-green-500",
|
||||||
|
comparing: "bg-amber-500/20 border-amber-500 text-amber-500",
|
||||||
|
computing: "bg-blue-500/20 border-blue-500 text-blue-500",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function GridCell({
|
||||||
|
cell,
|
||||||
|
size = "md",
|
||||||
|
className,
|
||||||
|
}: GridCellProps) {
|
||||||
|
const isComputing = cell.state === "computing";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
scale: cell.state === "highlighted" || cell.state === "computing" ? 1.05 : 1,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center rounded-lg border-2 font-mono font-medium transition-colors duration-200",
|
||||||
|
SIZE_CLASSES[size],
|
||||||
|
STATE_CLASSES[cell.state],
|
||||||
|
isComputing && "animate-pulse",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cell.value === Infinity ? "∞" : cell.value}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1111
frontend/src/content/algorithms/coin-change.ts
Normal file
1111
frontend/src/content/algorithms/coin-change.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,9 @@ export interface DataState {
|
|||||||
trees?: BinaryTreeState[];
|
trees?: BinaryTreeState[];
|
||||||
// Queue support
|
// Queue support
|
||||||
queues?: QueueState[];
|
queues?: QueueState[];
|
||||||
|
// Grid support (for DP visualizations)
|
||||||
|
grids?: GridState[];
|
||||||
|
gridPointers?: GridPointerState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single step in the visualization */
|
/** Single step in the visualization */
|
||||||
@@ -246,3 +249,34 @@ export interface QueueState {
|
|||||||
elements: QueueElementState[];
|
elements: QueueElementState[];
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Grid Types (for DP visualizations)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/** State of a grid cell */
|
||||||
|
export interface GridCellState {
|
||||||
|
id: string;
|
||||||
|
value: number | string;
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
state: 'normal' | 'highlighted' | 'comparing' | 'computing' | 'success' | 'dimmed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Complete grid state */
|
||||||
|
export interface GridState {
|
||||||
|
id: string;
|
||||||
|
cells: GridCellState[][]; // 2D array: cells[row][col]
|
||||||
|
colLabels?: (string | number)[];
|
||||||
|
rowLabels?: (string | number)[];
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pointer for grid visualization */
|
||||||
|
export interface GridPointerState {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
color: 'current' | 'dependency' | 'result' | 'default';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user