Practice Focus
Level 3: Intermediate Advanced - Master complex problem-solving strategies and interview preparation
Practice Focus
Complex Problem Decomposition
Breaking Down Complex Problems
Complex problems often require systematic decomposition into smaller, manageable subproblems. This involves identifying patterns, recognizing familiar structures, and applying appropriate algorithms.
🏗️ Real-Life Analogy:
Think of building a house: you don't build everything at once. You break it down into foundation, framing, plumbing, electrical, and finishing. Each part has its own requirements and can be tackled separately.
🎯 Decomposition Strategies:
- Identify Core Problem: Strip away details to find the fundamental challenge
- Recognize Patterns: Map to known problem types (graph, DP, greedy, etc.)
- Define Subproblems: Break into smaller, solvable pieces
- Build Solutions: Combine subproblem solutions systematically
TypeScript
// Example: Word Break Problem Decomposition // Problem: Can we break string s into words from dictionary? // Step 1: Identify core problem - sequence segmentation // Step 2: Recognize pattern - dynamic programming // Step 3: Define subproblems - can break s[0:i]? // Step 4: Build solution - combine smaller solutions class WordBreakSolver { // Approach 1: Top-down with memoization wordBreakMemo(s: string, wordDict: string[]): boolean { const wordSet = new Set(wordDict); const memo = new Map(); return this.canBreak(s, 0, wordSet, memo); } private canBreak(s: string, start: number, wordSet: Set , memo: Map ): boolean { // Base case if (start === s.length) return true; // Check memo if (memo.has(start)) return memo.get(start)!; // Try all possible word endings for (let end = start + 1; end <= s.length; end++) { const word = s.substring(start, end); if (wordSet.has(word) && this.canBreak(s, end, wordSet, memo)) { memo.set(start, true); return true; } } memo.set(start, false); return false; } // Approach 2: Bottom-up dynamic programming wordBreakDP(s: string, wordDict: string[]): boolean { const wordSet = new Set(wordDict); const n = s.length; const dp = new Array(n + 1).fill(false); dp[0] = true; // Empty string for (let i = 1; i <= n; i++) { for (let j = 0; j < i; j++) { if (dp[j] && wordSet.has(s.substring(j, i))) { dp[i] = true; break; } } } return dp[n]; } // Approach 3: BFS for finding all solutions wordBreakAllSolutions(s: string, wordDict: string[]): string[][] { const wordSet = new Set(wordDict); const memo = new Map (); return this.findAllBreaks(s, 0, wordSet, memo); } private findAllBreaks(s: string, start: number, wordSet: Set , memo: Map ): string[][] { if (memo.has(start)) return memo.get(start)!; const result: string[][] = []; if (start === s.length) { result.push([]); return result; } for (let end = start + 1; end <= s.length; end++) { const word = s.substring(start, end); if (wordSet.has(word)) { const suffixBreaks = this.findAllBreaks(s, end, wordSet, memo); for (const suffix of suffixBreaks) { result.push([word, ...suffix]); } } } memo.set(start, result); return result; } } // Example: Complex scheduling problem // Problem: Schedule tasks with dependencies and resource constraints interface Task { id: string; duration: number; dependencies: string[]; resources: string[]; } class TaskScheduler { // Decomposition: // 1. Topological sort for dependencies // 2. Resource conflict resolution // 3. Time optimization scheduleTask(tasks: Task[]): Map { // Step 1: Build dependency graph const graph = this.buildDependencyGraph(tasks); // Step 2: Topological sort const order = this.topologicalSort(graph); // Step 3: Schedule with resource constraints return this.scheduleWithResources(tasks, order); } private buildDependencyGraph(tasks: Task[]): Map { const graph = new Map (); const taskMap = new Map (); for (const task of tasks) { taskMap.set(task.id, task); graph.set(task.id, []); } for (const task of tasks) { for (const dep of task.dependencies) { graph.get(dep)!.push(task.id); } } return graph; } private topologicalSort(graph: Map ): string[] { const inDegree = new Map (); const queue: string[] = []; const result: string[] = []; // Calculate in-degrees for (const [node, _] of graph) { inDegree.set(node, 0); } for (const [_, neighbors] of graph) { for (const neighbor of neighbors) { inDegree.set(neighbor, inDegree.get(neighbor)! + 1); } } // Find nodes with no dependencies for (const [node, degree] of inDegree) { if (degree === 0) queue.push(node); } // Process queue while (queue.length > 0) { const current = queue.shift()!; result.push(current); for (const neighbor of graph.get(current)!) { const newDegree = inDegree.get(neighbor)! - 1; inDegree.set(neighbor, newDegree); if (newDegree === 0) queue.push(neighbor); } } return result; } private scheduleWithResources(tasks: Task[], order: string[]): Map { const schedule = new Map (); const resourceAvailable = new Map (); const taskMap = new Map (); for (const task of tasks) { taskMap.set(task.id, task); } for (const taskId of order) { const task = taskMap.get(taskId)!; let startTime = 0; // Check dependencies for (const dep of task.dependencies) { const depEnd = schedule.get(dep)! + taskMap.get(dep)!.duration; startTime = Math.max(startTime, depEnd); } // Check resource availability for (const resource of task.resources) { const available = resourceAvailable.get(resource) || 0; startTime = Math.max(startTime, available); } // Schedule task schedule.set(taskId, startTime); // Update resource availability for (const resource of task.resources) { resourceAvailable.set(resource, startTime + task.duration); } } return schedule; } }
Python
# Problem Decomposition Example: Island Problems # Shows how similar problems can be solved with pattern recognition class IslandProblemSolver: """Demonstrates decomposition of various island-related problems""" def __init__(self): self.directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # Problem 1: Number of Islands def num_islands(self, grid): """Count number of islands in grid""" if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) visited = [[False] * cols for _ in range(rows)] count = 0 def dfs(r, c): if (r < 0 or r >= rows or c < 0 or c >= cols or visited[r][c] or grid[r][c] == '0'): return visited[r][c] = True for dr, dc in self.directions: dfs(r + dr, c + dc) for r in range(rows): for c in range(cols): if grid[r][c] == '1' and not visited[r][c]: dfs(r, c) count += 1 return count # Problem 2: Max Area of Island def max_area_of_island(self, grid): """Find maximum area of any island""" if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) max_area = 0 def dfs(r, c): if (r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == 0): return 0 grid[r][c] = 0 # Mark visited area = 1 for dr, dc in self.directions: area += dfs(r + dr, c + dc) return area for r in range(rows): for c in range(cols): if grid[r][c] == 1: max_area = max(max_area, dfs(r, c)) return max_area # Problem 3: Island Perimeter def island_perimeter(self, grid): """Calculate perimeter of the island""" if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) perimeter = 0 for r in range(rows): for c in range(cols): if grid[r][c] == 1: # Check all 4 sides for dr, dc in self.directions: nr, nc = r + dr, c + dc # Add to perimeter if edge or water if (nr < 0 or nr >= rows or nc < 0 or nc >= cols or grid[nr][nc] == 0): perimeter += 1 return perimeter # Problem 4: Surrounded Regions (capture problem) def solve_surrounded_regions(self, board): """Capture regions surrounded by 'X'""" if not board or not board[0]: return rows, cols = len(board), len(board[0]) def dfs(r, c): if (r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != 'O'): return board[r][c] = 'E' # Mark edge-connected for dr, dc in self.directions: dfs(r + dr, c + dc) # Mark edge-connected regions for r in range(rows): dfs(r, 0) dfs(r, cols - 1) for c in range(cols): dfs(0, c) dfs(rows - 1, c) # Capture surrounded regions for r in range(rows): for c in range(cols): if board[r][c] == 'O': board[r][c] = 'X' elif board[r][c] == 'E': board[r][c] = 'O' # Complex Problem Decomposition: Meeting Scheduler class MeetingScheduler: """Demonstrates decomposition of scheduling problems""" def find_meeting_time(self, calendars, meeting_duration): """ Find common free time for meeting Decomposition: 1. Merge individual calendars 2. Find free slots 3. Check duration constraint """ # Step 1: Merge all busy times all_busy = [] for calendar in calendars: all_busy.extend(calendar) # Step 2: Sort and merge overlapping intervals all_busy.sort() merged = [] for start, end in all_busy: if not merged or merged[-1][1] < start: merged.append([start, end]) else: merged[-1][1] = max(merged[-1][1], end) # Step 3: Find gaps of sufficient duration free_slots = [] # Check before first meeting if merged and merged[0][0] >= meeting_duration: free_slots.append([0, merged[0][0]]) # Check between meetings for i in range(1, len(merged)): gap_start = merged[i-1][1] gap_end = merged[i][0] if gap_end - gap_start >= meeting_duration: free_slots.append([gap_start, gap_end]) # Check after last meeting (assuming day ends at 1440 minutes) if merged and 1440 - merged[-1][1] >= meeting_duration: free_slots.append([merged[-1][1], 1440]) return free_slots def schedule_interviews(self, candidates, interviewers, duration): """ Complex scheduling with multiple constraints Decomposition: 1. Model as bipartite matching 2. Apply constraints 3. Optimize for preferences """ from collections import defaultdict # Build availability graph graph = defaultdict(list) for candidate in candidates: for interviewer in interviewers: # Check if they have common available time common_slots = self.find_common_slots( candidate['available'], interviewer['available'], duration ) if common_slots: graph[candidate['id']].append({ 'interviewer': interviewer['id'], 'slots': common_slots, 'preference': candidate['preferences'].get(interviewer['id'], 0) }) # Use Hungarian algorithm or greedy matching assignments = self.match_interviews(graph, candidates, interviewers) return assignments def find_common_slots(self, slots1, slots2, duration): """Find overlapping time slots of sufficient duration""" common = [] i = j = 0 while i < len(slots1) and j < len(slots2): start = max(slots1[i][0], slots2[j][0]) end = min(slots1[i][1], slots2[j][1]) if end - start >= duration: common.append([start, end]) if slots1[i][1] < slots2[j][1]: i += 1 else: j += 1 return common
OCaml
(* Problem Decomposition Patterns *) (* Pattern 1: Recursive decomposition with memoization *) module WordBreak = struct let word_break s word_dict = let word_set = List.fold_left (fun set w -> StringSet.add w set ) StringSet.empty word_dict in let memo = Hashtbl.create 100 in let rec can_break start = if start = String.length s then true else if Hashtbl.mem memo start then Hashtbl.find memo start else let result = ref false in for end_pos = start + 1 to String.length s do let word = String.sub s start (end_pos - start) in if StringSet.mem word word_set && can_break end_pos then result := true done; Hashtbl.add memo start !result; !result in can_break 0 (* Find all possible word breaks *) let all_word_breaks s word_dict = let word_set = List.fold_left (fun set w -> StringSet.add w set ) StringSet.empty word_dict in let memo = Hashtbl.create 100 in let rec find_breaks start = if Hashtbl.mem memo start then Hashtbl.find memo start else let results = if start = String.length s then [[]] else let all_results = ref [] in for end_pos = start + 1 to String.length s do let word = String.sub s start (end_pos - start) in if StringSet.mem word word_set then let suffix_breaks = find_breaks end_pos in List.iter (fun suffix -> all_results := (word :: suffix) :: !all_results ) suffix_breaks done; !all_results in Hashtbl.add memo start results; results in find_breaks 0 end (* Pattern 2: Graph-based decomposition *) module TaskScheduler = struct type task = { id: string; duration: int; dependencies: string list; resources: string list; } type schedule = (string * int) list let schedule_tasks tasks = (* Step 1: Build dependency graph *) let graph = Hashtbl.create (List.length tasks) in let in_degree = Hashtbl.create (List.length tasks) in List.iter (fun task -> Hashtbl.add graph task.id []; Hashtbl.add in_degree task.id 0 ) tasks; List.iter (fun task -> List.iter (fun dep -> let deps = Hashtbl.find graph dep in Hashtbl.replace graph dep (task.id :: deps); let degree = Hashtbl.find in_degree task.id in Hashtbl.replace in_degree task.id (degree + 1) ) task.dependencies ) tasks; (* Step 2: Topological sort *) let queue = Queue.create () in Hashtbl.iter (fun id degree -> if degree = 0 then Queue.push id queue ) in_degree; let order = ref [] in while not (Queue.is_empty queue) do let current = Queue.pop queue in order := current :: !order; List.iter (fun neighbor -> let degree = Hashtbl.find in_degree neighbor - 1 in Hashtbl.replace in_degree neighbor degree; if degree = 0 then Queue.push neighbor queue ) (Hashtbl.find graph current) done; (* Step 3: Schedule with resource constraints *) let schedule = Hashtbl.create (List.length tasks) in let resource_available = Hashtbl.create 10 in let task_map = List.fold_left (fun map task -> StringMap.add task.id task map ) StringMap.empty tasks in List.iter (fun task_id -> let task = StringMap.find task_id task_map in let start_time = ref 0 in (* Check dependencies *) List.iter (fun dep -> let dep_task = StringMap.find dep task_map in let dep_end = (Hashtbl.find schedule dep) + dep_task.duration in start_time := max !start_time dep_end ) task.dependencies; (* Check resources *) List.iter (fun resource -> let available = try Hashtbl.find resource_available resource with Not_found -> 0 in start_time := max !start_time available ) task.resources; (* Schedule task *) Hashtbl.add schedule task_id !start_time; (* Update resources *) List.iter (fun resource -> Hashtbl.replace resource_available resource (!start_time + task.duration) ) task.resources ) (List.rev !order); schedule end (* Pattern 3: Dynamic programming decomposition *) module IntervalScheduling = struct type interval = { start_time: int; end_time: int; value: int; } (* Maximum value interval scheduling *) let max_value_schedule intervals = (* Sort by end time *) let sorted = List.sort (fun a b -> compare a.end_time b.end_time ) intervals in let n = List.length sorted in let arr = Array.of_list sorted in let dp = Array.make n 0 in (* Find latest non-overlapping interval *) let find_latest_non_overlapping i = let rec binary_search low high = if low > high then -1 else let mid = (low + high) / 2 in if arr.(mid).end_time <= arr.(i).start_time then if mid + 1 <= high && arr.(mid + 1).end_time <= arr.(i).start_time then binary_search (mid + 1) high else mid else binary_search low (mid - 1) in binary_search 0 (i - 1) in (* Fill DP table *) dp.(0) <- arr.(0).value; for i = 1 to n - 1 do let include_current = arr.(i).value in let latest = find_latest_non_overlapping i in let include_value = if latest >= 0 then include_current + dp.(latest) else include_current in dp.(i) <- max include_value dp.(i - 1) done; dp.(n - 1) end
Algorithm Design Patterns
Common Algorithm Design Patterns
Recognizing and applying common algorithm design patterns can dramatically simplify problem solving. These patterns provide templates for approaching various problem types.
🎨 Real-Life Analogy:
Like an artist who knows different painting techniques (watercolor, oil, acrylic), a programmer should know different algorithm patterns and when to apply each one.
🔧 Key Design Patterns:
- Two Pointers: Linear traversal with multiple positions
- Sliding Window: Maintain a range while moving through data
- Fast & Slow Pointers: Cycle detection and middle finding
- Merge Intervals: Combining overlapping ranges
- Top K Elements: Using heaps for selection
Pattern | When to Use | Time Complexity | Example Problems |
---|---|---|---|
Two Pointers | Sorted arrays, pairs | O(n) | Two Sum, Container with Water |
Sliding Window | Subarray/substring | O(n) | Max Sum Subarray, Longest Substring |
Fast & Slow | Cycle detection | O(n) | Linked List Cycle, Happy Number |
Merge Intervals | Overlapping ranges | O(n log n) | Meeting Rooms, Insert Interval |
Top K Elements | Finding k best/worst | O(n log k) | K Closest Points, Top K Frequent |
Optimization Techniques
Advanced Optimization Strategies
Optimization involves improving algorithm performance through various techniques including space-time tradeoffs, preprocessing, caching, and algorithmic improvements.
🏎️ Real-Life Analogy:
Think of optimizing like tuning a race car: you can improve the engine (algorithm), reduce weight (space), add better tires (data structures), or find a shorter track (preprocessing).
⚡ Optimization Strategies:
- Preprocessing: Compute once, use many times
- Caching/Memoization: Store computed results
- Space-Time Tradeoff: Use more memory for speed
- Early Termination: Stop when answer is found
- Pruning: Skip unnecessary computations
TypeScript
// Optimization Example: Range Query Problems // Problem: Answer multiple range sum queries // Naive: O(n) per query // Optimized: O(1) per query with O(n) preprocessing class RangeQueryOptimizer { // Technique 1: Prefix Sum for Range Sum private prefixSum: number[]; constructor(nums: number[]) { this.prefixSum = new Array(nums.length + 1).fill(0); for (let i = 0; i < nums.length; i++) { this.prefixSum[i + 1] = this.prefixSum[i] + nums[i]; } } sumRange(left: number, right: number): number { return this.prefixSum[right + 1] - this.prefixSum[left]; } } // Technique 2: Sparse Table for Range Minimum class SparseTable { private table: number[][]; private logs: number[]; constructor(nums: number[]) { const n = nums.length; const k = Math.floor(Math.log2(n)) + 1; // Precompute logarithms this.logs = new Array(n + 1).fill(0); for (let i = 2; i <= n; i++) { this.logs[i] = this.logs[Math.floor(i / 2)] + 1; } // Build sparse table this.table = Array(k).fill(null).map(() => Array(n).fill(0)); // Initialize for length 1 for (let i = 0; i < n; i++) { this.table[0][i] = nums[i]; } // Build for other lengths for (let j = 1; j < k; j++) { for (let i = 0; i + (1 << j) <= n; i++) { this.table[j][i] = Math.min( this.table[j - 1][i], this.table[j - 1][i + (1 << (j - 1))] ); } } } rangeMin(left: number, right: number): number { const len = right - left + 1; const k = this.logs[len]; return Math.min( this.table[k][left], this.table[k][right - (1 << k) + 1] ); } } // Technique 3: Binary Indexed Tree for Dynamic Updates class OptimizedBIT { private tree: number[]; private nums: number[]; constructor(nums: number[]) { this.nums = [...nums]; this.tree = new Array(nums.length + 1).fill(0); // Optimized construction O(n) for (let i = 0; i < nums.length; i++) { this.updateDelta(i, nums[i]); } } private updateDelta(index: number, delta: number): void { index++; while (index < this.tree.length) { this.tree[index] += delta; index += index & (-index); } } update(index: number, value: number): void { const delta = value - this.nums[index]; this.nums[index] = value; this.updateDelta(index, delta); } sumRange(left: number, right: number): number { return this.prefixSum(right) - (left > 0 ? this.prefixSum(left - 1) : 0); } private prefixSum(index: number): number { index++; let sum = 0; while (index > 0) { sum += this.tree[index]; index -= index & (-index); } return sum; } } // Technique 4: Optimization through Bit Manipulation class BitOptimizations { // Count set bits using Brian Kernighan's algorithm countSetBits(n: number): number { let count = 0; while (n) { n &= n - 1; // Clear rightmost set bit count++; } return count; } // Find missing number in O(n) time, O(1) space findMissingNumber(nums: number[]): number { let xor = 0; // XOR all array elements for (const num of nums) { xor ^= num; } // XOR with all numbers from 0 to n for (let i = 0; i <= nums.length; i++) { xor ^= i; } return xor; } // Check if number is power of 2 isPowerOfTwo(n: number): boolean { return n > 0 && (n & (n - 1)) === 0; } } // Technique 5: String Matching Optimization class StringOptimizer { // Rolling Hash for substring matching findSubstringRollingHash(text: string, pattern: string): number[] { const matches: number[] = []; const base = 256; const prime = 101; const m = pattern.length; const n = text.length; if (m > n) return matches; let patternHash = 0; let textHash = 0; let h = 1; // Precompute h = base^(m-1) % prime for (let i = 0; i < m - 1; i++) { h = (h * base) % prime; } // Initial hashes for (let i = 0; i < m; i++) { patternHash = (base * patternHash + pattern.charCodeAt(i)) % prime; textHash = (base * textHash + text.charCodeAt(i)) % prime; } // Slide and check for (let i = 0; i <= n - m; i++) { if (patternHash === textHash) { // Verify to avoid hash collision if (text.substring(i, i + m) === pattern) { matches.push(i); } } if (i < n - m) { textHash = (base * (textHash - text.charCodeAt(i) * h) + text.charCodeAt(i + m)) % prime; if (textHash < 0) textHash += prime; } } return matches; } }
Python
# Optimization Techniques Examples import bisect from collections import deque import heapq class OptimizationTechniques: """Collection of optimization techniques for common problems""" # Technique 1: Binary Search Optimization def search_in_rotated_array(self, nums, target): """Find target in rotated sorted array - O(log n)""" left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid # Determine which half is sorted if nums[left] <= nums[mid]: # Left half is sorted if nums[left] <= target < nums[mid]: right = mid - 1 else: left = mid + 1 else: # Right half is sorted if nums[mid] < target <= nums[right]: left = mid + 1 else: right = mid - 1 return -1 # Technique 2: Monotonic Stack Optimization def next_greater_elements(self, nums): """Find next greater element for each element - O(n)""" n = len(nums) result = [-1] * n stack = [] # Monotonic decreasing stack # Process array twice for circular array for i in range(2 * n): while stack and nums[stack[-1]] < nums[i % n]: result[stack.pop()] = nums[i % n] if i < n: stack.append(i) return result # Technique 3: Sliding Window with Deque def max_sliding_window(self, nums, k): """Maximum in sliding window - O(n)""" if not nums: return [] dq = deque() # Store indices result = [] for i in range(len(nums)): # Remove elements outside window while dq and dq[0] <= i - k: dq.popleft() # Remove smaller elements while dq and nums[dq[-1]] < nums[i]: dq.pop() dq.append(i) # Add to result after first window if i >= k - 1: result.append(nums[dq[0]]) return result # Technique 4: Bucket Sort Optimization def top_k_frequent_bucket(self, nums, k): """Top K frequent elements using buckets - O(n)""" from collections import Counter count = Counter(nums) buckets = [[] for _ in range(len(nums) + 1)] # Group by frequency for num, freq in count.items(): buckets[freq].append(num) # Collect top k result = [] for i in range(len(buckets) - 1, -1, -1): result.extend(buckets[i]) if len(result) >= k: return result[:k] return result # Technique 5: Matrix Optimization def search_2d_matrix(self, matrix, target): """Search in row and column sorted matrix - O(m + n)""" if not matrix or not matrix[0]: return False rows, cols = len(matrix), len(matrix[0]) row, col = 0, cols - 1 # Start from top-right while row < rows and col >= 0: if matrix[row][col] == target: return True elif matrix[row][col] > target: col -= 1 else: row += 1 return False # Advanced Caching Techniques class LRUCache: """Least Recently Used Cache with O(1) operations""" class Node: def __init__(self, key=0, value=0): self.key = key self.value = value self.prev = None self.next = None def __init__(self, capacity): self.capacity = capacity self.cache = {} # key -> node self.head = self.Node() # Dummy head self.tail = self.Node() # Dummy tail self.head.next = self.tail self.tail.prev = self.head def _add_to_head(self, node): """Add node right after head""" node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _remove_node(self, node): """Remove node from list""" prev_node = node.prev next_node = node.next prev_node.next = next_node next_node.prev = prev_node def get(self, key): if key not in self.cache: return -1 # Move to head (mark as recently used) node = self.cache[key] self._remove_node(node) self._add_to_head(node) return node.value def put(self, key, value): if key in self.cache: # Update existing node = self.cache[key] node.value = value self._remove_node(node) self._add_to_head(node) else: # Add new node = self.Node(key, value) self.cache[key] = node self._add_to_head(node) if len(self.cache) > self.capacity: # Remove LRU lru = self.tail.prev self._remove_node(lru) del self.cache[lru.key]
OCaml
(* Optimization Techniques in OCaml *) (* Technique 1: Memoization for Dynamic Programming *) module Memoize = struct (* Generic memoization function *) let memoize f = let table = Hashtbl.create 100 in let rec memoized x = try Hashtbl.find table x with Not_found -> let result = f memoized x in Hashtbl.add table x result; result in memoized (* Example: Fibonacci with memoization *) let fib = let rec fib_helper memo n = if n <= 1 then n else memo (n - 1) + memo (n - 2) in memoize fib_helper (* Longest Common Subsequence with memoization *) let lcs s1 s2 = let n1 = String.length s1 in let n2 = String.length s2 in let memo = Hashtbl.create (n1 * n2) in let rec lcs_helper i j = if i = n1 || j = n2 then 0 else try Hashtbl.find memo (i, j) with Not_found -> let result = if s1.[i] = s2.[j] then 1 + lcs_helper (i + 1) (j + 1) else max (lcs_helper (i + 1) j) (lcs_helper i (j + 1)) in Hashtbl.add memo (i, j) result; result in lcs_helper 0 0 end (* Technique 2: Lazy Evaluation for Optimization *) module LazyOptimization = struct (* Lazy list for infinite sequences *) type 'a lazy_list = Nil | Cons of 'a * 'a lazy_list Lazy.t let rec take n = function | Nil -> [] | Cons (h, t) -> if n <= 0 then [] else h :: take (n - 1) (Lazy.force t) (* Prime numbers using Sieve of Eratosthenes *) let primes = let rec sieve = function | Nil -> Nil | Cons (p, xs) -> let rec filter n = function | Nil -> Nil | Cons (x, xs) -> if x mod n = 0 then filter n (Lazy.force xs) else Cons (x, lazy (filter n (Lazy.force xs))) in Cons (p, lazy (sieve (filter p (Lazy.force xs)))) in let rec integers_from n = Cons (n, lazy (integers_from (n + 1))) in sieve (integers_from 2) end (* Technique 3: Bit Manipulation Optimizations *) module BitOptimizations = struct (* Count set bits *) let count_bits n = let rec count n acc = if n = 0 then acc else count (n land (n - 1)) (acc + 1) in count n 0 (* Find single number (all others appear twice) *) let find_single nums = List.fold_left (lxor) 0 nums (* Power set generation using bits *) let power_set arr = let n = Array.length arr in let total = 1 lsl n in let result = ref [] in for i = 0 to total - 1 do let subset = ref [] in for j = 0 to n - 1 do if (i land (1 lsl j)) <> 0 then subset := arr.(j) :: !subset done; result := !subset :: !result done; !result end (* Technique 4: Functional Data Structure Optimizations *) module PersistentQueue = struct (* Amortized O(1) persistent queue *) type 'a queue = { front: 'a list; rear: 'a list; } let empty = { front = []; rear = [] } let enqueue x q = { q with rear = x :: q.rear } let dequeue = function | { front = []; rear = [] } -> failwith "Empty queue" | { front = h :: t; rear } -> (h, { front = t; rear }) | { front = []; rear } -> let front = List.rev rear in (List.hd front, { front = List.tl front; rear = [] }) end (* Technique 5: Parallel Processing Optimization *) module ParallelOptimization = struct (* Parallel map using Domain module (OCaml 5.0+) *) let parallel_map f lst = let n_domains = Domain.recommended_domain_count () in let chunk_size = (List.length lst + n_domains - 1) / n_domains in let rec chunks lst = match lst with | [] -> [] | _ -> let chunk, rest = try List.split_at chunk_size lst with _ -> lst, [] in chunk :: chunks rest in let work_chunks = chunks lst in let domains = List.map (fun chunk -> Domain.spawn (fun () -> List.map f chunk) ) work_chunks in List.fold_left (fun acc d -> acc @ Domain.join d ) [] domains end
Graded Problem Sets
Progressive Difficulty Training
Practice problems organized by difficulty to build skills systematically. Each problem includes solution approaches and complexity analysis.
🏋️ Real-Life Analogy:
Like training at the gym, you start with lighter weights and gradually increase. Each difficulty level builds the foundation for the next.
🟢 Easy Problems
Foundation problems that introduce core concepts and basic pattern recognition.
Problem 1: Valid Parentheses
Problem: Given a string containing '(', ')', '{', '}', '[', ']', determine if the input string is valid.
Approach: Use a stack to track opening brackets
Hint: Push opening brackets, pop and match closing brackets
Time: O(n), Space: O(n)
Key Insight: Stack naturally handles nested structures
Approach: Use a stack to track opening brackets
Hint: Push opening brackets, pop and match closing brackets
Time: O(n), Space: O(n)
Key Insight: Stack naturally handles nested structures
Problem 2: Two Sum
Problem: Find two numbers in array that add up to target.
Approach: Use hash map to store complements
Hint: For each number x, check if (target - x) exists
Time: O(n), Space: O(n)
Key Insight: Trade space for time with hash lookups
Approach: Use hash map to store complements
Hint: For each number x, check if (target - x) exists
Time: O(n), Space: O(n)
Key Insight: Trade space for time with hash lookups
Problem 3: Maximum Subarray
Problem: Find contiguous subarray with largest sum.
Approach: Kadane's algorithm - track current and global max
Hint: Reset current sum when it becomes negative
Time: O(n), Space: O(1)
Key Insight: Negative prefix never helps future sums
Approach: Kadane's algorithm - track current and global max
Hint: Reset current sum when it becomes negative
Time: O(n), Space: O(1)
Key Insight: Negative prefix never helps future sums
Problem 4: Binary Tree Inorder Traversal
Problem: Return inorder traversal of binary tree.
Approach: Recursive: left → root → right
Hint: Use stack for iterative solution
Time: O(n), Space: O(h) where h is height
Key Insight: Recursion naturally handles tree structure
Approach: Recursive: left → root → right
Hint: Use stack for iterative solution
Time: O(n), Space: O(h) where h is height
Key Insight: Recursion naturally handles tree structure
Problem 5: Merge Two Sorted Lists
Problem: Merge two sorted linked lists into one sorted list.
Approach: Two pointers, compare and advance smaller
Hint: Use dummy head to simplify edge cases
Time: O(m + n), Space: O(1)
Key Insight: Dummy nodes eliminate special cases
Approach: Two pointers, compare and advance smaller
Hint: Use dummy head to simplify edge cases
Time: O(m + n), Space: O(1)
Key Insight: Dummy nodes eliminate special cases
🟡 Medium Problems
Intermediate problems requiring pattern combination and deeper algorithmic thinking.
Problem 1: Longest Substring Without Repeating Characters
Problem: Find length of longest substring without repeating characters.
Approach: Sliding window with hash set
Hint: Expand window until duplicate, then shrink from left
Time: O(n), Space: O(min(m,n)) where m is charset size
Key Insight: Window technique for substring problems
Approach: Sliding window with hash set
Hint: Expand window until duplicate, then shrink from left
Time: O(n), Space: O(min(m,n)) where m is charset size
Key Insight: Window technique for substring problems
Problem 2: Group Anagrams
Problem: Group strings that are anagrams of each other.
Approach: Use sorted string or character count as key
Hint: Anagrams have same sorted characters
Time: O(n * k log k) where k is max string length
Key Insight: Canonical form for grouping
Approach: Use sorted string or character count as key
Hint: Anagrams have same sorted characters
Time: O(n * k log k) where k is max string length
Key Insight: Canonical form for grouping
Problem 3: 3Sum
Problem: Find all unique triplets that sum to zero.
Approach: Sort array, fix one element, two pointers for rest
Hint: Skip duplicates to avoid repeated triplets
Time: O(n²), Space: O(1)
Key Insight: Reduce to 2Sum with sorting
Approach: Sort array, fix one element, two pointers for rest
Hint: Skip duplicates to avoid repeated triplets
Time: O(n²), Space: O(1)
Key Insight: Reduce to 2Sum with sorting
Problem 4: Binary Tree Level Order Traversal
Problem: Return level-by-level traversal of binary tree.
Approach: BFS using queue, track level boundaries
Hint: Process all nodes at current level before next
Time: O(n), Space: O(w) where w is max width
Key Insight: BFS naturally processes by levels
Approach: BFS using queue, track level boundaries
Hint: Process all nodes at current level before next
Time: O(n), Space: O(w) where w is max width
Key Insight: BFS naturally processes by levels
Problem 5: Coin Change
Problem: Find minimum coins needed to make amount.
Approach: Dynamic programming - build up from smaller amounts
Hint: dp[i] = min(dp[i-coin] + 1) for all coins ≤ i
Time: O(amount × coins), Space: O(amount)
Key Insight: Optimal substructure - optimal solution contains optimal subsolutions
Approach: Dynamic programming - build up from smaller amounts
Hint: dp[i] = min(dp[i-coin] + 1) for all coins ≤ i
Time: O(amount × coins), Space: O(amount)
Key Insight: Optimal substructure - optimal solution contains optimal subsolutions
🔴 Hard Problems
Advanced problems requiring sophisticated algorithms and optimization techniques.
Problem 1: Median of Two Sorted Arrays
Problem: Find median of two sorted arrays in O(log(min(m,n))) time.
Approach: Binary search on smaller array to partition both
Hint: Ensure left partition ≤ right partition
Time: O(log(min(m,n))), Space: O(1)
Key Insight: Median divides array into equal halves
Approach: Binary search on smaller array to partition both
Hint: Ensure left partition ≤ right partition
Time: O(log(min(m,n))), Space: O(1)
Key Insight: Median divides array into equal halves
Problem 2: Trapping Rain Water
Problem: Calculate trapped rainwater given elevation map.
Approach: Two pointers from both ends, track max heights
Hint: Water level limited by minimum of left/right max
Time: O(n), Space: O(1)
Key Insight: Water trapped = min(left_max, right_max) - height
Approach: Two pointers from both ends, track max heights
Hint: Water level limited by minimum of left/right max
Time: O(n), Space: O(1)
Key Insight: Water trapped = min(left_max, right_max) - height
Problem 3: Word Ladder
Problem: Find shortest transformation sequence from start to end word.
Approach: BFS treating words as graph nodes
Hint: Each word differs by exactly one character
Time: O(M² × N) where M is word length, N is word count
Key Insight: Shortest path in unweighted graph = BFS
Approach: BFS treating words as graph nodes
Hint: Each word differs by exactly one character
Time: O(M² × N) where M is word length, N is word count
Key Insight: Shortest path in unweighted graph = BFS
Common Interview Questions
Topic-Based Question Categories
Frequently asked interview questions organized by algorithmic topics and data structures.
📚 Real-Life Analogy:
Like studying for an exam, knowing the most common question types helps you prepare effectively and recognize patterns during interviews.
Topic | Common Questions | Key Techniques | Difficulty Range |
---|---|---|---|
Arrays & Strings | Two Sum, Longest Palindrome, Rotate Array, Product Except Self | Two Pointers, Sliding Window, Hash Maps | Easy - Medium |
Linked Lists | Reverse List, Detect Cycle, Merge Lists, Remove Nth Node | Fast/Slow Pointers, Dummy Nodes | Easy - Medium |
Trees & Graphs | Tree Traversals, Validate BST, Course Schedule, Clone Graph | DFS, BFS, Recursion, Topological Sort | Medium - Hard |
Dynamic Programming | Climbing Stairs, House Robber, Edit Distance, Knapsack | Memoization, Tabulation, State Machines | Medium - Hard |
System Design | Design Cache, Rate Limiter, Chat System, Search Engine | Scalability, Load Balancing, Databases | Hard |
Optimization Challenges
Before/After Optimization Examples
Real optimization scenarios showing how to improve algorithm performance through better techniques and data structures.
🔧 Real-Life Analogy:
Like tuning a car engine for better performance, algorithm optimization involves identifying bottlenecks and applying targeted improvements.
Challenge 1: Range Sum Queries
❌ Before: Naive Approach
Problem: Answer Q range sum queries on array of size N
Approach: For each query, iterate through range
Time: O(Q × N), Space: O(1)
Issue: Recalculating same sums repeatedly
Approach: For each query, iterate through range
Time: O(Q × N), Space: O(1)
Issue: Recalculating same sums repeatedly
✅ After: Prefix Sum Optimization
Approach: Precompute prefix sums, answer queries in O(1)
Time: O(N + Q), Space: O(N)
Improvement: 1000x faster for large Q
Trade-off: Extra space for dramatic time savings
Time: O(N + Q), Space: O(N)
Improvement: 1000x faster for large Q
Trade-off: Extra space for dramatic time savings
Challenge 2: Finding Duplicates
❌ Before: Nested Loops
Problem: Find if array contains duplicates
Approach: Compare every pair of elements
Time: O(N²), Space: O(1)
Issue: Quadratic time complexity
Approach: Compare every pair of elements
Time: O(N²), Space: O(1)
Issue: Quadratic time complexity
✅ After: Hash Set Optimization
Approach: Use hash set to track seen elements
Time: O(N), Space: O(N)
Improvement: Linear vs quadratic time
Trade-off: Space for time - classic optimization pattern
Time: O(N), Space: O(N)
Improvement: Linear vs quadratic time
Trade-off: Space for time - classic optimization pattern
Challenge 3: Top K Elements
❌ Before: Sort Everything
Problem: Find K largest elements in array
Approach: Sort entire array, take last K elements
Time: O(N log N), Space: O(1)
Issue: Sorting more than necessary
Approach: Sort entire array, take last K elements
Time: O(N log N), Space: O(1)
Issue: Sorting more than necessary
✅ After: Min-Heap Optimization
Approach: Maintain min-heap of size K
Time: O(N log K), Space: O(K)
Improvement: Better when K << N
Alternative: QuickSelect for O(N) average case
Time: O(N log K), Space: O(K)
Improvement: Better when K << N
Alternative: QuickSelect for O(N) average case
Challenge 4: String Matching
❌ Before: Brute Force
Problem: Find pattern in text
Approach: Check pattern at every position
Time: O(N × M), Space: O(1)
Issue: Redundant character comparisons
Approach: Check pattern at every position
Time: O(N × M), Space: O(1)
Issue: Redundant character comparisons
✅ After: KMP Algorithm
Approach: Use failure function to skip redundant checks
Time: O(N + M), Space: O(M)
Improvement: Linear time complexity
Key Insight: Preprocess pattern to avoid backtracking
Time: O(N + M), Space: O(M)
Improvement: Linear time complexity
Key Insight: Preprocess pattern to avoid backtracking
Interview Problem Categories
Common Interview Problem Types
Understanding common interview problem categories helps you quickly identify the appropriate approach and data structures to use.
🗂️ Real-Life Analogy:
Like a doctor who recognizes symptoms to diagnose illnesses, recognizing problem patterns helps you prescribe the right algorithmic solution.
Category | Key Indicators | Common Techniques | Example Problems |
---|---|---|---|
Array/String | Sequential data, subarray/substring | Two pointers, Sliding window, Prefix sum | Two Sum, Longest Palindrome, Maximum Subarray |
Tree/Graph | Hierarchical/connected data | DFS, BFS, Recursion, Backtracking | Valid BST, Shortest Path, Connected Components |
Dynamic Programming | Optimal substructure, overlapping subproblems | Memoization, Tabulation, State transition | Knapsack, Edit Distance, House Robber |
Greedy | Local optimal leads to global optimal | Sorting, Priority queue, Proof by exchange | Activity Selection, Huffman Coding, Dijkstra's |
Backtracking | Explore all possibilities, constraints | Recursion, Pruning, State space tree | N-Queens, Sudoku Solver, Permutations |
💡 Interview Tips:
- Always clarify problem constraints and edge cases
- Start with a brute force solution, then optimize
- Think out loud and explain your approach
- Test your solution with examples
- Analyze time and space complexity