feat(viz): union-find
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, CoinChangeVisualization, BacktrackingVisualization, HeapVisualization, GreedyVisualization, IntervalsVisualization, MatrixTraversalVisualization } from "@/components/visualizations-new";
|
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization, BacktrackingVisualization, HeapVisualization, GreedyVisualization, IntervalsVisualization, MatrixTraversalVisualization, TrieVisualization, UnionFindVisualization } 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";
|
||||||
@@ -31,6 +31,8 @@ import { kthLargestAlgorithm } from "@/content/algorithms/kth-largest";
|
|||||||
import { jumpGameAlgorithm } from "@/content/algorithms/jump-game";
|
import { jumpGameAlgorithm } from "@/content/algorithms/jump-game";
|
||||||
import { mergeIntervalsAlgorithm } from "@/content/algorithms/merge-intervals";
|
import { mergeIntervalsAlgorithm } from "@/content/algorithms/merge-intervals";
|
||||||
import { numberOfIslandsAlgorithm } from "@/content/algorithms/number-of-islands";
|
import { numberOfIslandsAlgorithm } from "@/content/algorithms/number-of-islands";
|
||||||
|
import { implementTrieAlgorithm } from "@/content/algorithms/implement-trie";
|
||||||
|
import { redundantConnectionAlgorithm } from "@/content/algorithms/redundant-connection";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
@@ -155,6 +157,10 @@ export default async function PatternDetailPage({ params }: PageProps) {
|
|||||||
<IntervalsVisualization algorithm={mergeIntervalsAlgorithm} />
|
<IntervalsVisualization algorithm={mergeIntervalsAlgorithm} />
|
||||||
) : slug === "matrix-traversal" ? (
|
) : slug === "matrix-traversal" ? (
|
||||||
<MatrixTraversalVisualization algorithm={numberOfIslandsAlgorithm} />
|
<MatrixTraversalVisualization algorithm={numberOfIslandsAlgorithm} />
|
||||||
|
) : slug === "trie" ? (
|
||||||
|
<TrieVisualization algorithm={implementTrieAlgorithm} />
|
||||||
|
) : slug === "union-find" ? (
|
||||||
|
<UnionFindVisualization algorithm={redundantConnectionAlgorithm} />
|
||||||
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
|
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||||
|
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||||
|
import { VisualizationContainer } from '../core/visualization-container';
|
||||||
|
import { UnionFindView } from '../data-structures/union-find-view';
|
||||||
|
|
||||||
|
interface UnionFindVisualizationProps {
|
||||||
|
algorithm: AlgorithmDefinition;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnionFindVisualization({
|
||||||
|
algorithm,
|
||||||
|
className,
|
||||||
|
}: UnionFindVisualizationProps) {
|
||||||
|
const {
|
||||||
|
currentStep,
|
||||||
|
currentStepIndex,
|
||||||
|
totalSteps,
|
||||||
|
playback,
|
||||||
|
controls,
|
||||||
|
currentPhase,
|
||||||
|
progress,
|
||||||
|
} = useVisualization(algorithm);
|
||||||
|
|
||||||
|
const { dataState } = currentStep;
|
||||||
|
const unionFind = dataState.unionFind?.[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 items-start justify-center min-h-[200px]">
|
||||||
|
{unionFind && <UnionFindView unionFind={unionFind} />}
|
||||||
|
</div>
|
||||||
|
</VisualizationContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { UnionFindState, UnionFindNodeState } from '@/lib/visualizations/types';
|
||||||
|
import { UnionFindNode } from '../primitives/union-find-node';
|
||||||
|
|
||||||
|
interface UnionFindViewProps {
|
||||||
|
unionFind: UnionFindState;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_RADIUS = 24;
|
||||||
|
const LEVEL_HEIGHT = 72;
|
||||||
|
const MIN_NODE_SPACING = 64;
|
||||||
|
const SVG_PADDING = 48;
|
||||||
|
const TREE_GAP = 40;
|
||||||
|
|
||||||
|
interface NodePosition {
|
||||||
|
node: UnionFindNodeState;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
parentX?: number;
|
||||||
|
parentY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeInfo {
|
||||||
|
rootId: string;
|
||||||
|
nodes: Set<string>;
|
||||||
|
depth: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNodeMap(nodes: UnionFindNodeState[]): Map<string, UnionFindNodeState> {
|
||||||
|
const map = new Map<string, UnionFindNodeState>();
|
||||||
|
for (const node of nodes) {
|
||||||
|
map.set(node.id, node);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build children map: for each node, which nodes have it as parent?
|
||||||
|
*/
|
||||||
|
function buildChildrenMap(nodes: UnionFindNodeState[]): Map<string, string[]> {
|
||||||
|
const childrenMap = new Map<string, string[]>();
|
||||||
|
for (const node of nodes) {
|
||||||
|
childrenMap.set(node.id, []);
|
||||||
|
}
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.parentId !== null) {
|
||||||
|
const children = childrenMap.get(node.parentId);
|
||||||
|
if (children) {
|
||||||
|
children.push(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return childrenMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify roots (nodes where parentId is null).
|
||||||
|
*/
|
||||||
|
function findRoots(nodes: UnionFindNodeState[]): string[] {
|
||||||
|
return nodes.filter(n => n.parentId === null).map(n => n.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the depth of a tree rooted at the given node.
|
||||||
|
*/
|
||||||
|
function calculateTreeDepth(
|
||||||
|
nodeId: string,
|
||||||
|
childrenMap: Map<string, string[]>
|
||||||
|
): number {
|
||||||
|
const children = childrenMap.get(nodeId) ?? [];
|
||||||
|
if (children.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let maxChildDepth = 0;
|
||||||
|
for (const childId of children) {
|
||||||
|
const childDepth = calculateTreeDepth(childId, childrenMap);
|
||||||
|
maxChildDepth = Math.max(maxChildDepth, childDepth);
|
||||||
|
}
|
||||||
|
return 1 + maxChildDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the width needed for a subtree rooted at the given node.
|
||||||
|
*/
|
||||||
|
function calculateSubtreeWidth(
|
||||||
|
nodeId: string,
|
||||||
|
childrenMap: Map<string, string[]>
|
||||||
|
): number {
|
||||||
|
const children = childrenMap.get(nodeId) ?? [];
|
||||||
|
if (children.length === 0) {
|
||||||
|
return MIN_NODE_SPACING;
|
||||||
|
}
|
||||||
|
let totalWidth = 0;
|
||||||
|
for (const childId of children) {
|
||||||
|
totalWidth += calculateSubtreeWidth(childId, childrenMap);
|
||||||
|
}
|
||||||
|
return Math.max(totalWidth, MIN_NODE_SPACING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all node IDs in a tree rooted at rootId.
|
||||||
|
*/
|
||||||
|
function collectTreeNodes(
|
||||||
|
rootId: string,
|
||||||
|
childrenMap: Map<string, string[]>,
|
||||||
|
collected: Set<string>
|
||||||
|
): void {
|
||||||
|
collected.add(rootId);
|
||||||
|
const children = childrenMap.get(rootId) ?? [];
|
||||||
|
for (const childId of children) {
|
||||||
|
collectTreeNodes(childId, childrenMap, collected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build tree info for each root in the forest.
|
||||||
|
*/
|
||||||
|
function buildTreeInfos(
|
||||||
|
roots: string[],
|
||||||
|
childrenMap: Map<string, string[]>
|
||||||
|
): TreeInfo[] {
|
||||||
|
return roots.map(rootId => {
|
||||||
|
const nodes = new Set<string>();
|
||||||
|
collectTreeNodes(rootId, childrenMap, nodes);
|
||||||
|
const depth = calculateTreeDepth(rootId, childrenMap);
|
||||||
|
const width = calculateSubtreeWidth(rootId, childrenMap);
|
||||||
|
return { rootId, nodes, depth, width };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate positions for all nodes in a forest layout.
|
||||||
|
* Roots are at the TOP, children are BELOW.
|
||||||
|
* Parent pointers go UPWARD from child to parent.
|
||||||
|
*/
|
||||||
|
function calculatePositions(
|
||||||
|
unionFind: UnionFindState
|
||||||
|
): { positions: NodePosition[]; svgWidth: number; svgHeight: number } {
|
||||||
|
const nodeMap = buildNodeMap(unionFind.nodes);
|
||||||
|
const childrenMap = buildChildrenMap(unionFind.nodes);
|
||||||
|
const roots = findRoots(unionFind.nodes);
|
||||||
|
const treeInfos = buildTreeInfos(roots, childrenMap);
|
||||||
|
|
||||||
|
// Calculate total width needed
|
||||||
|
const totalWidth = treeInfos.reduce((sum, t) => sum + t.width, 0) +
|
||||||
|
(treeInfos.length - 1) * TREE_GAP +
|
||||||
|
SVG_PADDING * 2;
|
||||||
|
|
||||||
|
// Calculate max depth for height
|
||||||
|
const maxDepth = Math.max(1, ...treeInfos.map(t => t.depth));
|
||||||
|
const svgHeight = maxDepth * LEVEL_HEIGHT + SVG_PADDING * 2;
|
||||||
|
|
||||||
|
// Pre-calculate subtree widths
|
||||||
|
const subtreeWidths = new Map<string, number>();
|
||||||
|
for (const node of unionFind.nodes) {
|
||||||
|
subtreeWidths.set(node.id, calculateSubtreeWidth(node.id, childrenMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions: NodePosition[] = [];
|
||||||
|
let currentX = SVG_PADDING;
|
||||||
|
|
||||||
|
for (const treeInfo of treeInfos) {
|
||||||
|
const treeWidth = treeInfo.width;
|
||||||
|
|
||||||
|
// Traverse tree and position nodes
|
||||||
|
function traverse(
|
||||||
|
nodeId: string,
|
||||||
|
level: number,
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
parentX?: number,
|
||||||
|
parentY?: number
|
||||||
|
) {
|
||||||
|
const node = nodeMap.get(nodeId);
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const x = (left + right) / 2;
|
||||||
|
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS;
|
||||||
|
|
||||||
|
positions.push({ node, x, y, parentX, parentY });
|
||||||
|
|
||||||
|
const children = childrenMap.get(nodeId) ?? [];
|
||||||
|
if (children.length > 0) {
|
||||||
|
const childrenWidth = children.reduce(
|
||||||
|
(sum, childId) => sum + (subtreeWidths.get(childId) ?? MIN_NODE_SPACING),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let childLeft = x - childrenWidth / 2;
|
||||||
|
|
||||||
|
for (const childId of children) {
|
||||||
|
const childWidth = subtreeWidths.get(childId) ?? MIN_NODE_SPACING;
|
||||||
|
traverse(
|
||||||
|
childId,
|
||||||
|
level + 1,
|
||||||
|
childLeft,
|
||||||
|
childLeft + childWidth,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
);
|
||||||
|
childLeft += childWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(treeInfo.rootId, 0, currentX, currentX + treeWidth);
|
||||||
|
currentX += treeWidth + TREE_GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { positions, svgWidth: Math.max(totalWidth, 200), svgHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnionFindView({ unionFind, className }: UnionFindViewProps) {
|
||||||
|
const { positions, svgWidth, svgHeight } = useMemo(
|
||||||
|
() => calculatePositions(unionFind),
|
||||||
|
[unionFind]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build a set of highlighted path nodes for edge styling
|
||||||
|
const pathNodeIds = new Set(unionFind.findPath ?? []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col items-center', className)}>
|
||||||
|
{unionFind.label && (
|
||||||
|
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
|
||||||
|
{unionFind.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
width={svgWidth}
|
||||||
|
height={svgHeight}
|
||||||
|
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
|
||||||
|
className="overflow-visible"
|
||||||
|
>
|
||||||
|
{/* Draw edges (from child UP to parent) */}
|
||||||
|
{positions.map(({ node, x, y, parentX, parentY }) => {
|
||||||
|
if (parentX === undefined || parentY === undefined) return null;
|
||||||
|
|
||||||
|
// Determine if this edge is on the current find path
|
||||||
|
const isOnPath = pathNodeIds.has(node.id);
|
||||||
|
|
||||||
|
// Draw line from child to parent (upward)
|
||||||
|
// Arrow points toward parent (at the top)
|
||||||
|
const startY = y - NODE_RADIUS;
|
||||||
|
const endY = parentY + NODE_RADIUS;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={`edge-${node.id}`}>
|
||||||
|
<motion.line
|
||||||
|
x1={x}
|
||||||
|
y1={startY}
|
||||||
|
x2={parentX}
|
||||||
|
y2={endY}
|
||||||
|
className={cn(
|
||||||
|
'stroke-[var(--border)]',
|
||||||
|
isOnPath && 'stroke-[var(--primary)]',
|
||||||
|
node.state === 'cycle' && 'stroke-[var(--error)]'
|
||||||
|
)}
|
||||||
|
strokeWidth={isOnPath ? 2.5 : 2}
|
||||||
|
markerEnd="url(#arrowhead)"
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Arrow marker definition */}
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
id="arrowhead"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="7"
|
||||||
|
refX="9"
|
||||||
|
refY="3.5"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="0 0, 10 3.5, 0 7"
|
||||||
|
className="fill-[var(--border)]"
|
||||||
|
/>
|
||||||
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="arrowhead-primary"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="7"
|
||||||
|
refX="9"
|
||||||
|
refY="3.5"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="0 0, 10 3.5, 0 7"
|
||||||
|
className="fill-[var(--primary)]"
|
||||||
|
/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Draw nodes */}
|
||||||
|
{positions.map(({ node, x, y }) => (
|
||||||
|
<UnionFindNode
|
||||||
|
key={node.id}
|
||||||
|
node={node}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
radius={NODE_RADIUS}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Current operation indicator */}
|
||||||
|
{unionFind.message && (
|
||||||
|
<text
|
||||||
|
x={svgWidth / 2}
|
||||||
|
y={svgHeight - 10}
|
||||||
|
textAnchor="middle"
|
||||||
|
className="fill-[var(--muted-foreground)] text-xs"
|
||||||
|
>
|
||||||
|
{unionFind.message}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current edge indicator */}
|
||||||
|
{unionFind.currentEdge && (
|
||||||
|
<text
|
||||||
|
x={svgWidth / 2}
|
||||||
|
y={20}
|
||||||
|
textAnchor="middle"
|
||||||
|
className="fill-[var(--foreground)] text-sm font-mono"
|
||||||
|
>
|
||||||
|
Processing edge: [{unionFind.currentEdge[0]}, {unionFind.currentEdge[1]}]
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ export { GridCell } from "./primitives/grid-cell";
|
|||||||
export { DecisionNode } from "./primitives/decision-node";
|
export { DecisionNode } from "./primitives/decision-node";
|
||||||
export { HeapNode } from "./primitives/heap-node";
|
export { HeapNode } from "./primitives/heap-node";
|
||||||
export { IntervalBar } from "./primitives/interval-bar";
|
export { IntervalBar } from "./primitives/interval-bar";
|
||||||
|
export { TrieNode } from "./primitives/trie-node";
|
||||||
|
export { UnionFindNode } from "./primitives/union-find-node";
|
||||||
|
|
||||||
// Data structures
|
// Data structures
|
||||||
export { ArrayView } from "./data-structures/array-view";
|
export { ArrayView } from "./data-structures/array-view";
|
||||||
@@ -29,6 +31,8 @@ export { GridView } from "./data-structures/grid-view";
|
|||||||
export { DecisionTreeView } from "./data-structures/decision-tree-view";
|
export { DecisionTreeView } from "./data-structures/decision-tree-view";
|
||||||
export { HeapView } from "./data-structures/heap-view";
|
export { HeapView } from "./data-structures/heap-view";
|
||||||
export { IntervalView } from "./data-structures/interval-view";
|
export { IntervalView } from "./data-structures/interval-view";
|
||||||
|
export { TrieView } from "./data-structures/trie-view";
|
||||||
|
export { UnionFindView } from "./data-structures/union-find-view";
|
||||||
|
|
||||||
// Algorithm visualizations
|
// Algorithm visualizations
|
||||||
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
||||||
@@ -44,3 +48,5 @@ export { HeapVisualization } from "./algorithms/heap";
|
|||||||
export { GreedyVisualization } from "./algorithms/greedy";
|
export { GreedyVisualization } from "./algorithms/greedy";
|
||||||
export { IntervalsVisualization } from "./algorithms/intervals";
|
export { IntervalsVisualization } from "./algorithms/intervals";
|
||||||
export { MatrixTraversalVisualization } from "./algorithms/matrix-traversal";
|
export { MatrixTraversalVisualization } from "./algorithms/matrix-traversal";
|
||||||
|
export { TrieVisualization } from "./algorithms/trie";
|
||||||
|
export { UnionFindVisualization } from "./algorithms/union-find";
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { UnionFindNodeState } from '@/lib/visualizations/types';
|
||||||
|
|
||||||
|
interface UnionFindNodeProps {
|
||||||
|
node: UnionFindNodeState;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
radius?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATE_CLASSES = {
|
||||||
|
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
|
||||||
|
root: 'fill-[var(--primary)]/20 stroke-[var(--primary)]',
|
||||||
|
finding: 'fill-[var(--info)]/20 stroke-[var(--info)]',
|
||||||
|
compressing: 'fill-[var(--info)]/30 stroke-[var(--info)]',
|
||||||
|
merging: 'fill-[var(--warning)]/20 stroke-[var(--warning)]',
|
||||||
|
highlighted: 'fill-[var(--primary)]/30 stroke-[var(--primary)]',
|
||||||
|
cycle: 'fill-[var(--error)]/20 stroke-[var(--error)]',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const TEXT_CLASSES = {
|
||||||
|
normal: 'fill-[var(--foreground)]',
|
||||||
|
root: 'fill-[var(--primary)]',
|
||||||
|
finding: 'fill-[var(--info)]',
|
||||||
|
compressing: 'fill-[var(--info)]',
|
||||||
|
merging: 'fill-[var(--warning)]',
|
||||||
|
highlighted: 'fill-[var(--primary)]',
|
||||||
|
cycle: 'fill-[var(--error)]',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function UnionFindNode({
|
||||||
|
node,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
radius = 24,
|
||||||
|
className,
|
||||||
|
}: UnionFindNodeProps) {
|
||||||
|
const isActive = node.state === 'finding' || node.state === 'compressing' || node.state === 'merging';
|
||||||
|
const isRoot = node.parentId === null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.g
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
scale: isActive ? 1.1 : 1,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30,
|
||||||
|
}}
|
||||||
|
style={{ transformOrigin: `${x}px ${y}px` }}
|
||||||
|
className={cn(className)}
|
||||||
|
>
|
||||||
|
{/* Main circle */}
|
||||||
|
<motion.circle
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r={radius}
|
||||||
|
strokeWidth={isRoot ? 3 : 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 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Root indicator: inner circle */}
|
||||||
|
{isRoot && (
|
||||||
|
<motion.circle
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r={radius - 6}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="none"
|
||||||
|
className={cn(
|
||||||
|
'transition-colors duration-200',
|
||||||
|
STATE_CLASSES[node.state]
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Value label */}
|
||||||
|
<text
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none select-none font-mono text-sm font-medium',
|
||||||
|
TEXT_CLASSES[node.state]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{node.value}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* Rank badge (top-right corner) */}
|
||||||
|
<g>
|
||||||
|
<circle
|
||||||
|
cx={x + radius * 0.7}
|
||||||
|
cy={y - radius * 0.7}
|
||||||
|
r={10}
|
||||||
|
className="fill-[var(--surface)] stroke-[var(--border)]"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={x + radius * 0.7}
|
||||||
|
y={y - radius * 0.7}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
className="pointer-events-none select-none font-mono text-xs fill-[var(--muted-foreground)]"
|
||||||
|
>
|
||||||
|
{node.rank}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</motion.g>
|
||||||
|
);
|
||||||
|
}
|
||||||
682
frontend/src/content/algorithms/redundant-connection.ts
Normal file
682
frontend/src/content/algorithms/redundant-connection.ts
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
import type { AlgorithmDefinition, UnionFindNodeState, UnionFindState } from '@/lib/visualizations/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redundant Connection - LeetCode 684
|
||||||
|
*
|
||||||
|
* In this problem, a tree with n nodes has one extra edge added.
|
||||||
|
* We need to find the edge that can be removed to restore the tree.
|
||||||
|
*
|
||||||
|
* Example: edges = [[1,2], [1,3], [2,3]]
|
||||||
|
* Node 1 connects to 2, node 1 connects to 3, node 2 connects to 3.
|
||||||
|
* The edge [2,3] creates a cycle, so it's redundant.
|
||||||
|
*
|
||||||
|
* Visual:
|
||||||
|
* 1
|
||||||
|
* / \
|
||||||
|
* 2---3 (edge [2,3] creates cycle)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Node IDs
|
||||||
|
const NODE_1 = 'node-1';
|
||||||
|
const NODE_2 = 'node-2';
|
||||||
|
const NODE_3 = 'node-3';
|
||||||
|
|
||||||
|
type NodeState = 'normal' | 'root' | 'finding' | 'compressing' | 'merging' | 'highlighted' | 'cycle';
|
||||||
|
|
||||||
|
// Helper to create a single union-find node
|
||||||
|
function createNode(
|
||||||
|
id: string,
|
||||||
|
value: number,
|
||||||
|
parentId: string | null,
|
||||||
|
rank: number,
|
||||||
|
state: NodeState
|
||||||
|
): UnionFindNodeState {
|
||||||
|
return { id, value, parentId, rank, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create union-find state
|
||||||
|
function createUnionFindState(
|
||||||
|
nodes: UnionFindNodeState[],
|
||||||
|
currentEdge?: [number, number],
|
||||||
|
findPath?: string[],
|
||||||
|
message?: string
|
||||||
|
): UnionFindState {
|
||||||
|
return {
|
||||||
|
id: 'union-find',
|
||||||
|
nodes,
|
||||||
|
label: 'Union-Find Forest',
|
||||||
|
currentEdge,
|
||||||
|
findPath,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state: each node is its own root
|
||||||
|
function initialNodes(): UnionFindNodeState[] {
|
||||||
|
return [
|
||||||
|
createNode(NODE_1, 1, null, 0, 'root'),
|
||||||
|
createNode(NODE_2, 2, null, 0, 'root'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'root'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// After union(1,2): node 2's parent is 1
|
||||||
|
function afterUnion12(states: Partial<Record<string, NodeState>> = {}): UnionFindNodeState[] {
|
||||||
|
return [
|
||||||
|
createNode(NODE_1, 1, null, 1, states[NODE_1] ?? 'root'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, states[NODE_2] ?? 'normal'),
|
||||||
|
createNode(NODE_3, 3, null, 0, states[NODE_3] ?? 'root'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// After union(1,3): node 3's parent is also 1
|
||||||
|
function afterUnion13(states: Partial<Record<string, NodeState>> = {}): UnionFindNodeState[] {
|
||||||
|
return [
|
||||||
|
createNode(NODE_1, 1, null, 1, states[NODE_1] ?? 'root'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, states[NODE_2] ?? 'normal'),
|
||||||
|
createNode(NODE_3, 3, NODE_1, 0, states[NODE_3] ?? 'normal'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redundantConnectionAlgorithm: AlgorithmDefinition = {
|
||||||
|
id: 'redundant-connection',
|
||||||
|
title: 'Redundant Connection',
|
||||||
|
slug: 'redundant-connection',
|
||||||
|
pattern: {
|
||||||
|
name: 'Union-Find',
|
||||||
|
description:
|
||||||
|
'A data structure that tracks a partition of elements into disjoint sets, supporting near-constant time union and find operations with path compression and union by rank.',
|
||||||
|
},
|
||||||
|
problemStatement:
|
||||||
|
'Given a graph that started as a tree with n nodes (labeled 1 to n), with one additional edge added. Find the edge that can be removed so that the remaining graph is a tree.',
|
||||||
|
intuition:
|
||||||
|
'A tree with n nodes has exactly n-1 edges. If we add one more edge, it creates exactly one cycle. If we process edges one by one and try to union the two nodes, the moment we find that both nodes already share the same root, we\'ve found the redundant edge that would create a cycle.',
|
||||||
|
code: {
|
||||||
|
language: 'python',
|
||||||
|
code: `def findRedundantConnection(edges):
|
||||||
|
n = len(edges)
|
||||||
|
parent = list(range(n + 1))
|
||||||
|
rank = [0] * (n + 1)
|
||||||
|
|
||||||
|
def find(x):
|
||||||
|
if parent[x] != x:
|
||||||
|
parent[x] = find(parent[x]) # Path compression
|
||||||
|
return parent[x]
|
||||||
|
|
||||||
|
def union(x, y):
|
||||||
|
px, py = find(x), find(y)
|
||||||
|
if px == py:
|
||||||
|
return False # Already connected!
|
||||||
|
# Union by rank
|
||||||
|
if rank[px] < rank[py]:
|
||||||
|
px, py = py, px
|
||||||
|
parent[py] = px
|
||||||
|
if rank[px] == rank[py]:
|
||||||
|
rank[px] += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
for u, v in edges:
|
||||||
|
if not union(u, v):
|
||||||
|
return [u, v] # Redundant edge found!`,
|
||||||
|
},
|
||||||
|
initialExample: {
|
||||||
|
input: { edges: [[1, 2], [1, 3], [2, 3]] },
|
||||||
|
expected: [2, 3],
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
// ==========================================
|
||||||
|
// Phase 1: Problem (2 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'problem-1',
|
||||||
|
phase: 'problem',
|
||||||
|
explanation:
|
||||||
|
'We have a graph that started as a tree with n nodes, but one extra edge was added, creating a cycle. Our task is to find and return the edge that can be removed to restore the tree structure.',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edges', name: 'edges', value: '[[1,2], [1,3], [2,3]]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'problem-2',
|
||||||
|
phase: 'problem',
|
||||||
|
explanation:
|
||||||
|
'With edges [[1,2], [1,3], [2,3]], we have 3 nodes. A valid tree with 3 nodes should have exactly 2 edges. We have 3 edges, so one is redundant and creates a cycle.',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edges', name: 'edges', value: '[[1,2], [1,3], [2,3]]' },
|
||||||
|
{ id: 'n', name: 'n', value: '3 nodes, 3 edges' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes(), undefined, undefined, 'Tree needs n-1 edges, we have n')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 2: Intuition (3 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'intuition-1',
|
||||||
|
phase: 'intuition',
|
||||||
|
explanation:
|
||||||
|
'Think of each node as a person, and an edge as "these two people are in the same group." Initially, everyone is in their own group. When we process an edge, we merge the two groups.',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'insight', name: 'Key Insight', value: 'Each node starts as its own group' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes(), undefined, undefined, 'Each node is its own root')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'intuition-2',
|
||||||
|
phase: 'intuition',
|
||||||
|
explanation:
|
||||||
|
'If we try to connect two people who are already in the same group, that connection is redundant! It doesn\'t add any new information. This redundant connection is exactly what creates the cycle.',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'insight', name: 'Key Insight', value: 'Same group = cycle detected!' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'intuition-3',
|
||||||
|
phase: 'intuition',
|
||||||
|
explanation:
|
||||||
|
'Union-Find gives us near-constant time operations: find(x) tells us which group x belongs to (its root), and union(x,y) merges two groups. Path compression and union by rank optimize these operations.',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'time', name: 'Time', value: 'O(n * alpha(n)) ~ O(n)' },
|
||||||
|
{ id: 'space', name: 'Space', value: 'O(n)' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 3: Pattern (3 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'pattern-1',
|
||||||
|
phase: 'pattern',
|
||||||
|
explanation:
|
||||||
|
'Union-Find uses two arrays: parent[x] stores the parent of node x (initially itself), and rank[x] stores the tree height for union by rank optimization.',
|
||||||
|
codeLine: 3,
|
||||||
|
codeHighlightLines: [2, 3, 4],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'parent', name: 'parent', value: '[_, 1, 2, 3]' },
|
||||||
|
{ id: 'rank', name: 'rank', value: '[0, 0, 0, 0]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes(), undefined, undefined, 'parent[i] = i initially')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pattern-2',
|
||||||
|
phase: 'pattern',
|
||||||
|
explanation:
|
||||||
|
'find(x) traverses parent pointers until reaching a root (where parent[x] == x). Path compression flattens the tree by making every node on the path point directly to the root.',
|
||||||
|
codeLine: 6,
|
||||||
|
codeHighlightLines: [6, 7, 8, 9],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'op', name: 'find(x)', value: 'Returns root of x\'s tree' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pattern-3',
|
||||||
|
phase: 'pattern',
|
||||||
|
explanation:
|
||||||
|
'union(x,y) finds roots of both nodes. If same root, they\'re already connected (return False). Otherwise, merge by making one root point to the other, preferring higher rank.',
|
||||||
|
codeLine: 11,
|
||||||
|
codeHighlightLines: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'op', name: 'union(x,y)', value: 'Merge two groups' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 4: Code (3 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'code-1',
|
||||||
|
phase: 'code',
|
||||||
|
explanation:
|
||||||
|
'Initialize parent array: each node is its own parent (root). Initialize rank array to all zeros. This represents n separate single-node trees.',
|
||||||
|
codeLine: 3,
|
||||||
|
codeHighlightLines: [2, 3, 4],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'parent', name: 'parent', value: '[_, 1, 2, 3]' },
|
||||||
|
{ id: 'rank', name: 'rank', value: '[_, 0, 0, 0]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'code-2',
|
||||||
|
phase: 'code',
|
||||||
|
explanation:
|
||||||
|
'The find function with path compression: recursively find the root, then update parent[x] to point directly to root. This flattens the tree for future lookups.',
|
||||||
|
codeLine: 7,
|
||||||
|
codeHighlightLines: [6, 7, 8, 9],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'code-3',
|
||||||
|
phase: 'code',
|
||||||
|
explanation:
|
||||||
|
'Process each edge: try to union the two nodes. If union returns False, both nodes already have the same root, so this edge creates a cycle. Return it as the redundant edge.',
|
||||||
|
codeLine: 22,
|
||||||
|
codeHighlightLines: [22, 23, 24],
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(initialNodes())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 5: Execution - Process edge [1,2] (4 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'exec-edge1-1',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Process edge [1,2]. First, find(1) returns 1 (node 1 is its own root).',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 2]' },
|
||||||
|
{ id: 'find1', name: 'find(1)', value: '1' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 0, 'finding'),
|
||||||
|
createNode(NODE_2, 2, null, 0, 'root'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'root'),
|
||||||
|
],
|
||||||
|
[1, 2],
|
||||||
|
[NODE_1]
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge1-2',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'find(2) returns 2 (node 2 is its own root). Since find(1) != find(2), they\'re in different groups.',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 2]' },
|
||||||
|
{ id: 'find1', name: 'find(1)', value: '1' },
|
||||||
|
{ id: 'find2', name: 'find(2)', value: '2' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 0, 'root'),
|
||||||
|
createNode(NODE_2, 2, null, 0, 'finding'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'root'),
|
||||||
|
],
|
||||||
|
[1, 2],
|
||||||
|
[NODE_2],
|
||||||
|
'Different roots: 1 != 2'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge1-3',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Different roots mean different groups. Union them by making node 2\'s root (itself) point to node 1\'s root (itself). Node 1 becomes the root of both.',
|
||||||
|
codeLine: 17,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 2]' },
|
||||||
|
{ id: 'action', name: 'action', value: 'union(1, 2)' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 0, 'merging'),
|
||||||
|
createNode(NODE_2, 2, null, 0, 'merging'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'root'),
|
||||||
|
],
|
||||||
|
[1, 2],
|
||||||
|
undefined,
|
||||||
|
'Merging groups...'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge1-4',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'After union: node 2\'s parent is now node 1. Node 1\'s rank increases to 1. Nodes 1 and 2 are now in the same group.',
|
||||||
|
codeLine: 20,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'parent', name: 'parent', value: '[_, 1, 1, 3]' },
|
||||||
|
{ id: 'rank', name: 'rank', value: '[_, 1, 0, 0]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
afterUnion12({ [NODE_1]: 'highlighted' }),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'Union complete: 2 -> 1'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 5: Execution - Process edge [1,3] (4 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'exec-edge2-1',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Process edge [1,3]. find(1) returns 1 (node 1 is its own root).',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 3]' },
|
||||||
|
{ id: 'find1', name: 'find(1)', value: '1' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'finding'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'normal'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'root'),
|
||||||
|
],
|
||||||
|
[1, 3],
|
||||||
|
[NODE_1]
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge2-2',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'find(3) returns 3 (node 3 is its own root). Since find(1) != find(3), they\'re in different groups.',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 3]' },
|
||||||
|
{ id: 'find1', name: 'find(1)', value: '1' },
|
||||||
|
{ id: 'find3', name: 'find(3)', value: '3' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'root'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'normal'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'finding'),
|
||||||
|
],
|
||||||
|
[1, 3],
|
||||||
|
[NODE_3],
|
||||||
|
'Different roots: 1 != 3'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge2-3',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Different roots mean different groups. Union them. Since rank[1] = 1 > rank[3] = 0, node 3\'s root points to node 1.',
|
||||||
|
codeLine: 17,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[1, 3]' },
|
||||||
|
{ id: 'action', name: 'action', value: 'union(1, 3)' },
|
||||||
|
{ id: 'ranks', name: 'ranks', value: 'rank[1]=1 > rank[3]=0' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'merging'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'normal'),
|
||||||
|
createNode(NODE_3, 3, null, 0, 'merging'),
|
||||||
|
],
|
||||||
|
[1, 3],
|
||||||
|
undefined,
|
||||||
|
'Merging groups (by rank)...'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge2-4',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'After union: node 3\'s parent is now node 1. All three nodes are in the same group with node 1 as root.',
|
||||||
|
codeLine: 20,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'parent', name: 'parent', value: '[_, 1, 1, 1]' },
|
||||||
|
{ id: 'rank', name: 'rank', value: '[_, 1, 0, 0]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
afterUnion13({ [NODE_1]: 'highlighted' }),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'Union complete: 3 -> 1'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Phase 5: Execution - Process edge [2,3] - Cycle Detection (5 steps)
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'exec-edge3-1',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Process edge [2,3]. find(2) follows parent pointer: 2 -> 1. Returns 1.',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[2, 3]' },
|
||||||
|
{ id: 'find2', name: 'find(2)', value: '2 -> 1' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'highlighted'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'finding'),
|
||||||
|
createNode(NODE_3, 3, NODE_1, 0, 'normal'),
|
||||||
|
],
|
||||||
|
[2, 3],
|
||||||
|
[NODE_2, NODE_1],
|
||||||
|
'Traversing: 2 -> 1'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge3-2',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'find(3) follows parent pointer: 3 -> 1. Returns 1.',
|
||||||
|
codeLine: 12,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[2, 3]' },
|
||||||
|
{ id: 'find2', name: 'find(2)', value: '1' },
|
||||||
|
{ id: 'find3', name: 'find(3)', value: '3 -> 1' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'highlighted'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'normal'),
|
||||||
|
createNode(NODE_3, 3, NODE_1, 0, 'finding'),
|
||||||
|
],
|
||||||
|
[2, 3],
|
||||||
|
[NODE_3, NODE_1],
|
||||||
|
'Traversing: 3 -> 1'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge3-3',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'find(2) = 1 and find(3) = 1. Same root! Both nodes are already in the same group.',
|
||||||
|
codeLine: 13,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[2, 3]' },
|
||||||
|
{ id: 'find2', name: 'find(2)', value: '1' },
|
||||||
|
{ id: 'find3', name: 'find(3)', value: '1' },
|
||||||
|
{ id: 'same', name: 'px == py', value: 'True!' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'cycle'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'cycle'),
|
||||||
|
createNode(NODE_3, 3, NODE_1, 0, 'cycle'),
|
||||||
|
],
|
||||||
|
[2, 3],
|
||||||
|
undefined,
|
||||||
|
'SAME ROOT! Cycle detected!'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge3-4',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'This edge [2,3] would connect two nodes already in the same connected component. Adding it would create a cycle. This is our redundant edge!',
|
||||||
|
codeLine: 14,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'edge', name: 'edge', value: '[2, 3]' },
|
||||||
|
{ id: 'verdict', name: 'verdict', value: 'REDUNDANT!' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
[
|
||||||
|
createNode(NODE_1, 1, null, 1, 'normal'),
|
||||||
|
createNode(NODE_2, 2, NODE_1, 0, 'cycle'),
|
||||||
|
createNode(NODE_3, 3, NODE_1, 0, 'cycle'),
|
||||||
|
],
|
||||||
|
[2, 3],
|
||||||
|
undefined,
|
||||||
|
'Edge [2,3] creates a cycle!'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exec-edge3-5',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Return [2, 3] as the redundant edge. Removing it restores the tree structure with edges [1,2] and [1,3].',
|
||||||
|
codeLine: 24,
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'result', name: 'return', value: '[2, 3]' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(
|
||||||
|
afterUnion13({ [NODE_2]: 'highlighted', [NODE_3]: 'highlighted' }),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'Redundant edge found!'
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Final Summary
|
||||||
|
// ==========================================
|
||||||
|
{
|
||||||
|
id: 'exec-done',
|
||||||
|
phase: 'execution',
|
||||||
|
explanation:
|
||||||
|
'Complete! Union-Find detected that edge [2,3] would create a cycle because both nodes already shared the same root. Time: O(n * alpha(n)) ~ O(n), Space: O(n).',
|
||||||
|
dataState: {
|
||||||
|
arrays: [],
|
||||||
|
pointers: [],
|
||||||
|
variables: [
|
||||||
|
{ id: 'result', name: 'result', value: '[2, 3]' },
|
||||||
|
{ id: 'time', name: 'complexity', value: 'O(n * alpha(n)) ~ O(n)' },
|
||||||
|
{ id: 'space', name: 'space', value: 'O(n)' },
|
||||||
|
],
|
||||||
|
calculations: [],
|
||||||
|
unionFind: [createUnionFindState(afterUnion13())],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -95,6 +95,10 @@ export interface DataState {
|
|||||||
heaps?: HeapState[];
|
heaps?: HeapState[];
|
||||||
// Interval support
|
// Interval support
|
||||||
intervals?: IntervalListState[];
|
intervals?: IntervalListState[];
|
||||||
|
// Trie support
|
||||||
|
tries?: TrieState[];
|
||||||
|
// Union-Find support
|
||||||
|
unionFind?: UnionFindState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Single step in the visualization */
|
/** Single step in the visualization */
|
||||||
@@ -353,3 +357,50 @@ export interface IntervalListState {
|
|||||||
minValue?: number; // Timeline range
|
minValue?: number; // Timeline range
|
||||||
maxValue?: number;
|
maxValue?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Trie Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/** State of a trie node */
|
||||||
|
export interface TrieNodeState {
|
||||||
|
id: string;
|
||||||
|
char: string; // Character this node represents (empty for root)
|
||||||
|
state: 'normal' | 'current' | 'found' | 'notfound' | 'highlighted' | 'creating';
|
||||||
|
isEndOfWord: boolean;
|
||||||
|
children: string[]; // IDs of child nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Complete trie state */
|
||||||
|
export interface TrieState {
|
||||||
|
id: string;
|
||||||
|
nodes: TrieNodeState[];
|
||||||
|
rootId: string;
|
||||||
|
label?: string;
|
||||||
|
currentPath?: string[]; // Node IDs in current traversal
|
||||||
|
searchWord?: string; // Word being processed
|
||||||
|
searchIndex?: number; // Current character index
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Union-Find Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/** State of a union-find node */
|
||||||
|
export interface UnionFindNodeState {
|
||||||
|
id: string;
|
||||||
|
value: number; // Element value (1-indexed for this problem)
|
||||||
|
parentId: string | null; // Parent pointer (null = self/root)
|
||||||
|
rank: number; // For union by rank optimization
|
||||||
|
state: 'normal' | 'root' | 'finding' | 'compressing' | 'merging' | 'highlighted' | 'cycle';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Complete union-find forest state */
|
||||||
|
export interface UnionFindState {
|
||||||
|
id: string;
|
||||||
|
nodes: UnionFindNodeState[];
|
||||||
|
label?: string;
|
||||||
|
currentEdge?: [number, number]; // Edge being processed
|
||||||
|
findPath?: string[]; // Path during find operation
|
||||||
|
message?: string; // Operation status message
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user