Advanced Algorithms
Level 4: Advanced Mastery - Master sophisticated algorithms for complex problem solving
Advanced Algorithms
Shortest Path Algorithms
Dijkstra's Algorithm
Dijkstra's algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph with non-negative edge weights.
πΊοΈ Real-Life Analogy:
Think of finding the shortest route on a GPS navigation system. The algorithm explores the closest unvisited locations first, gradually expanding outward while keeping track of the shortest known distance to each destination.
TypeScript
// Priority Queue implementation for Dijkstra class PriorityQueue{ private heap: Array<{item: T, priority: number}> = []; enqueue(item: T, priority: number): void { this.heap.push({item, priority}); this.heapifyUp(this.heap.length - 1); } dequeue(): T | undefined { if (this.heap.length === 0) return undefined; const result = this.heap[0]; const last = this.heap.pop()!; if (this.heap.length > 0) { this.heap[0] = last; this.heapifyDown(0); } return result.item; } isEmpty(): boolean { return this.heap.length === 0; } private heapifyUp(index: number): void { while (index > 0) { const parentIndex = Math.floor((index - 1) / 2); if (this.heap[index].priority >= this.heap[parentIndex].priority) break; [this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]]; index = parentIndex; } } private heapifyDown(index: number): void { while (true) { const leftChild = 2 * index + 1; const rightChild = 2 * index + 2; let smallest = index; if (leftChild < this.heap.length && this.heap[leftChild].priority < this.heap[smallest].priority) { smallest = leftChild; } if (rightChild < this.heap.length && this.heap[rightChild].priority < this.heap[smallest].priority) { smallest = rightChild; } if (smallest === index) break; [this.heap[index], this.heap[smallest]] = [this.heap[smallest], this.heap[index]]; index = smallest; } } } // Dijkstra's Algorithm implementation class Graph { private adjacencyList: Map >; constructor() { this.adjacencyList = new Map(); } addVertex(vertex: number): void { if (!this.adjacencyList.has(vertex)) { this.adjacencyList.set(vertex, []); } } addEdge(from: number, to: number, weight: number): void { this.addVertex(from); this.addVertex(to); this.adjacencyList.get(from)!.push({node: to, weight}); } dijkstra(start: number): {distances: Map , previous: Map } { const distances = new Map (); const previous = new Map (); const pq = new PriorityQueue (); const visited = new Set (); // Initialize distances for (const vertex of this.adjacencyList.keys()) { distances.set(vertex, vertex === start ? 0 : Infinity); previous.set(vertex, null); } pq.enqueue(start, 0); while (!pq.isEmpty()) { const current = pq.dequeue()!; if (visited.has(current)) continue; visited.add(current); const neighbors = this.adjacencyList.get(current) || []; for (const neighbor of neighbors) { const {node, weight} = neighbor; const newDistance = distances.get(current)! + weight; if (newDistance < distances.get(node)!) { distances.set(node, newDistance); previous.set(node, current); pq.enqueue(node, newDistance); } } } return {distances, previous}; } getShortestPath(start: number, end: number): number[] { const {previous} = this.dijkstra(start); const path: number[] = []; let current: number | null = end; while (current !== null) { path.unshift(current); current = previous.get(current)!; } return path[0] === start ? path : []; } } // Bellman-Ford Algorithm (handles negative weights) class BellmanFord { static findShortestPaths( edges: Array<{from: number, to: number, weight: number}>, vertices: number[], source: number ): {distances: Map , hasNegativeCycle: boolean} { const distances = new Map (); // Initialize distances for (const vertex of vertices) { distances.set(vertex, vertex === source ? 0 : Infinity); } // Relax edges V-1 times for (let i = 0; i < vertices.length - 1; i++) { for (const edge of edges) { const {from, to, weight} = edge; const fromDist = distances.get(from)!; const toDist = distances.get(to)!; if (fromDist !== Infinity && fromDist + weight < toDist) { distances.set(to, fromDist + weight); } } } // Check for negative cycles let hasNegativeCycle = false; for (const edge of edges) { const {from, to, weight} = edge; const fromDist = distances.get(from)!; const toDist = distances.get(to)!; if (fromDist !== Infinity && fromDist + weight < toDist) { hasNegativeCycle = true; break; } } return {distances, hasNegativeCycle}; } } // Floyd-Warshall Algorithm (all-pairs shortest paths) class FloydWarshall { static allPairsShortestPaths( vertices: number[], edges: Array<{from: number, to: number, weight: number}> ): Map { const dist = new Map (); const n = vertices.length; // Initialize distances for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const key = `${vertices[i]}-${vertices[j]}`; dist.set(key, i === j ? 0 : Infinity); } } // Set edge weights for (const edge of edges) { const key = `${edge.from}-${edge.to}`; dist.set(key, edge.weight); } // Floyd-Warshall main loop for (let k = 0; k < n; k++) { for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const ikKey = `${vertices[i]}-${vertices[k]}`; const kjKey = `${vertices[k]}-${vertices[j]}`; const ijKey = `${vertices[i]}-${vertices[j]}`; const ikDist = dist.get(ikKey)!; const kjDist = dist.get(kjKey)!; const ijDist = dist.get(ijKey)!; if (ikDist + kjDist < ijDist) { dist.set(ijKey, ikDist + kjDist); } } } } return dist; } }
Python
import heapq from collections import defaultdict from typing import Dict, List, Tuple, Optional class Graph: def __init__(self): self.adjacency_list = defaultdict(list) def add_edge(self, from_vertex: int, to_vertex: int, weight: int): self.adjacency_list[from_vertex].append((to_vertex, weight)) def dijkstra(self, start: int) -> Tuple[Dict[int, int], Dict[int, Optional[int]]]: """Dijkstra's algorithm for shortest paths""" distances = defaultdict(lambda: float('inf')) previous = {} distances[start] = 0 pq = [(0, start)] visited = set() while pq: current_distance, current = heapq.heappop(pq) if current in visited: continue visited.add(current) for neighbor, weight in self.adjacency_list[current]: distance = current_distance + weight if distance < distances[neighbor]: distances[neighbor] = distance previous[neighbor] = current heapq.heappush(pq, (distance, neighbor)) return dict(distances), previous def get_shortest_path(self, start: int, end: int) -> List[int]: """Get shortest path between two vertices""" _, previous = self.dijkstra(start) if end not in previous: return [] path = [] current = end while current is not None: path.append(current) current = previous.get(current) path.reverse() return path if path[0] == start else [] def bellman_ford(edges: List[Tuple[int, int, int]], vertices: List[int], source: int) -> Tuple[Dict[int, int], bool]: """Bellman-Ford algorithm (handles negative weights)""" distances = {v: float('inf') for v in vertices} distances[source] = 0 # Relax edges V-1 times for _ in range(len(vertices) - 1): for from_v, to_v, weight in edges: if distances[from_v] != float('inf') and \ distances[from_v] + weight < distances[to_v]: distances[to_v] = distances[from_v] + weight # Check for negative cycles has_negative_cycle = False for from_v, to_v, weight in edges: if distances[from_v] != float('inf') and \ distances[from_v] + weight < distances[to_v]: has_negative_cycle = True break return distances, has_negative_cycle def floyd_warshall(vertices: List[int], edges: List[Tuple[int, int, int]]) -> Dict[Tuple[int, int], int]: """Floyd-Warshall algorithm for all-pairs shortest paths""" n = len(vertices) dist = {} # Initialize distances for i in range(n): for j in range(n): key = (vertices[i], vertices[j]) dist[key] = 0 if i == j else float('inf') # Set edge weights for from_v, to_v, weight in edges: dist[(from_v, to_v)] = weight # Floyd-Warshall main loop for k in range(n): for i in range(n): for j in range(n): v_k = vertices[k] v_i = vertices[i] v_j = vertices[j] if dist[(v_i, v_k)] + dist[(v_k, v_j)] < dist[(v_i, v_j)]: dist[(v_i, v_j)] = dist[(v_i, v_k)] + dist[(v_k, v_j)] return dist # A* Algorithm for pathfinding with heuristic def a_star(graph: Dict[int, List[Tuple[int, int]]], start: int, goal: int, heuristic: Dict[int, int]) -> List[int]: """A* pathfinding algorithm with heuristic""" open_set = [(0, start)] came_from = {} g_score = defaultdict(lambda: float('inf')) g_score[start] = 0 f_score = defaultdict(lambda: float('inf')) f_score[start] = heuristic[start] while open_set: current = heapq.heappop(open_set)[1] if current == goal: # Reconstruct path path = [] while current in came_from: path.append(current) current = came_from[current] path.append(start) return path[::-1] for neighbor, weight in graph.get(current, []): tentative_g = g_score[current] + weight if tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + heuristic[neighbor] heapq.heappush(open_set, (f_score[neighbor], neighbor)) return [] # No path found # Johnson's Algorithm (all-pairs with negative weights) def johnsons_algorithm(vertices: List[int], edges: List[Tuple[int, int, int]]) -> Dict[Tuple[int, int], int]: """Johnson's algorithm for all-pairs shortest paths with negative weights""" # Add auxiliary vertex connected to all vertices with weight 0 aux_vertex = max(vertices) + 1 extended_edges = edges + [(aux_vertex, v, 0) for v in vertices] extended_vertices = vertices + [aux_vertex] # Run Bellman-Ford from auxiliary vertex h, has_negative_cycle = bellman_ford(extended_edges, extended_vertices, aux_vertex) if has_negative_cycle: raise ValueError("Graph contains negative cycle") # Reweight edges reweighted_edges = [] for from_v, to_v, weight in edges: new_weight = weight + h[from_v] - h[to_v] reweighted_edges.append((from_v, to_v, new_weight)) # Build graph for Dijkstra graph = defaultdict(list) for from_v, to_v, weight in reweighted_edges: graph[from_v].append((to_v, weight)) # Run Dijkstra from each vertex all_distances = {} for source in vertices: graph_obj = Graph() for from_v, neighbors in graph.items(): for to_v, weight in neighbors: graph_obj.add_edge(from_v, to_v, weight) distances, _ = graph_obj.dijkstra(source) # Restore original weights for target in vertices: if target in distances: original_distance = distances[target] - h[source] + h[target] all_distances[(source, target)] = original_distance return all_distances
OCaml
(* Priority Queue implementation using binary heap *) module PriorityQueue = struct type 'a t = { mutable heap: ('a * int) array; mutable size: int; } let create capacity = { heap = Array.make capacity (Obj.magic (), 0); size = 0; } let parent i = (i - 1) / 2 let left_child i = 2 * i + 1 let right_child i = 2 * i + 2 let swap heap i j = let temp = heap.(i) in heap.(i) <- heap.(j); heap.(j) <- temp let rec heapify_up heap i = if i > 0 then let p = parent i in if snd heap.(i) < snd heap.(p) then begin swap heap i p; heapify_up heap p end let rec heapify_down heap size i = let l = left_child i in let r = right_child i in let smallest = ref i in if l < size && snd heap.(l) < snd heap.(!smallest) then smallest := l; if r < size && snd heap.(r) < snd heap.(!smallest) then smallest := r; if !smallest <> i then begin swap heap i !smallest; heapify_down heap size !smallest end let enqueue pq item priority = let {heap; size} = pq in heap.(size) <- (item, priority); heapify_up heap size; pq.size <- size + 1 let dequeue pq = if pq.size = 0 then None else begin let result = fst pq.heap.(0) in pq.size <- pq.size - 1; if pq.size > 0 then begin pq.heap.(0) <- pq.heap.(pq.size); heapify_down pq.heap pq.size 0 end; Some result end let is_empty pq = pq.size = 0 end (* Graph representation *) module Graph = struct type t = (int, (int * int) list) Hashtbl.t let create () = Hashtbl.create 16 let add_edge graph from_v to_v weight = let neighbors = try Hashtbl.find graph from_v with Not_found -> [] in Hashtbl.replace graph from_v ((to_v, weight) :: neighbors) (* Dijkstra's algorithm *) let dijkstra graph start = let distances = Hashtbl.create 16 in let previous = Hashtbl.create 16 in let pq = PriorityQueue.create 100 in let visited = Hashtbl.create 16 in (* Initialize distances *) Hashtbl.iter (fun vertex _ -> Hashtbl.add distances vertex (if vertex = start then 0 else max_int); Hashtbl.add previous vertex None ) graph; PriorityQueue.enqueue pq start 0; let rec loop () = match PriorityQueue.dequeue pq with | None -> () | Some current -> if not (Hashtbl.mem visited current) then begin Hashtbl.add visited current true; let current_dist = Hashtbl.find distances current in let neighbors = try Hashtbl.find graph current with Not_found -> [] in List.iter (fun (neighbor, weight) -> let new_dist = current_dist + weight in let old_dist = Hashtbl.find distances neighbor in if new_dist < old_dist then begin Hashtbl.replace distances neighbor new_dist; Hashtbl.replace previous neighbor (Some current); PriorityQueue.enqueue pq neighbor new_dist end ) neighbors end; loop () in loop (); (distances, previous) let get_shortest_path graph start target = let (_, previous) = dijkstra graph start in let rec build_path acc current = match Hashtbl.find previous current with | None -> if current = start then current :: acc else [] | Some prev -> build_path (current :: acc) prev in try build_path [] target with Not_found -> [] end (* Bellman-Ford algorithm *) let bellman_ford edges vertices source = let distances = Hashtbl.create 16 in (* Initialize distances *) List.iter (fun v -> Hashtbl.add distances v (if v = source then 0 else max_int) ) vertices; (* Relax edges V-1 times *) for _ = 1 to List.length vertices - 1 do List.iter (fun (from_v, to_v, weight) -> let from_dist = Hashtbl.find distances from_v in let to_dist = Hashtbl.find distances to_v in if from_dist <> max_int && from_dist + weight < to_dist then Hashtbl.replace distances to_v (from_dist + weight) ) edges done; (* Check for negative cycles *) let has_negative_cycle = ref false in List.iter (fun (from_v, to_v, weight) -> let from_dist = Hashtbl.find distances from_v in let to_dist = Hashtbl.find distances to_v in if from_dist <> max_int && from_dist + weight < to_dist then has_negative_cycle := true ) edges; (distances, !has_negative_cycle) (* Floyd-Warshall algorithm *) let floyd_warshall vertices edges = let n = List.length vertices in let dist = Hashtbl.create (n * n) in (* Initialize distances *) List.iteri (fun i v_i -> List.iteri (fun j v_j -> let key = (v_i, v_j) in Hashtbl.add dist key (if i = j then 0 else max_int) ) vertices ) vertices; (* Set edge weights *) List.iter (fun (from_v, to_v, weight) -> Hashtbl.replace dist (from_v, to_v) weight ) edges; (* Main Floyd-Warshall loop *) List.iter (fun v_k -> List.iter (fun v_i -> List.iter (fun v_j -> let ik_dist = try Hashtbl.find dist (v_i, v_k) with Not_found -> max_int in let kj_dist = try Hashtbl.find dist (v_k, v_j) with Not_found -> max_int in let ij_dist = try Hashtbl.find dist (v_i, v_j) with Not_found -> max_int in if ik_dist <> max_int && kj_dist <> max_int && ik_dist + kj_dist < ij_dist then Hashtbl.replace dist (v_i, v_j) (ik_dist + kj_dist) ) vertices ) vertices ) vertices; dist
β±οΈ Complexity Analysis
Algorithm | Time Complexity | Space Complexity | Best Use Case |
---|---|---|---|
Dijkstra's | O((V + E) log V) | O(V) | Single source, non-negative weights |
Bellman-Ford | O(VE) | O(V) | Single source, negative weights |
Floyd-Warshall | O(VΒ³) | O(VΒ²) | All pairs shortest paths |
A* | O(b^d) | O(b^d) | Pathfinding with heuristic |
Tarjan's Algorithm for Strongly Connected Components
Strongly Connected Components (SCC)
Tarjan's algorithm finds all strongly connected components in a directed graph in linear time. A strongly connected component is a maximal set of vertices where every vertex is reachable from every other vertex in the set.
π Real-Life Analogy:
Think of social media networks where users follow each other. A strongly connected component represents a group where everyone can reach everyone else through mutual connections - like a tight-knit community where information can flow in all directions.
π DFS Traversal Visualization:
Graph: A β B β C β β β D β E β F DFS Stack Evolution: Step 1: [A] - discovery[A]=0, low[A]=0 Step 2: [A,B] - discovery[B]=1, low[B]=1 Step 3: [A,B,C] - discovery[C]=2, low[C]=2 Step 4: [A,B,C,F] - discovery[F]=3, low[F]=3 Step 5: [A,B,C,F,E] - discovery[E]=4, low[E]=4 Step 6: [A,B,C,F,E,D] - discovery[D]=5, low[D]=5 Step 7: Back edge DβA: low[D] = min(low[D], discovery[A]) = 0 SCC Found: {A,B,C,D,E,F} when low[A] == discovery[A]
TypeScript
class TarjanSCC { private graph: Map; private time: number; private stack: number[]; private onStack: Set ; private discovered: Map ; private low: Map ; private sccs: number[][]; constructor() { this.graph = new Map(); this.time = 0; this.stack = []; this.onStack = new Set(); this.discovered = new Map(); this.low = new Map(); this.sccs = []; } addEdge(from: number, to: number): void { if (!this.graph.has(from)) { this.graph.set(from, []); } this.graph.get(from)!.push(to); } findSCCs(): number[][] { // Initialize all vertices for (const vertex of this.graph.keys()) { if (!this.discovered.has(vertex)) { this.dfs(vertex); } } return this.sccs; } private dfs(vertex: number): void { // Initialize discovery time and low value this.discovered.set(vertex, this.time); this.low.set(vertex, this.time); this.time++; // Push current vertex to stack this.stack.push(vertex); this.onStack.add(vertex); // Visit all adjacent vertices const neighbors = this.graph.get(vertex) || []; for (const neighbor of neighbors) { if (!this.discovered.has(neighbor)) { // Tree edge: recursively visit this.dfs(neighbor); this.low.set(vertex, Math.min( this.low.get(vertex)!, this.low.get(neighbor)! )); } else if (this.onStack.has(neighbor)) { // Back edge: update low value this.low.set(vertex, Math.min( this.low.get(vertex)!, this.discovered.get(neighbor)! )); } } // If vertex is a root of SCC if (this.low.get(vertex) === this.discovered.get(vertex)) { const scc: number[] = []; let current: number; // Pop vertices from stack until current vertex do { current = this.stack.pop()!; this.onStack.delete(current); scc.push(current); } while (current !== vertex); this.sccs.push(scc); } } // Check if graph is strongly connected isStronglyConnected(): boolean { const sccs = this.findSCCs(); return sccs.length === 1; } // Find condensation graph (DAG of SCCs) getCondensationGraph(): {nodes: number[], edges: Array<{from: number, to: number}>} { const sccs = this.findSCCs(); const vertexToSCC = new Map (); // Map each vertex to its SCC index sccs.forEach((scc, index) => { scc.forEach(vertex => { vertexToSCC.set(vertex, index); }); }); const condensationEdges = new Set (); // Add edges between different SCCs for (const [from, neighbors] of this.graph) { const fromSCC = vertexToSCC.get(from)!; for (const to of neighbors) { const toSCC = vertexToSCC.get(to)!; if (fromSCC !== toSCC) { condensationEdges.add(`${fromSCC}-${toSCC}`); } } } const edges = Array.from(condensationEdges).map(edge => { const [from, to] = edge.split('-').map(Number); return {from, to}; }); return { nodes: Array.from({length: sccs.length}, (_, i) => i), edges }; } } // Usage example for social network analysis class SocialNetworkAnalyzer { static findCommunities(followGraph: Map ): string[][] { const tarjan = new TarjanSCC(); const userToId = new Map (); const idToUser = new Map (); let nextId = 0; // Convert usernames to numeric IDs for (const [user, follows] of followGraph) { if (!userToId.has(user)) { userToId.set(user, nextId); idToUser.set(nextId, user); nextId++; } for (const followed of follows) { if (!userToId.has(followed)) { userToId.set(followed, nextId); idToUser.set(nextId, followed); nextId++; } } } // Build graph with numeric IDs for (const [user, follows] of followGraph) { const userId = userToId.get(user)!; for (const followed of follows) { const followedId = userToId.get(followed)!; tarjan.addEdge(userId, followedId); } } // Find SCCs and convert back to usernames const numericSCCs = tarjan.findSCCs(); return numericSCCs.map(scc => scc.map(id => idToUser.get(id)!) ); } }
Python
from collections import defaultdict from typing import List, Dict, Set, Tuple class TarjanSCC: def __init__(self): self.graph = defaultdict(list) self.time = 0 self.stack = [] self.on_stack = set() self.discovered = {} self.low = {} self.sccs = [] def add_edge(self, from_vertex: int, to_vertex: int): """Add directed edge to graph""" self.graph[from_vertex].append(to_vertex) def find_sccs(self) -> List[List[int]]: """Find all strongly connected components""" self.time = 0 self.stack = [] self.on_stack = set() self.discovered = {} self.low = {} self.sccs = [] # Get all vertices vertices = set(self.graph.keys()) for neighbors in self.graph.values(): vertices.update(neighbors) # Run DFS from each unvisited vertex for vertex in vertices: if vertex not in self.discovered: self._dfs(vertex) return self.sccs def _dfs(self, vertex: int): """DFS with Tarjan's algorithm logic""" # Initialize discovery time and low value self.discovered[vertex] = self.time self.low[vertex] = self.time self.time += 1 # Push to stack self.stack.append(vertex) self.on_stack.add(vertex) # Visit all neighbors for neighbor in self.graph[vertex]: if neighbor not in self.discovered: # Tree edge self._dfs(neighbor) self.low[vertex] = min(self.low[vertex], self.low[neighbor]) elif neighbor in self.on_stack: # Back edge self.low[vertex] = min(self.low[vertex], self.discovered[neighbor]) # Check if vertex is root of SCC if self.low[vertex] == self.discovered[vertex]: scc = [] while True: current = self.stack.pop() self.on_stack.remove(current) scc.append(current) if current == vertex: break self.sccs.append(scc) def is_strongly_connected(self) -> bool: """Check if entire graph is strongly connected""" sccs = self.find_sccs() return len(sccs) == 1 def get_condensation_graph(self) -> Tuple[List[int], List[Tuple[int, int]]]: """Get condensation graph (DAG of SCCs)""" sccs = self.find_sccs() vertex_to_scc = {} # Map vertices to SCC indices for i, scc in enumerate(sccs): for vertex in scc: vertex_to_scc[vertex] = i # Find edges between different SCCs condensation_edges = set() for from_vertex, neighbors in self.graph.items(): from_scc = vertex_to_scc[from_vertex] for to_vertex in neighbors: to_scc = vertex_to_scc[to_vertex] if from_scc != to_scc: condensation_edges.add((from_scc, to_scc)) nodes = list(range(len(sccs))) edges = list(condensation_edges) return nodes, edges def web_crawler_analysis(web_links: Dict[str, List[str]]) -> List[List[str]]: """Analyze web pages for strongly connected link clusters""" tarjan = TarjanSCC() url_to_id = {} id_to_url = {} next_id = 0 # Convert URLs to numeric IDs for url, links in web_links.items(): if url not in url_to_id: url_to_id[url] = next_id id_to_url[next_id] = url next_id += 1 for link in links: if link not in url_to_id: url_to_id[link] = next_id id_to_url[next_id] = link next_id += 1 # Build graph for url, links in web_links.items(): url_id = url_to_id[url] for link in links: link_id = url_to_id[link] tarjan.add_edge(url_id, link_id) # Find SCCs and convert back to URLs numeric_sccs = tarjan.find_sccs() return [[id_to_url[node_id] for node_id in scc] for scc in numeric_sccs] # Example: Kosaraju's algorithm alternative def kosaraju_scc(graph: Dict[int, List[int]]) -> List[List[int]]: """Alternative SCC algorithm using two DFS passes""" def dfs1(vertex, visited, stack): visited.add(vertex) for neighbor in graph.get(vertex, []): if neighbor not in visited: dfs1(neighbor, visited, stack) stack.append(vertex) def dfs2(vertex, visited, component, transpose): visited.add(vertex) component.append(vertex) for neighbor in transpose.get(vertex, []): if neighbor not in visited: dfs2(neighbor, visited, component, transpose) # Get all vertices vertices = set(graph.keys()) for neighbors in graph.values(): vertices.update(neighbors) # First DFS to get finish times visited = set() stack = [] for vertex in vertices: if vertex not in visited: dfs1(vertex, visited, stack) # Create transpose graph transpose = defaultdict(list) for vertex, neighbors in graph.items(): for neighbor in neighbors: transpose[neighbor].append(vertex) # Second DFS on transpose in reverse finish order visited = set() sccs = [] while stack: vertex = stack.pop() if vertex not in visited: component = [] dfs2(vertex, visited, component, transpose) sccs.append(component) return sccs
OCaml
(* Tarjan's Strongly Connected Components Algorithm *) module TarjanSCC = struct type graph = (int, int list) Hashtbl.t type state = { mutable time: int; mutable stack: int list; on_stack: (int, bool) Hashtbl.t; discovered: (int, int) Hashtbl.t; low: (int, int) Hashtbl.t; mutable sccs: int list list; } let create_state () = { time = 0; stack = []; on_stack = Hashtbl.create 16; discovered = Hashtbl.create 16; low = Hashtbl.create 16; sccs = []; } let add_edge graph from_v to_v = let neighbors = try Hashtbl.find graph from_v with Not_found -> [] in Hashtbl.replace graph from_v (to_v :: neighbors) let rec dfs graph state vertex = (* Initialize discovery time and low value *) Hashtbl.add state.discovered vertex state.time; Hashtbl.add state.low vertex state.time; state.time <- state.time + 1; (* Push to stack *) state.stack <- vertex :: state.stack; Hashtbl.add state.on_stack vertex true; (* Visit all neighbors *) let neighbors = try Hashtbl.find graph vertex with Not_found -> [] in List.iter (fun neighbor -> if not (Hashtbl.mem state.discovered neighbor) then begin (* Tree edge *) dfs graph state neighbor; let low_vertex = Hashtbl.find state.low vertex in let low_neighbor = Hashtbl.find state.low neighbor in Hashtbl.replace state.low vertex (min low_vertex low_neighbor) end else if Hashtbl.mem state.on_stack neighbor then begin (* Back edge *) let low_vertex = Hashtbl.find state.low vertex in let disc_neighbor = Hashtbl.find state.discovered neighbor in Hashtbl.replace state.low vertex (min low_vertex disc_neighbor) end ) neighbors; (* Check if vertex is root of SCC *) let low_vertex = Hashtbl.find state.low vertex in let disc_vertex = Hashtbl.find state.discovered vertex in if low_vertex = disc_vertex then begin let rec pop_scc acc = match state.stack with | [] -> acc | current :: rest -> state.stack <- rest; Hashtbl.remove state.on_stack current; let new_acc = current :: acc in if current = vertex then new_acc else pop_scc new_acc in let scc = pop_scc [] in state.sccs <- scc :: state.sccs end let find_sccs graph = let state = create_state () in (* Get all vertices *) let vertices = Hashtbl.fold (fun v neighbors acc -> let all_neighbors = List.fold_left (fun acc n -> n :: acc) acc neighbors in v :: all_neighbors ) graph [] in let unique_vertices = List.sort_uniq compare vertices in (* Run DFS from each unvisited vertex *) List.iter (fun vertex -> if not (Hashtbl.mem state.discovered vertex) then dfs graph state vertex ) unique_vertices; List.rev state.sccs let is_strongly_connected graph = let sccs = find_sccs graph in List.length sccs = 1 let get_condensation_graph graph = let sccs = find_sccs graph in let vertex_to_scc = Hashtbl.create 16 in (* Map vertices to SCC indices *) List.iteri (fun i scc -> List.iter (fun vertex -> Hashtbl.add vertex_to_scc vertex i ) scc ) sccs; (* Find edges between different SCCs *) let condensation_edges = ref [] in Hashtbl.iter (fun from_vertex neighbors -> let from_scc = Hashtbl.find vertex_to_scc from_vertex in List.iter (fun to_vertex -> let to_scc = Hashtbl.find vertex_to_scc to_vertex in if from_scc <> to_scc then condensation_edges := (from_scc, to_scc) :: !condensation_edges ) neighbors ) graph; let unique_edges = List.sort_uniq compare !condensation_edges in let nodes = Array.to_list (Array.init (List.length sccs) (fun i -> i)) in (nodes, unique_edges) end (* Application: Web crawling analysis *) let analyze_web_graph url_links = let graph = Hashtbl.create 16 in let url_to_id = Hashtbl.create 16 in let id_to_url = Hashtbl.create 16 in let next_id = ref 0 in (* Convert URLs to numeric IDs *) List.iter (fun (url, links) -> if not (Hashtbl.mem url_to_id url) then begin Hashtbl.add url_to_id url !next_id; Hashtbl.add id_to_url !next_id url; incr next_id end; List.iter (fun link -> if not (Hashtbl.mem url_to_id link) then begin Hashtbl.add url_to_id link !next_id; Hashtbl.add id_to_url !next_id link; incr next_id end ) links ) url_links; (* Build numeric graph *) List.iter (fun (url, links) -> let url_id = Hashtbl.find url_to_id url in List.iter (fun link -> let link_id = Hashtbl.find url_to_id link in TarjanSCC.add_edge graph url_id link_id ) links ) url_links; (* Find SCCs and convert back to URLs *) let numeric_sccs = TarjanSCC.find_sccs graph in List.map (fun scc -> List.map (fun id -> Hashtbl.find id_to_url id) scc ) numeric_sccs
β±οΈ Complexity Analysis
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
Find all SCCs | O(V + E) | O(V) | Linear time, single DFS pass |
Condensation graph | O(V + E) | O(V + E) | Creates DAG representation |
Strong connectivity check | O(V + E) | O(V) | Single SCC means strongly connected |
π Real-World Applications
- Social Networks: Find communities where users mutually follow each other
- Web Crawling: Identify clusters of pages that link to each other
- Dependency Analysis: Find circular dependencies in software modules
- Circuit Analysis: Identify strongly connected components in electrical circuits
- Call Graph Analysis: Find recursive function groups in programs
Articulation Points and Bridges
Critical Network Components
Articulation points (cut vertices) are vertices whose removal increases the number of connected components. Bridges (cut edges) are edges whose removal increases the number of connected components. These are critical for network reliability analysis.
π Real-Life Analogy:
Think of a transportation network where articulation points are critical intersections and bridges are critical roads. If a bridge collapses or an intersection is blocked, parts of the network become disconnected, causing major disruptions.
β±οΈ Complexity Analysis
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
Find articulation points | O(V + E) | O(V) | Single DFS traversal |
Find bridges | O(V + E) | O(V) | Same traversal as articulation points |
π Real-World Applications
- Network Reliability: Identify critical infrastructure points and connections
- Transportation Systems: Find critical intersections and roads in traffic networks
- Communication Networks: Locate critical routers and links in internet infrastructure
- Social Network Analysis: Find key influencers whose removal fragments communities
- Supply Chain Management: Identify critical suppliers and distribution points
Network Flow Algorithms
Maximum Flow and Minimum Cut
Network flow algorithms solve problems related to finding the maximum amount of flow that can be sent from a source to a sink in a flow network. The Ford-Fulkerson method and its implementations like Edmonds-Karp are fundamental algorithms for this problem.
π° Real-Life Analogy:
Think of a water distribution system with pipes of different capacities. You want to find the maximum amount of water that can flow from a reservoir (source) to a city (sink) through the network of pipes, where each pipe has a maximum capacity constraint.
β±οΈ Complexity Analysis
Algorithm | Time Complexity | Space Complexity | Notes |
---|---|---|---|
Ford-Fulkerson | O(E Γ max_flow) | O(V + E) | With DFS path finding |
Edmonds-Karp | O(VEΒ²) | O(V + E) | With BFS path finding |
Dinic's Algorithm | O(VΒ²E) | O(V + E) | Uses level graphs and blocking flows |
π Real-World Applications
- Resource Allocation: Optimal distribution of resources across networks
- Traffic Flow: Maximize vehicle throughput in transportation networks
- Network Capacity: Find bottlenecks in communication networks
- Supply Chain: Optimize material flow through manufacturing pipelines
- Bipartite Matching: Assignment problems like job scheduling
A* Search Algorithm
Heuristic-Based Pathfinding
A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide the search toward the goal more efficiently. It guarantees the shortest path if the heuristic is admissible (never overestimates the true cost).
π§ Real-Life Analogy:
Think of using a GPS with traffic prediction. While Dijkstra's explores all roads equally, A* is like having a smart navigator that prioritizes roads that seem to lead toward your destination, using both the distance traveled and an estimate of remaining distance.
π A* vs Dijkstra Comparison:
Grid Pathfinding (S = Start, G = Goal, # = Wall): S . . . . G . # # . . . . . . . # . . # # . . . . . . . . . Dijkstra explores: O O O O O G O # # O O O O O O O # O O # # O O O O O O O O O A* with Manhattan heuristic explores: S β β β β G β # # β β β β β β β # β β # # β β β β β β β β β A* explores fewer nodes by focusing on promising directions!
β±οΈ Complexity Analysis
Scenario | Time Complexity | Space Complexity | Notes |
---|---|---|---|
Perfect heuristic | O(d) | O(d) | Follows optimal path directly |
Admissible heuristic | O(b^d) | O(b^d) | b = branching factor, d = depth |
Poor heuristic | O(b^d) | O(b^d) | Degrades to breadth-first search |
π‘ Interview Key Points
- Heuristic Design: Must be admissible (h(n) β€ actual cost) for optimality
- f(n) = g(n) + h(n): Total cost = actual cost + heuristic estimate
- vs Dijkstra: A* uses domain knowledge to search smarter, not harder
- vs Greedy: A* considers both path cost and heuristic, ensuring optimality
- Trade-offs: Better heuristics mean faster search but more computation per node
π Real-World Applications
- Game AI: Pathfinding for NPCs in video games
- Robotics: Navigation planning for autonomous vehicles
- GPS Navigation: Route planning with traffic and road conditions
- Puzzle Solving: Finding optimal solutions in search spaces
- Network Routing: Efficient packet routing in computer networks