0% found this document useful (0 votes)
2 views

Graph Algorithms_ A Comprehensive Guide

The document provides a comprehensive guide on graph algorithms, covering traversal methods such as Breadth First Search (BFS) and Depth First Search (DFS), as well as advanced topics like finding connected components, bridges, articulation points, and strongly connected components. It includes detailed descriptions, implementations in JavaScript, and applications for each algorithm. Additionally, it discusses Dijkstra's algorithm for finding shortest paths in graphs with non-negative edge weights.

Uploaded by

Haina Kumari
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

Graph Algorithms_ A Comprehensive Guide

The document provides a comprehensive guide on graph algorithms, covering traversal methods such as Breadth First Search (BFS) and Depth First Search (DFS), as well as advanced topics like finding connected components, bridges, articulation points, and strongly connected components. It includes detailed descriptions, implementations in JavaScript, and applications for each algorithm. Additionally, it discusses Dijkstra's algorithm for finding shortest paths in graphs with non-negative edge weights.

Uploaded by

Haina Kumari
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 23

Graph Algorithms: A Comprehensive Guide

Chapter 1: Graph Traversal

1.1 Breadth First Search (BFS)


About the Algorithm: Breadth First Search (BFS) is a fundamental graph traversal algorithm that explores all
vertices at the present depth level before moving on to vertices at the next depth level. It uses a queue data
structure to remember which vertex to visit next.

Description: BFS starts at a selected vertex (the "source") and explores all its neighbors at the present depth
prior to moving on to nodes at the next depth level. The algorithm:

1.​ Visits the starting vertex


2.​ Explores all its neighbors
3.​ For each of those neighbors, explores their unexplored neighbors, and so on

BFS guarantees that it will find the shortest path (in terms of number of edges) from the source vertex to all
other vertices in an unweighted graph.

Implementation (JavaScript):
function bfs(graph, start) {​
const visited = new Set();​
const queue = [start];​
visited.add(start);​
while (queue.length > 0) {​
const vertex = queue.shift();​
console.log(vertex); // Process the vertex​
for (const neighbor of graph[vertex]) {​
if (!visited.has(neighbor)) {​
visited.add(neighbor);​
queue.push(neighbor);​
}​
}​
}​
}​
// Example usage:​
const graph = {​
A: ["B", "C"],​
B: ["A", "D", "E"],​
C: ["A", "F"],​
D: ["B"],​
E: ["B", "F"],​
F: ["C", "E"],​
};​

bfs(graph, "A"); // Starts BFS from vertex 'A'​

Applications:

1.​ Shortest path in unweighted graphs


