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