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

This commit is contained in:
2025-09-03 21:47:33 +01:00
parent a9d71de4b1
commit 6c4b827c36
15 changed files with 3063 additions and 1 deletions

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]]),
],
},
},
],
};