2.​ Web crawling
3.​ Social network analysis (finding people within a certain distance)
4.​ GPS navigation systems
5.​ Finding connected components in graphs
6.​ Solving puzzles with minimum moves (e.g., Rubik's cube)

1.2 Depth First Search (DFS)


About the Algorithm: Depth First Search (DFS) is another fundamental graph traversal algorithm that
explores each branch as far as possible before backtracking. It uses a stack data structure, either explicitly or
implicitly via recursion.

Description: DFS starts at a selected vertex and explores as far as possible along each branch before
backtracking. The algorithm:

1.​ Visits the starting vertex


2.​ Explores one of its neighbors
3.​ Continues deeper into the graph before backtracking to explore other neighbors

DFS is often used when you need to explore all possibilities or when the solution is likely to be found deep in
the graph.

Implementation (JavaScript):
// Recursive implementation​
function dfs(graph, vertex, visited = new Set()) {​
visited.add(vertex);​
console.log(vertex); // Process the vertex​
for (const neighbor of graph[vertex]) {​
if (!visited.has(neighbor)) {​
dfs(graph, neighbor, visited);​
}​
}​
}​
// Iterative implementation​
function dfsIterative(graph, start) {​
const visited = new Set();​
const stack = [start];​
while (stack.length > 0) {​
const vertex = stack.pop();​
if (!visited.has(vertex)) {​
visited.add(vertex);​
console.log(vertex); // Process the vertex​
// Push neighbors in reverse order to maintain same order as recursive​
for (let i = graph[vertex].length - 1; i >= 0; i--) {​
const neighbor = graph[vertex][i];​
if (!visited.has(neighbor)) {​
stack.push(neighbor);​
}​
}​
}​
}​
}​

// Example usage:​

dfs(graph, "A"); // Starts DFS from vertex 'A'​
dfsIterative(graph, "A"); // Starts DFS from vertex 'A'​

Applications:

1.​ Topological sorting


2.​ Solving puzzles with only one solution (e.g., mazes)
3.​ Finding strongly connected components
4.​ Path finding
5.​ Detecting cycles in graphs
6.​ Generating mazes

Chapter 2: Connected Components, Bridges, Articulation Points

2.1 Finding Connected Components


About the Algorithm: Finding connected components in a graph involves identifying all subsets of vertices
where each pair of vertices is connected via a path, and no vertex in the subset is connected to any vertex
outside the subset.

Description: For undirected graphs, a connected component is a maximal set of vertices where there is a
path between every pair of vertices. The algorithm typically uses either BFS or DFS to explore all vertices
reachable from a starting vertex, marking them as belonging to the same component.

Implementation (JavaScript):
function findConnectedComponents(graph) {​
const visited = new Set();​
const components = [];​
for (const vertex in graph) {​
if (!visited.has(vertex)) {​
// New component found​
const component = [];​
const queue = [vertex];​
visited.add(vertex);​
while (queue.length > 0) {​
const current = queue.shift();​
component.push(current);​
for (const neighbor of graph[current]) {​
if (!visited.has(neighbor)) {​
visited.add(neighbor);​
queue.push(neighbor);​
}​
}​
}​
components.push(component);​
}​
}​
return components;​
}​
// Example usage:​
const disconnectedGraph = {​
A: ["B"],​
B: ["A"],​
C: ["D"],​
D: ["C", "E"],​
E: ["D"],​
F: [],​
};​
console.log(findConnectedComponents(disconnectedGraph));​
// Output: [['A', 'B'], ['C', 'D', 'E'], ['F']]​

Applications:

1.​ Network analysis (identifying clusters)


2.​ Image processing (connected pixel regions)
3.​ Social networks (finding friend groups)
4.​ VLSI design (circuit connectivity)
5.​ Graph partitioning problems

2.2 Finding Bridges in O(N+M)


About the Algorithm: A bridge in a graph is an edge whose removal increases the number of connected
components. The algorithm finds all such bridges in linear time O(V+E).

Description: The algorithm uses a modified DFS approach that assigns discovery times and low values to
vertices. A bridge is identified when the low value of a vertex is greater than the discovery time of its parent.

Key steps:

1.​ Perform DFS, keeping track of discovery times


2.​ For each vertex, calculate the low value (earliest visited vertex reachable from subtree)
3.​ An edge (u, v) is a bridge if low[v] > disc[u]

Implementation (JavaScript):
function findBridges(graph) {​
let time = 0;​
const bridges = [];​
const disc = {}; // Discovery times​
const low = {}; // Low values​
const visited = new Set();​
const parent = {}; // Parent vertices​
// Initialize data structures​
for (const vertex in graph) {​
disc[vertex] = 0;​
low[vertex] = 0;​
parent[vertex] = null;​
}​
function dfs(u) {​
visited.add(u);​
disc[u] = low[u] = ++time;​
for (const v of graph[u]) {​
if (!visited.has(v)) {​
parent[v] = u;​
dfs(v);​
// Update low value of u​
low[u] = Math.min(low[u], low[v]);​
// Check if the edge (u, v) is a bridge​
if (low[v] > disc[u]) {​
bridges.push([u, v]);​
}​
} else if (v !== parent[u]) {​
// Update low value of u for back edge​
low[u] = Math.min(low[u], disc[v]);​
}​
}​
}​
// Run DFS for each unvisited vertex​
for (const vertex in graph) {​
if (!visited.has(vertex)) {​
dfs(vertex);​
}​
}​
return bridges;​
}​
// Example usage:​
const graphWithBridges = {​
A: ["B", "C"],​
B: ["A", "C", "D"],​
C: ["A", "B"],​
D: ["B", "E"],​
E: ["D"],​
};​
console.log(findBridges(graphWithBridges)); // Output: [['D', 'E'], ['B', 'D']]​

Applications:

1.​ Network reliability analysis


2.​ Critical infrastructure identification
3.​ Graph theory research
4.​ Social network analysis (finding critical connections)
5.​ Transportation network planning

2.3 Finding Articulation Points in O(N+M)


About the Algorithm: An articulation point (or cut vertex) is a vertex whose removal increases the number of
connected components in the graph. This algorithm finds all such vertices in linear time.

Description: Similar to the bridge-finding algorithm, this approach uses DFS with discovery times and low
values. A vertex u is an articulation point if:

1.​ It's the root of the DFS tree and has at least two children, or
2.​ It's not the root and has a child v such that low[v] ≥ disc[u]

Implementation (JavaScript):
function findArticulationPoints(graph) {​
let time = 0;​
const articulationPoints = new Set();​
const disc = {};​
const low = {};​
const visited = new Set();​
const parent = {};​
const children = {}; // Count of children in DFS tree​
// Initialize​
for (const vertex in graph) {​
disc[vertex] = 0;​
low[vertex] = 0;​
parent[vertex] = null;​
children[vertex] = 0;​
}​
function dfs(u) {​
visited.add(u);​
disc[u] = low[u] = ++time;​
let childCount = 0;​
for (const v of graph[u]) {​
if (!visited.has(v)) {​
childCount++;​
parent[v] = u;​
dfs(v);​
// Update low value of u​
low[u] = Math.min(low[u], low[v]);​
// Check for articulation point​
if (parent[u] === null && childCount > 1) {​
articulationPoints.add(u);​
}​
if (parent[u] !== null && low[v] >= disc[u]) {​
articulationPoints.add(u);​
}​
} else if (v !== parent[u]) {​
// Back edge​
low[u] = Math.min(low[u], disc[v]);​
}​
}​
}​
for (const vertex in graph) {​
if (!visited.has(vertex)) {​
dfs(vertex);​
}​
}​
return Array.from(articulationPoints);​
}​
// Example usage:​
console.log(findArticulationPoints(graphWithBridges)); // Output: ['B', 'D']​

Applications:

1.​ Network vulnerability analysis


2.​ Identifying critical nodes in infrastructure
3.​ Social network analysis (finding influential people)
4.​ Graph clustering algorithms
5.​ Transportation network resilience planning

2.4 Strongly Connected Components and Condensation Graph


About the Algorithm: A strongly connected component (SCC) is a maximal subgraph where every vertex is
reachable from every other vertex. Kosaraju's algorithm finds all SCCs in linear time.

Description: The algorithm involves:

1.​ Performing DFS and pushing vertices to a stack in order of finishing times
2.​ Transposing the graph (reversing all edges)
3.​ Performing DFS on the transposed graph in the order defined by the stack

Implementation (JavaScript):
function findSCCs(graph) {​
const visited = new Set();​
const stack = [];​
const sccs = [];​
// First pass to fill the stack​
function dfsPass1(u) {​
visited.add(u);​
for (const v of graph[u] || []) {​
if (!visited.has(v)) {​
dfsPass1(v);​
}​
}​
stack.push(u);​
}​
// Second pass on reversed graph​
function dfsPass2(u, scc) {​
visited.add(u);​
scc.push(u);​
for (const v of reversedGraph[u] || []) {​
if (!visited.has(v)) {​
dfsPass2(v, scc);​
}​
}​
}​
// Create reversed graph​
const reversedGraph = {};​
for (const u in graph) {​
reversedGraph[u] = [];​
}​
for (const u in graph) {​
for (const v of graph[u]) {​
reversedGraph[v].push(u);​
}​
}​
// First DFS pass​
for (const u in graph) {​
if (!visited.has(u)) {​
dfsPass1(u);​
}​
}​
// Reset visited for second pass​
visited.clear();​
// Second DFS pass in reverse order​
while (stack.length > 0) {​
const u = stack.pop();​
if (!visited.has(u)) {​
const scc = [];​
dfsPass2(u, scc);​
sccs.push(scc);​
}​
}​
return sccs;​
}​
// Example usage:​
const directedGraph = {​
A: ["B"],​
B: ["C", "D"],​
C: ["A"],​
D: ["E"],​
E: ["F"],​
F: ["D", "G"],​
G: ["H"],​
H: ["I"],​
I: ["G", "J"],​
J: [],​
};​
console.log(findSCCs(directedGraph));​
// Output: [['A', 'C', 'B'], ['D', 'F', 'E'], ['G', 'I', 'H'], ['J']]​

Applications:

1.​ Compiler design (control flow analysis)


2.​ Social network analysis (identifying communities)
3.​ Web page link analysis
4.​ Circuit design
5.​ Recommendation systems
Chapter 3: Single-source Shortest Paths

3.1 Dijkstra's Algorithm


About the Algorithm: Dijkstra's algorithm finds the shortest paths from a single source vertex to all other
vertices in a graph with non-negative edge weights.

Description: The algorithm works by:

1.​ Maintaining a set of vertices whose shortest distance from the source is already known
2.​ At each step, selecting the vertex with the minimum distance estimate
3.​ Relaxing all edges outgoing from this vertex
4.​ Repeating until all vertices are processed

Implementation (JavaScript):
class PriorityQueue {​
constructor() {​
this.heap = [];​
}​
enqueue(element, priority) {​
this.heap.push({ element, priority });​
this.bubbleUp();​
}​
bubbleUp() {​
let index = this.heap.length - 1;​
while (index > 0) {​
const parentIndex = Math.floor((index - 1) / 2);​
if (this.heap[parentIndex].priority <= this.heap[index].priority) break;​
[this.heap[parentIndex], this.heap[index]] = [​
this.heap[index],​
this.heap[parentIndex],​
];​
index = parentIndex;​
}​
}​
dequeue() {​
const min = this.heap[0];​
const end = this.heap.pop();​
if (this.heap.length > 0) {​
this.heap[0] = end;​
this.sinkDown();​
}​
return min;​
}​
sinkDown() {​
let index = 0;​
const length = this.heap.length;​
while (true) {​
const leftChildIndex = 2 * index + 1;​
const rightChildIndex = 2 * index + 2;​
let swapIndex = null;​
if (leftChildIndex < length) {​
if (this.heap[leftChildIndex].priority < this.heap[index].priority) {​
swapIndex = leftChildIndex;​
}​
}​
if (rightChildIndex < length) {​
if (​
(swapIndex === null &&​
this.heap[rightChildIndex].priority < this.heap[index].priority) ||​
(swapIndex !== null &&​
this.heap[rightChildIndex].priority <​
this.heap[leftChildIndex].priority)​
) {​
swapIndex = rightChildIndex;​
}​
}​
if (swapIndex === null) break;​
[this.heap[index], this.heap[swapIndex]] = [​
this.heap[swapIndex],​
this.heap[index],​
];​
index = swapIndex;​
}​
}​
isEmpty() {​
return this.heap.length === 0;​
}​
}​

function dijkstra(graph, start) {​
const distances = {};​
const previous = {};​
const pq = new PriorityQueue();​
// Initialize distances​
for (const vertex in graph) {​
distances[vertex] = vertex === start ? 0 : Infinity;​
pq.enqueue(vertex, distances[vertex]);​
previous[vertex] = null;​
}​
while (!pq.isEmpty()) {​
const { element: current, priority: currentDistance } = pq.dequeue();​
// Skip if we already found a better path​
if (currentDistance > distances[current]) continue;​
for (const neighbor in graph[current]) {​
const distance = currentDistance + graph[current][neighbor];​
if (distance < distances[neighbor]) {​
distances[neighbor] = distance;​
previous[neighbor] = current;​
pq.enqueue(neighbor, distance);​
}​
}​
}​
return { distances, previous };​
}​

// Example usage:​
const weightedGraph = {​
A: { B: 4, C: 2 },​
B: { A: 4, C: 1, D: 5 },​
C: { A: 2, B: 1, D: 8, E: 10 },​
D: { B: 5, C: 8, E: 2 },​
E: { C: 10, D: 2 },​
};​
const { distances, previous } = dijkstra(weightedGraph, "A");​
console.log(distances); // Shortest distances from 'A'​
console.log(previous); // Previous nodes in shortest paths​

Applications:

1.​ GPS navigation systems


2.​ Network routing protocols
3.​ Traffic information systems
4.​ Flight itinerary planning
5.​ Robotics path planning

3.2 Bellman-Ford Algorithm


About the Algorithm: The Bellman-Ford algorithm finds shortest paths from a single source vertex to all other
vertices in a weighted graph, even with negative edge weights (but no negative cycles).

Description: The algorithm relaxes all edges |V|-1 times, where |V| is the number of vertices. After |V|-1
iterations, if we can still relax any edge, it indicates the presence of a negative weight cycle.

Implementation (JavaScript):
function bellmanFord(graph, start) {​
const distances = {};​
const previous = {};​
// Initialize distances​
for (const vertex in graph) {​
distances[vertex] = vertex === start ? 0 : Infinity;​
previous[vertex] = null;​
}​
// Relax all edges |V|-1 times​
const vertices = Object.keys(graph);​
for (let i = 0; i < vertices.length - 1; i++) {​
for (const u in graph) {​
for (const v in graph[u]) {​
const weight = graph[u][v];​
if (distances[u] + weight < distances[v]) {​
distances[v] = distances[u] + weight;​
previous[v] = u;​
}​
}​
}​
}​
// Check for negative weight cycles​
for (const u in graph) {​
for (const v in graph[u]) {​
const weight = graph[u][v];​
if (distances[u] + weight < distances[v]) {​
return { error: "Graph contains a negative weight cycle" };​
}​
}​
}​
return { distances, previous };​
}​
// Example usage:​
const graphWithNegativeWeights = {​
A: { B: -1, C: 4 },​
B: { C: 3, D: 2, E: 2 },​
C: {},​
D: { B: 1, C: 5 },​
E: { D: -3 },​
};​
const result = bellmanFord(graphWithNegativeWeights, "A");​
console.log(result.distances);​
console.log(result.previous);​

Applications:

1.​ Routing in networks with potentially negative costs


2.​ Currency arbitrage detection
3.​ Distance-vector routing protocols
4.​ Financial modeling with negative relationships
5.​ Game theory applications

Chapter 4: All-pairs Shortest Paths

4.1 Floyd-Warshall Algorithm


About the Algorithm: The Floyd-Warshall algorithm finds shortest paths between all pairs of vertices in a
weighted graph, including handling negative weights (but no negative cycles).

Description: The algorithm uses dynamic programming to systematically improve the shortest path estimates
by considering each vertex as an intermediate point in paths between other vertices.

Implementation (JavaScript):
function floydWarshall(graph) {​
const dist = {};​
const next = {};​
const vertices = Object.keys(graph);​
// Initialize distance and next matrices​
for (const u of vertices) {​
dist[u] = {};​
next[u] = {};​
for (const v of vertices) {​
if (u === v) {​
dist[u][v] = 0;​
} else if (graph[u][v] !== undefined) {​
dist[u][v] = graph[u][v];​
next[u][v] = v;​
} else {​
dist[u][v] = Infinity;​
}​
}​
}​
// Floyd-Warshall algorithm​
for (const k of vertices) {​
for (const i of vertices) {​
for (const j of vertices) {​
if (dist[i][j] > dist[i][k] + dist[k][j]) {​
dist[i][j] = dist[i][k] + dist[k][j];​
next[i][j] = next[i][k];​
}​
}​
}​
}​
// Check for negative cycles​
for (const u of vertices) {​
if (dist[u][u] < 0) {​
return { error: "Graph contains a negative weight cycle" };​
}​
}​
return { distances: dist, next };​
}​
function getPath(next, u, v) {​
if (next[u][v] === undefined) return [];​
const path = [u];​
while (u !== v) {​
u = next[u][v];​
path.push(u);​
}​
return path;​
}​
// Example usage:​
const allPairsGraph = {​
A: { B: 3, D: 7 },​
B: { A: 8, C: 2 },​
C: { A: 5, D: 1 },​
D: { B: 2 },​
};​

const fwResult = floydWarshall(allPairsGraph);​
console.log(fwResult.distances);​
console.log(getPath(fwResult.next, "A", "C"));​

Applications:

1.​ Transportation network planning


2.​ Network routing tables
3.​ Transitive closure of relations
4.​ Social network analysis (degrees of separation)
5.​ VLSI chip design

Chapter 5: Spanning Trees

5.1 Prim's Algorithm


About the Algorithm: Prim's algorithm finds a minimum spanning tree (MST) for a connected weighted
undirected graph, meaning a subset of edges that connects all vertices with the minimal total edge weight.

Description: The algorithm grows the MST one vertex at a time, always adding the cheapest edge that
connects a vertex in the MST to a vertex outside the MST.

Implementation (JavaScript):
function primMST(graph) {​
const vertices = Object.keys(graph);​
const parent = {};​
const key = {};​
const inMST = new Set();​
const mst = [];​
// Initialize keys to infinity​
for (const vertex of vertices) {​
key[vertex] = Infinity;​
}​
// Start with first vertex​
key[vertices[0]] = 0;​
parent[vertices[0]] = null;​
const pq = new PriorityQueue();​
for (const vertex of vertices) {​
pq.enqueue(vertex, key[vertex]);​
}​
while (!pq.isEmpty()) {​
const { element: u } = pq.dequeue();​
inMST.add(u);​
for (const v in graph[u]) {​
const weight = graph[u][v];​
if (!inMST.has(v) && weight < key[v]) {​
parent[v] = u;​
key[v] = weight;​
// Update priority queue (simplified approach)​

// In a real implementation, we'd need decrease-key operation​
const newPq = new PriorityQueue();​
while (!pq.isEmpty()) {​
const item = pq.dequeue();​
if (item.element === v) {​
newPq.enqueue(v, key[v]);​
} else {​
newPq.enqueue(item.element, item.priority);​
}​
}​
while (!newPq.isEmpty()) {​
const item = newPq.dequeue();​
pq.enqueue(item.element, item.priority);​
}​
}​
}​
}​
// Build MST edges​
for (const vertex of vertices) {​
if (parent[vertex] !== null) {​
mst.push({​
from: parent[vertex],​
to: vertex,​
weight: graph[parent[vertex]][vertex],​
});​
}​
}​
return mst;​
}​

// Example usage:​
const mstGraph = {​
A: { B: 2, D: 6 },​
B: { A: 2, C: 3, D: 8, E: 5 },​
C: { B: 3, E: 7 },​
D: { A: 6, B: 8, E: 9 },​
E: { B: 5, C: 7, D: 9 },​
};​

console.log(primMST(mstGraph));​

Applications:

1.​ Network design (telephone, electrical, hydraulic, TV cable)


2.​ Cluster analysis
3.​ Image segmentation
4.​ Approximation algorithms for NP-hard problems
5.​ Circuit design

5.2 Kruskal's Algorithm


About the Algorithm: Kruskal's algorithm is another approach to find a minimum spanning tree, working by
sorting all edges from lowest to highest weight, then adding them to the MST as long as they don't form a
cycle.
Description: The algorithm uses a disjoint-set (union-find) data structure to efficiently manage and merge sets
of vertices, ensuring no cycles are formed.

Implementation (JavaScript):
class DisjointSet {​
constructor() {​
this.parent = {};​
this.rank = {};​
}​
makeSet(x) {​
this.parent[x] = x;​
this.rank[x] = 0;​
}​
find(x) {​
if (this.parent[x] !== x) {​
this.parent[x] = this.find(this.parent[x]); // Path compression​
}​
return this.parent[x];​
}​
union(x, y) {​
const xRoot = this.find(x);​
const yRoot = this.find(y);​
if (xRoot === yRoot) return;​
// Union by rank​
if (this.rank[xRoot] < this.rank[yRoot]) {​
this.parent[xRoot] = yRoot;​
} else if (this.rank[xRoot] > this.rank[yRoot]) {​
this.parent[yRoot] = xRoot;​
} else {​
this.parent[yRoot] = xRoot;​

this.rank[xRoot]++;​
}​
}​
}​
function kruskalMST(graph) {​
const ds = new DisjointSet();​
const edges = [];​
const mst = [];​
// Prepare edges and initialize disjoint set​
for (const u in graph) {​
ds.makeSet(u);​
for (const v in graph[u]) {​
edges.push({u,v,weight: graph[u][v],});​
}​
}​
// Sort edges by weight​
edges.sort((a, b) => a.weight - b.weight);​
// Process edges in order​
for (const edge of edges) {​
if (ds.find(edge.u) !== ds.find(edge.v)) {​
mst.push(edge);​
ds.union(edge.u, edge.v);​
}​
}​
return mst;​
}​
// Example usage:​
console.log(kruskalMST(mstGraph));​

Applications:
1.​ Network design (same as Prim's)
2.​ Approximation algorithms for traveling salesman problem
3.​ Image recognition
4.​ Geographic information systems
5.​ Circuit board wiring

Chapter 6: Cycles

6.1 Detecting Cycles in Graphs


About the Algorithm: Cycle detection determines whether a graph contains at least one cycle, which is a
path that starts and ends at the same vertex.

Description: For undirected graphs, we can use DFS and check for back edges to already visited vertices
(excluding the immediate parent). For directed graphs, we need to track vertices in the current recursion stack.

Implementation (JavaScript):
// Undirected graph cycle detection​
function hasCycleUndirected(graph) {​
const visited = new Set();​
function dfs(u, parent) {​
visited.add(u);​
for (const v of graph[u]) {​
if (!visited.has(v)) {​
if (dfs(v, u)) return true;​
} else if (v !== parent) {​
return true;​
}​
}​
return false;​
}​
for (const vertex in graph) {​
if (!visited.has(vertex)) {​
if (dfs(vertex, null)) return true;​
}​
}​
return false;​
}​
// Directed graph cycle detection​
function hasCycleDirected(graph) {​
const visited = new Set();​
const recursionStack = new Set();​
function dfs(u) {​
visited.add(u);​
recursionStack.add(u);​
for (const v of graph[u] || []) {​
if (!visited.has(v)) {​
if (dfs(v)) return true;​
} else if (recursionStack.has(v)) {​
return true;​
}​
}​
recursionStack.delete(u);​
return false;​
}​
for (const vertex in graph) {​
if (!visited.has(vertex)) {​
if (dfs(vertex)) return true;​
}​
}​
return false;​
}​
// Example usage:​

const cyclicGraph = {​
A: ["B"],​
B: ["C"],​
C: ["A"],​
};​

console.log(hasCycleDirected(cyclicGraph)); // true​

console.log(​
hasCycleUndirected({​
A: ["B", "C"],​
B: ["A", "C"],​
C: ["A", "B"],​
})​
); // true​

Applications:

1.​ Deadlock detection in operating systems


2.​ Dependency resolution (e.g., makefiles, package managers)
3.​ Workflow management systems
4.​ Compiler optimization (detecting loops in control flow)
5.​ Biological network analysis

Chapter 7: Lowest Common Ancestor (LCA)

7.1 Binary Lifting for LCA


About the Algorithm: The Lowest Common Ancestor (LCA) of two nodes in a tree is the deepest node that
has both nodes as descendants. Binary lifting is an efficient method to answer LCA queries with
preprocessing.

Description: The algorithm preprocesses the tree to allow LCA queries in O(log N) time. It works by storing
ancestors at power-of-two distances from each node, allowing logarithmic time jumps up the tree.

Implementation (JavaScript):
class LCABinaryLifting {​
constructor(root, tree) {​
this.n = Object.keys(tree).length;​
this.log = Math.ceil(Math.log2(this.n)) + 1;​
this.up = {};​
this.depth = {};​
// Initialize structures​
for (const node in tree) {​
this.up[node] = Array(this.log).fill(null);​
this.depth[node] = 0;​
}​
// DFS to set up initial parent and depth​
this.dfs(root, null, tree);​
// Binary lifting preprocessing​
for (let k = 1; k < this.log; k++) {​
for (const node in tree) {​
const upKMinus1 = this.up[node][k - 1];​

if (upKMinus1 !== null) {​
this.up[node][k] = this.up[upKMinus1][k - 1];​
}​
}​
}​
}​
dfs(node, parent, tree) {​
this.up[node][0] = parent;​

for (const child of tree[node] || []) {​
if (child !== parent) {​
this.depth[child] = this.depth[node] + 1;​

this.dfs(child, node, tree);​
}​
}​
}​
lca(u, v) {​
// Ensure u is deeper​
if (this.depth[u] < this.depth[v]) {​
[u, v] = [v, u];​
}​
// Bring u up to the depth of v​
for (let k = this.log - 1; k >= 0; k--) {​
if (this.depth[u] - (1 << k) >= this.depth[v]) {​
u = this.up[u][k];​
}​
}​
if (u === v) return u;​
// Now bring both up as much as possible​
for (let k = this.log - 1; k >= 0; k--) {​
if (this.up[u][k] !== this.up[v][k]) {​
u = this.up[u][k];​
v = this.up[v][k];​
}​
}​
return this.up[u][0];​
}​
}​

// Example usage:​
const tree = {​
A: ["B", "C"],​
B: ["D", "E"],​
C: ["F", "G"],​
D: [],​
E: ["H", "I"],​
F: [],​
G: [],​
H: [],​
I: [],​
};​
const lcaFinder = new LCABinaryLifting("A", tree);​
console.log(lcaFinder.lca("D", "I")); // 'B'​
console.log(lcaFinder.lca("H", "G")); // 'A'​

Applications:

1.​ Computing distances between nodes in trees


2.​ Solving range minimum queries (RMQ)
3.​ Genealogy research
4.​ File system hierarchy analysis
5.​ Network routing in hierarchical topologies

Chapter 8: Flows and Related Problems

8.1 Ford-Fulkerson and Edmonds-Karp Algorithms


About the Algorithm: The Ford-Fulkerson method computes the maximum flow in a flow network. The
Edmonds-Karp algorithm is an implementation that uses BFS to find augmenting paths, ensuring polynomial
time complexity.

Description: The algorithm works by:

1.​ Finding an augmenting path from source to sink


2.​ Determining the maximum flow that can be sent along this path
3.​ Updating the residual capacities of edges
4.​ Repeating until no more augmenting paths exist

Implementation (JavaScript):
function edmondsKarp(graph, source, sink) {​
// Create residual graph​
const residualGraph = {};​
for (const u in graph) {​
residualGraph[u] = {};​
for (const v in graph[u]) {​
residualGraph[u][v] = graph[u][v];​
// Initialize reverse edge with 0 capacity if not exists​
if (!residualGraph[v]) residualGraph[v] = {};​
if (residualGraph[v][u] === undefined) residualGraph[v][u] = 0;​
}​
}​
let maxFlow = 0;​
while (true) {​
// BFS to find augmenting path​
const parent = {};​
const visited = new Set();​
const queue = [source];​
visited.add(source);​
let foundPath = false;​
while (queue.length > 0 && !foundPath) {​
const u = queue.shift();​
for (const v in residualGraph[u]) {​
if (!visited.has(v) && residualGraph[u][v] > 0) {​
parent[v] = u;​
visited.add(v);​
queue.push(v);​
if (v === sink) {​
foundPath = true;​
break;​
}​
}​
}​
}​

if (!foundPath) break; // No more augmenting paths​
// Find minimum residual capacity along the path​
let pathFlow = Infinity;​
let v = sink;​
while (v !== source) {​
const u = parent[v];​
pathFlow = Math.min(pathFlow, residualGraph[u][v]);​
v = u;​
}​
// Update residual capacities​
v = sink;​
while (v !== source) {​
const u = parent[v];​
residualGraph[u][v] -= pathFlow;​
residualGraph[v][u] += pathFlow;​
v = u;​
}​
maxFlow += pathFlow;​
}​
return maxFlow;​
}​
// Example usage:​
const flowNetwork = {​
S: { A: 10, B: 5, C: 10 },​
A: { D: 10 },​
B: { C: 10 },​
C: { E: 15 },​
D: { B: 5, T: 10 },​
E: { D: 5, T: 10 },​
T: {},​
};​

console.log(edmondsKarp(flowNetwork, "S", "T")); // Output: 20​

Applications:

1.​ Network flow problems


2.​ Bipartite matching
3.​ Image segmentation
4.​ Airline scheduling
5.​ Baseball elimination problems

Chapter 9: Matchings and Related Problems

9.1 Kuhn's Algorithm for Maximum Bipartite Matching


About the Algorithm: Kuhn's algorithm (also called the Hungarian algorithm) finds a maximum matching in a
bipartite graph, which is the largest set of edges without common vertices.

Description: The algorithm uses a DFS-based approach to find augmenting paths in the bipartite graph,
flipping matched and unmatched edges along the path to increase the matching size.

Implementation (JavaScript):
function kuhnMaxMatching(graph, leftVertices, rightVertices) {​
const pairU = {};​
const pairV = {};​
const visited = new Set();​
// Initialize​
for (const u of leftVertices) {​
pairU[u] = null;​
}​
for (const v of rightVertices) {​
pairV[v] = null;​
}​
function dfs(u) {​
if (visited.has(u)) return false;​
visited.add(u);​
for (const v of graph[u] || []) {​
if (pairV[v] === null || dfs(pairV[v])) {​
pairU[u] = v;​
pairV[v] = u;​
return true;​
}​
}​
return false;​
}​
let matching = 0;​
for (const u of leftVertices) {​
visited.clear();​
if (dfs(u)) {​
matching++;​
}​
}​
return {​
matchingSize: matching,​
pairU,​
pairV,​
};​
}​

// Example usage:​

const bipartiteGraph = {​
A: ["X", "Y"],​
B: ["X", "Z"],​
C: ["Y", "W"],​
D: ["Z"],​
};​

const left = ["A", "B", "C", "D"];​
const right = ["W", "X", "Y", "Z"];​
const result = kuhnMaxMatching(bipartiteGraph, left, right);​
console.log(result.matchingSize); // 4 (perfect matching possible)​
console.log(result.pairU); // e.g., { A: 'Y', B: 'X', C: 'W', D: 'Z' }​

Applications:

1.​ Job assignment problems


2.​ Dating/matching applications
3.​ Classroom scheduling
4.​ Image feature matching
5.​ Network switch scheduling
Chapter 10: Miscellaneous Graph Algorithms

10.1 Topological Sorting


About the Algorithm: Topological sorting arranges the vertices of a directed acyclic graph (DAG) in a linear
order where for every directed edge (u, v), vertex u comes before v in the ordering.

Description: The algorithm works by repeatedly selecting vertices with no incoming edges (in-degree zero),
removing them from the graph, and adding them to the topological order.

Implementation (JavaScript):
function topologicalSort(graph) {​
const inDegree = {};​
const queue = [];​
const topOrder = [];​
// Initialize in-degree​
for (const u in graph) {​
inDegree[u] = 0;​
}​
// Calculate in-degree​
for (const u in graph) {​
for (const v of graph[u]) {​
inDegree[v] = (inDegree[v] || 0) + 1;​
}​
}​
// Enqueue vertices with in-degree 0​
for (const u in inDegree) {​
if (inDegree[u] === 0) {​
queue.push(u);​
}​
}​
let visited = 0;​
while (queue.length > 0) {​
const u = queue.shift();​
topOrder.push(u);​
visited++;​
for (const v of graph[u] || []) {​
inDegree[v]--;​
if (inDegree[v] === 0) {​
queue.push(v);​
}​
}​
}​
if (visited !== Object.keys(graph).length) {​
return { error: "Graph has at least one cycle" };​
}​
return topOrder;​
}​
// Example usage:​
const dag = {​
A: ["C"],​
B: ["C", "D"],​
C: ["E"],​
D: ["F"],​
E: ["F"],​
F: [],​
};​

console.log(topologicalSort(dag)); // One possible output: ['B', 'D', 'A', 'C', 'E', 'F']​
Applications:

1.​ Task scheduling


2.​ Build system dependency resolution
3.​ Course prerequisite structures
4.​ Event processing pipelines
5.​ Data serialization

10.2 2-SAT Problem


About the Algorithm: The 2-SAT problem involves determining whether there exists an assignment of
boolean values to variables that satisfies a given boolean formula in conjunctive normal form (CNF) where
each clause has exactly two literals.

Description: The problem can be solved by constructing an implication graph from the clauses and finding
strongly connected components. A solution exists if no variable and its negation are in the same component.

Implementation (JavaScript):
function solve2SAT(variables, clauses) {​
// Construct implication graph​
const graph = {};​
const n = variables.length;​
// Initialize graph with variables and their negations​
for (let i = 1; i <= n; i++) {​
graph[i] = [];​
graph[-i] = [];​
}​
// Add implications for each clause (a ∨ b) ≡ (¬a → b) ∧ (¬b → a)​
for (const [a, b] of clauses) {​
graph[-a].push(b);​

graph[-b].push(a);​
}​
// Find strongly connected components (using Kosaraju's algorithm)​
const { sccs } = findSCCs(graph);​
// Check for contradictions​
const component = {};​
for (let i = 0; i < sccs.length; i++) {​
for (const node of sccs[i]) {​
component[node] = i;​
}​
}​

for (let i = 1; i <= n; i++) {​
if (component[i] === component[-i]) {​
return { satisfiable: false };​
}​
}​
// Construct solution​
const assignment = {};​
const reversedOrder = [...sccs].reverse();​
for (const comp of reversedOrder) {​
for (const node of comp) {​
const varName = Math.abs(node);​

if (assignment[varName] === undefined) {​
assignment[varName] = node > 0;​
}​
}​
}​
return {​
satisfiable: true,​

assignment,​
};​
}​
// Example usage:​

const variables = [1, 2, 3]; // x1, x2, x3​
const clauses = [​
[1, 2], // x1 ∨ x2​
[-1, 3], // ¬x1 ∨ x3​
[-2, -3], // ¬x2 ∨ ¬x3​
[3, 1], // x3 ∨ x1​
];​
const result = solve2SAT(variables, clauses);​
console.log(result);​

Applications:

1.​ Circuit design verification


2.​ Scheduling problems with constraints
3.​ Resource allocation with dependencies
4.​ AI planning problems
5.​ Configuration management

This comprehensive guide covers fundamental graph algorithms with detailed explanations, implementations,
and applications. Each algorithm is presented with its theoretical background, practical implementation in
JavaScript, and real-world use cases to provide a thorough understanding of graph processing techniques.

You might also like