Graph Algorithms_ A Comprehensive Guide
Graph Algorithms_ A Comprehensive Guide
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:
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:
Description: DFS starts at a selected vertex and explores as far as possible along each branch before
backtracking. The algorithm:
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:
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:
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:
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:
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. 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. 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:
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:
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:
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:
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
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:
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:
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:
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:
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:
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:
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.