feat(viz): union-find

This commit is contained in:
2025-09-03 22:44:24 +01:00
parent b255ccba32
commit 7c2232e27c
7 changed files with 1273 additions and 1 deletions
@@ -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())],
},
},
],
};