Overview
A weighted graph represents a collection of nodes (vertices) connected by edges, where each edge carries a numerical value representing cost, distance, capacity, or another metric. Unlike unweighted graphs that treat all connections equally, weighted graphs model real-world scenarios where relationships between entities have varying significance.
The concept emerged from operations research and network analysis in the 1950s, particularly through Edsger Dijkstra's shortest path algorithm (1956) and the study of transportation networks. Weighted graphs appear throughout software systems: network routing protocols use them to find optimal paths, recommendation systems model user preferences as weighted connections, and compilers represent program dependencies with weighted directed acyclic graphs.
Two fundamental variants exist: directed weighted graphs (digraphs) where edges have direction, and undirected weighted graphs where edges represent bidirectional connections. A social network might use undirected edges with weights representing interaction frequency, while a delivery route planner uses directed edges with weights representing one-way street distances.
# Simple weighted graph representation
graph = {
'A' => { 'B' => 4, 'C' => 2 },
'B' => { 'A' => 4, 'C' => 1, 'D' => 5 },
'C' => { 'A' => 2, 'B' => 1, 'D' => 8 },
'D' => { 'B' => 5, 'C' => 8 }
}
# A connects to B (weight 4) and C (weight 2)
# B connects to A, C, and D with respective weights
The primary operations on weighted graphs include finding shortest paths between nodes, identifying minimum spanning trees that connect all nodes with minimum total weight, and detecting negative weight cycles that create paradoxical situations in certain algorithms. Different algorithms handle specific weight constraints: Dijkstra's algorithm requires non-negative weights, while Bellman-Ford handles negative weights but has higher computational cost.
Key Principles
A weighted graph G consists of a set V of vertices and a set E of edges, where each edge e connects two vertices and carries a weight w(e). For directed graphs, an edge (u, v) with weight w represents a connection from vertex u to vertex v with cost w. For undirected graphs, edge {u, v} represents a bidirectional connection.
The weight function assigns numerical values to edges based on the problem domain. In network routing, weights represent latency or bandwidth. In geographic mapping, weights represent physical distances. In financial systems, weights might represent transaction costs or risk factors. Negative weights appear in problems like arbitrage detection in currency exchange, where cycles with negative total weight indicate profitable opportunities.
Path weight calculation sums the weights of edges along a path. For path p = (v₀, v₁, v₂, ..., vₖ), the total weight equals w(v₀, v₁) + w(v₁, v₂) + ... + w(vₖ₋₁, vₖ). The shortest path between two vertices represents the path with minimum total weight among all possible paths. Multiple shortest paths may exist with equal weight.
# Path weight calculation
def path_weight(graph, path)
weight = 0
(0...path.length - 1).each do |i|
current = path[i]
next_node = path[i + 1]
return Float::INFINITY unless graph[current] && graph[current][next_node]
weight += graph[current][next_node]
end
weight
end
path = ['A', 'B', 'C', 'D']
# Calculates: weight(A->B) + weight(B->C) + weight(C->D)
Graph connectivity determines which paths exist. A strongly connected directed graph has a path from every vertex to every other vertex. A weakly connected directed graph becomes connected when treating edges as undirected. Connected components group vertices that can reach each other, which matters when operations require reachability between nodes.
Negative weight cycles create algorithmic challenges. A cycle whose edge weights sum to a negative value means traversing the cycle repeatedly decreases total path weight without bound, making the concept of "shortest path" undefined for vertices reachable from the cycle. Detection and handling of negative cycles distinguishes different shortest path algorithms.
The triangle inequality w(u, w) ≤ w(u, v) + w(v, w) holds in many real-world graphs but not all. Geographic distances satisfy the triangle inequality (going direct never costs more than going through an intermediate point), but other domains violate it. This property affects algorithm correctness and optimization opportunities.
Implementation Approaches
Three primary representations exist for weighted graphs, each with distinct performance characteristics and memory requirements.
Adjacency matrix representation uses a two-dimensional array where matrix[i][j] stores the weight of the edge from vertex i to vertex j. Non-existent edges typically use infinity, null, or a sentinel value. This representation excels for dense graphs where most vertex pairs connect. Edge existence checking and weight retrieval operate in O(1) constant time. Space complexity remains O(V²) regardless of actual edge count, making it inefficient for sparse graphs.
# Adjacency matrix (conceptual)
A B C D
A [ 0 4 2 inf]
B [ 4 0 1 5 ]
C [ 2 1 0 8 ]
D [inf 5 8 0 ]
# inf represents no edge
# Diagonal typically 0 (vertex to itself)
Adjacency list representation stores each vertex with a list of its outgoing edges and their weights. For each vertex, the structure maintains pairs of (neighbor, weight). This approach optimizes space for sparse graphs, using O(V + E) space proportional to actual edges. Iterating over a vertex's neighbors takes O(degree(v)) time, making it efficient for graph traversal algorithms that examine each edge once.
# Adjacency list structure
adjacency_list = {
'A' => [['B', 4], ['C', 2]],
'B' => [['A', 4], ['C', 1], ['D', 5]],
'C' => [['A', 2], ['B', 1], ['D', 8]],
'D' => [['B', 5], ['C', 8]]
}
# Each vertex maps to array of [neighbor, weight] pairs
Edge list representation stores edges as a collection of (source, destination, weight) tuples. This simple format suits algorithms that process edges sequentially, such as Kruskal's minimum spanning tree algorithm that sorts edges by weight. Edge lists use O(E) space and facilitate parallel processing since edges store independently. However, finding all edges from a specific vertex requires scanning the entire list in O(E) time.
# Edge list structure
edge_list = [
['A', 'B', 4],
['A', 'C', 2],
['B', 'C', 1],
['B', 'D', 5],
['C', 'D', 8]
]
# Each entry: [source, destination, weight]
Hybrid approaches combine representations for specific algorithm requirements. Storing both adjacency lists and a reverse adjacency list (incoming edges) supports efficient bidirectional search. Maintaining an adjacency matrix for frequent weight lookups alongside edge lists for sorting optimizes certain problem types at the cost of increased memory.
Selection criteria depend on graph density (edge count relative to maximum possible edges), operation frequency (insertions, deletions, lookups), and memory constraints. Dense graphs benefit from adjacency matrices when edge queries dominate operations. Sparse graphs with traversal-heavy algorithms favor adjacency lists. Algorithms processing edges independently work well with edge lists.
Ruby Implementation
Ruby's flexible data structures enable clean weighted graph implementations. Hash-based adjacency lists provide the most practical approach for general-purpose weighted graphs.
class WeightedGraph
def initialize(directed: false)
@graph = Hash.new { |h, k| h[k] = {} }
@directed = directed
end
def add_edge(from, to, weight)
@graph[from][to] = weight
@graph[to][from] = weight unless @directed
end
def weight(from, to)
@graph[from][to]
end
def neighbors(vertex)
@graph[vertex].keys
end
def edges_from(vertex)
@graph[vertex]
end
def vertices
@graph.keys
end
def edge_count
if @directed
@graph.values.sum { |edges| edges.size }
else
@graph.values.sum { |edges| edges.size } / 2
end
end
end
# Usage
g = WeightedGraph.new(directed: true)
g.add_edge('A', 'B', 4)
g.add_edge('A', 'C', 2)
g.add_edge('B', 'D', 5)
g.neighbors('A') # => ['B', 'C']
g.weight('A', 'B') # => 4
The hash-based structure uses nested hashes: outer hash maps vertices to inner hashes, which map neighboring vertices to edge weights. Using Hash.new { |h, k| h[k] = {} } automatically initializes missing vertices with empty hashes, preventing nil reference errors during edge addition.
Edge weight updates replace existing values by reassigning to the same hash key. Edge removal uses hash deletion operations:
class WeightedGraph
def remove_edge(from, to)
@graph[from].delete(to) if @graph[from]
@graph[to].delete(from) if !@directed && @graph[to]
end
def remove_vertex(vertex)
# Remove vertex and all its edges
@graph.delete(vertex)
# Remove edges pointing to this vertex
@graph.each_value do |edges|
edges.delete(vertex)
end
end
end
Matrix representation suits dense graphs or when O(1) weight lookup justifies O(V²) space:
class MatrixWeightedGraph
def initialize(vertices)
@vertices = vertices
@size = vertices.size
@vertex_index = vertices.each_with_index.to_h
@matrix = Array.new(@size) { Array.new(@size, Float::INFINITY) }
@size.times { |i| @matrix[i][i] = 0 }
end
def add_edge(from, to, weight)
i = @vertex_index[from]
j = @vertex_index[to]
@matrix[i][j] = weight
end
def weight(from, to)
i = @vertex_index[from]
j = @vertex_index[to]
@matrix[i][j]
end
def neighbors(vertex)
i = @vertex_index[vertex]
@vertices.select.with_index do |v, j|
@matrix[i][j] != Float::INFINITY && i != j
end
end
end
The matrix approach maintains a vertex-to-index mapping for O(1) lookups. Infinity represents absent edges, distinguishing them from zero-weight edges. The diagonal initializes to zero since vertex distance to itself equals zero.
Edge list implementation supports algorithms that process edges independently:
class EdgeListGraph
Edge = Struct.new(:from, :to, :weight) do
def <=>(other)
weight <=> other.weight
end
end
def initialize
@edges = []
@vertices = Set.new
end
def add_edge(from, to, weight)
@edges << Edge.new(from, to, weight)
@vertices.add(from)
@vertices.add(to)
end
def edges
@edges
end
def sorted_edges
@edges.sort
end
def vertices
@vertices.to_a
end
end
Using a Struct for edges provides comparison operators for sorting by weight. This representation excels for Kruskal's algorithm, which requires sorted edges.
File I/O handling enables loading graphs from external sources:
class WeightedGraph
def self.from_file(filename, directed: false)
graph = new(directed: directed)
File.readlines(filename).each do |line|
from, to, weight = line.strip.split
graph.add_edge(from, to, weight.to_f)
end
graph
end
def to_file(filename)
File.open(filename, 'w') do |f|
@graph.each do |from, edges|
edges.each do |to, weight|
f.puts "#{from} #{to} #{weight}"
end
end
end
end
end
Graph validation ensures data integrity:
class WeightedGraph
def validate!
@graph.each do |from, edges|
edges.each do |to, weight|
raise "Negative weight: #{from}->#{to}" if weight < 0 && !allow_negative?
raise "Invalid weight: #{weight}" unless weight.is_a?(Numeric)
raise "Self-loop: #{from}" if from == to && !allow_self_loops?
end
end
end
private
def allow_negative?
@allow_negative ||= false
end
def allow_self_loops?
@allow_self_loops ||= false
end
end
Common Patterns
Dijkstra's algorithm finds shortest paths from a source vertex to all other vertices in graphs with non-negative edge weights. The algorithm maintains a priority queue of vertices ordered by tentative distance from source, iteratively selecting the closest unvisited vertex and updating distances to its neighbors.
require 'set'
def dijkstra(graph, source)
distances = Hash.new(Float::INFINITY)
distances[source] = 0
previous = {}
unvisited = Set.new(graph.vertices)
while unvisited.any?
# Find unvisited vertex with minimum distance
current = unvisited.min_by { |v| distances[v] }
break if distances[current] == Float::INFINITY
unvisited.delete(current)
# Update distances to neighbors
graph.edges_from(current).each do |neighbor, weight|
next unless unvisited.include?(neighbor)
alt_distance = distances[current] + weight
if alt_distance < distances[neighbor]
distances[neighbor] = alt_distance
previous[neighbor] = current
end
end
end
{ distances: distances, previous: previous }
end
# Reconstruct shortest path
def shortest_path(previous, target)
path = []
current = target
while current
path.unshift(current)
current = previous[current]
end
path
end
This implementation uses a simple linear search to find the minimum distance vertex. Production implementations use a priority queue (binary heap or Fibonacci heap) to reduce the find-minimum operation from O(V) to O(log V), improving overall complexity from O(V²) to O(E log V).
# Priority queue implementation using Ruby's SortedSet
require 'rbtree'
def dijkstra_optimized(graph, source)
distances = Hash.new(Float::INFINITY)
distances[source] = 0
previous = {}
# Priority queue: [distance, vertex]
pq = RBTree.new
pq[0] = source
visited = Set.new
until pq.empty?
distance, current = pq.shift
next if visited.include?(current)
visited.add(current)
graph.edges_from(current).each do |neighbor, weight|
next if visited.include?(neighbor)
alt_distance = distances[current] + weight
if alt_distance < distances[neighbor]
distances[neighbor] = alt_distance
previous[neighbor] = current
pq[alt_distance] = neighbor
end
end
end
{ distances: distances, previous: previous }
end
Bellman-Ford algorithm handles graphs with negative edge weights and detects negative weight cycles. The algorithm relaxes all edges V-1 times, where V is the vertex count. If the V-th iteration still updates distances, a negative cycle exists.
def bellman_ford(graph, source)
distances = Hash.new(Float::INFINITY)
distances[source] = 0
previous = {}
vertices = graph.vertices
# Relax edges V-1 times
(vertices.size - 1).times do
vertices.each do |u|
graph.edges_from(u).each do |v, weight|
alt_distance = distances[u] + weight
if alt_distance < distances[v]
distances[v] = alt_distance
previous[v] = u
end
end
end
end
# Check for negative cycles
vertices.each do |u|
graph.edges_from(u).each do |v, weight|
if distances[u] + weight < distances[v]
return { negative_cycle: true }
end
end
end
{ distances: distances, previous: previous, negative_cycle: false }
end
The algorithm has O(VE) time complexity, making it slower than Dijkstra for dense graphs but necessary when negative weights exist. Applications include financial arbitrage detection and network flow problems.
Prim's algorithm computes minimum spanning trees for connected, undirected weighted graphs. The algorithm grows a tree from an arbitrary start vertex, repeatedly adding the minimum-weight edge connecting a tree vertex to a non-tree vertex.
def prim_mst(graph, start_vertex)
mst_edges = []
visited = Set.new([start_vertex])
vertices = graph.vertices
while visited.size < vertices.size
min_edge = nil
min_weight = Float::INFINITY
# Find minimum edge crossing the cut
visited.each do |u|
graph.edges_from(u).each do |v, weight|
next if visited.include?(v)
if weight < min_weight
min_weight = weight
min_edge = [u, v, weight]
end
end
end
break unless min_edge
from, to, weight = min_edge
mst_edges << min_edge
visited.add(to)
end
mst_edges
end
# Calculate total MST weight
def mst_weight(mst_edges)
mst_edges.sum { |_, _, weight| weight }
end
Kruskal's algorithm provides an alternative MST approach using edge sorting and union-find data structure. The algorithm sorts edges by weight and adds each edge to the MST if it doesn't create a cycle.
class UnionFind
def initialize(vertices)
@parent = vertices.to_h { |v| [v, v] }
@rank = vertices.to_h { |v| [v, 0] }
end
def find(vertex)
if @parent[vertex] != vertex
@parent[vertex] = find(@parent[vertex]) # Path compression
end
@parent[vertex]
end
def union(u, v)
root_u = find(u)
root_v = find(v)
return false if root_u == root_v # Already connected
# Union by rank
if @rank[root_u] < @rank[root_v]
@parent[root_u] = root_v
elsif @rank[root_u] > @rank[root_v]
@parent[root_v] = root_u
else
@parent[root_v] = root_u
@rank[root_u] += 1
end
true
end
end
def kruskal_mst(graph)
edges = []
graph.vertices.each do |u|
graph.edges_from(u).each do |v, weight|
edges << [u, v, weight] if u < v # Avoid duplicates in undirected graph
end
end
edges.sort_by! { |_, _, weight| weight }
uf = UnionFind.new(graph.vertices)
mst_edges = []
edges.each do |u, v, weight|
if uf.union(u, v)
mst_edges << [u, v, weight]
break if mst_edges.size == graph.vertices.size - 1
end
end
mst_edges
end
Floyd-Warshall algorithm computes shortest paths between all vertex pairs using dynamic programming. The algorithm considers paths through intermediate vertices, iteratively improving distance estimates.
def floyd_warshall(graph)
vertices = graph.vertices
n = vertices.size
vertex_index = vertices.each_with_index.to_h
# Initialize distance matrix
dist = Array.new(n) { Array.new(n, Float::INFINITY) }
next_vertex = Array.new(n) { Array.new(n) }
n.times { |i| dist[i][i] = 0 }
vertices.each do |u|
graph.edges_from(u).each do |v, weight|
i = vertex_index[u]
j = vertex_index[v]
dist[i][j] = weight
next_vertex[i][j] = j
end
end
# Floyd-Warshall iterations
n.times do |k|
n.times do |i|
n.times do |j|
if dist[i][k] + dist[k][j] < dist[i][j]
dist[i][j] = dist[i][k] + dist[k][j]
next_vertex[i][j] = next_vertex[i][k]
end
end
end
end
{ distances: dist, next_vertex: next_vertex, vertices: vertices }
end
def reconstruct_path_fw(result, from, to)
vertex_index = result[:vertices].each_with_index.to_h
i = vertex_index[from]
j = vertex_index[to]
return [] if result[:distances][i][j] == Float::INFINITY
path = [from]
while from != to
i = vertex_index[from]
j = vertex_index[to]
from = result[:vertices][result[:next_vertex][i][j]]
path << from
end
path
end
The O(V³) time complexity makes Floyd-Warshall suitable for small to medium graphs where all-pairs shortest paths are needed. The algorithm handles negative edges but not negative cycles.
Performance Considerations
Graph representation choice significantly impacts algorithm performance. Adjacency lists optimize sparse graph traversal, requiring O(V + E) space and O(degree(v)) time to iterate neighbors. Dense graphs with E approaching V² benefit from adjacency matrices that provide O(1) edge weight lookup at O(V²) space cost.
Dijkstra's algorithm complexity depends on priority queue implementation. Using a simple array for the priority queue yields O(V²) time complexity. Binary heap priority queues improve this to O((V + E) log V). Fibonacci heaps theoretically achieve O(E + V log V), though implementation complexity and constant factors often make binary heaps more practical.
# Comparing graph representations for neighbor iteration
require 'benchmark'
# Dense graph: 1000 vertices, ~500,000 edges
def benchmark_representations(vertex_count, edge_count)
# Build adjacency list
list_graph = {}
vertex_count.times do |i|
list_graph[i] = []
end
edge_count.times do
from = rand(vertex_count)
to = rand(vertex_count)
weight = rand(1..100)
list_graph[from] << [to, weight]
end
# Build adjacency matrix
matrix_graph = Array.new(vertex_count) { Array.new(vertex_count, nil) }
list_graph.each do |from, edges|
edges.each do |to, weight|
matrix_graph[from][to] = weight
end
end
Benchmark.bm(20) do |x|
x.report("List neighbor scan:") do
10000.times do
vertex = rand(vertex_count)
list_graph[vertex].each { |to, weight| weight * 2 }
end
end
x.report("Matrix neighbor scan:") do
10000.times do
vertex = rand(vertex_count)
vertex_count.times do |i|
weight = matrix_graph[vertex][i]
weight * 2 if weight
end
end
end
end
end
Memory locality affects cache performance. Adjacency matrices store data contiguously, improving cache hit rates during dense graph operations. Adjacency lists scatter data across heap allocations, potentially causing cache misses. For algorithms that repeatedly access the same edges, matrix representation may perform better despite theoretical complexity disadvantages.
Negative weight handling separates algorithm classes. Dijkstra runs in O(E log V) with binary heap but fails with negative weights. Bellman-Ford handles negative weights in O(VE) time. Floyd-Warshall computes all-pairs shortest paths in O(V³), handling negative edges but detecting, not handling, negative cycles. Johnson's algorithm combines both approaches: use Bellman-Ford once to reweight edges, then run Dijkstra from each vertex, achieving O(V² log V + VE) for sparse graphs.
Minimum spanning tree algorithms exhibit different performance profiles. Prim's algorithm with binary heap runs in O(E log V), performing well on dense graphs. Kruskal's algorithm requires O(E log E) for edge sorting plus O(E α(V)) for union-find operations, where α is the inverse Ackermann function (effectively constant). Kruskal excels on sparse graphs where edge sorting dominates.
Parallel processing opportunities exist in weighted graph algorithms. Bellman-Ford's edge relaxation parallelizes naturally since each edge examines independently during each iteration. Floyd-Warshall's inner loops parallelize for each intermediate vertex k. Dijkstra's algorithm parallelizes less naturally due to priority queue synchronization, though Delta-stepping algorithm variants enable parallel shortest path computation.
# Parallel edge relaxation in Bellman-Ford (conceptual)
require 'concurrent'
def parallel_bellman_ford(graph, source, thread_count: 4)
distances = Concurrent::Hash.new(Float::INFINITY)
distances[source] = 0
previous = Concurrent::Hash.new
vertices = graph.vertices
edges = []
# Collect all edges
vertices.each do |u|
graph.edges_from(u).each do |v, weight|
edges << [u, v, weight]
end
end
# Parallel edge relaxation
(vertices.size - 1).times do
pool = Concurrent::FixedThreadPool.new(thread_count)
edges.each do |u, v, weight|
pool.post do
alt = distances[u] + weight
if alt < distances[v]
distances[v] = alt
previous[v] = u
end
end
end
pool.shutdown
pool.wait_for_termination
end
{ distances: distances.to_h, previous: previous.to_h }
end
Approximation algorithms trade accuracy for performance on large graphs. For shortest paths, A* algorithm uses heuristics to guide search, reducing explored vertices. For traveling salesman problems on weighted graphs, Christofides algorithm guarantees solutions within 1.5× optimal in O(V³) time. Metric TSP admits 1.5-approximation while general weighted graphs remain NP-hard even to approximate closely.
Practical Examples
Network Routing: Internet routing protocols model networks as weighted directed graphs where edge weights represent link costs based on bandwidth, latency, or administrative policies. The protocol computes shortest paths to determine packet forwarding tables.
class NetworkRouter
def initialize
@network = WeightedGraph.new(directed: true)
end
def add_link(from_router, to_router, cost)
@network.add_edge(from_router, to_router, cost)
end
def compute_routing_table(source)
result = dijkstra(@network, source)
distances = result[:distances]
previous = result[:previous]
routing_table = {}
@network.vertices.each do |dest|
next if dest == source
path = shortest_path(previous, dest)
next if path.empty?
# Next hop is second vertex in shortest path
next_hop = path[1]
routing_table[dest] = {
next_hop: next_hop,
distance: distances[dest],
path: path
}
end
routing_table
end
def handle_link_failure(from, to)
@network.remove_edge(from, to)
# Trigger routing table recomputation
end
end
# Usage
router = NetworkRouter.new
router.add_link('R1', 'R2', 10)
router.add_link('R1', 'R3', 5)
router.add_link('R2', 'R4', 1)
router.add_link('R3', 'R4', 8)
router.add_link('R3', 'R2', 3)
table = router.compute_routing_table('R1')
# Routes packets from R1 to all reachable routers
# R1 -> R4: via R2 (cost 14)
GPS Navigation: Mapping applications represent road networks as weighted graphs where vertices represent intersections and edges represent road segments. Edge weights combine distance, speed limits, traffic conditions, and user preferences.
class GPSNavigator
def initialize
@road_network = WeightedGraph.new(directed: true)
@coordinates = {}
end
def add_road(from_intersection, to_intersection, distance, speed_limit)
travel_time = (distance / speed_limit.to_f) * 60 # minutes
@road_network.add_edge(from_intersection, to_intersection, travel_time)
end
def set_coordinates(intersection, lat, lon)
@coordinates[intersection] = { lat: lat, lon: lon }
end
def find_route(start, destination, prefer: :time)
if prefer == :time
result = dijkstra(@road_network, start)
else
# Recalculate weights for distance preference
# Would require rebuilding graph or maintaining multiple weight sets
end
path = shortest_path(result[:previous], destination)
total_time = result[:distances][destination]
{
path: path,
estimated_time: total_time,
instructions: generate_turn_by_turn(path)
}
end
def update_traffic(from, to, delay_factor)
current_weight = @road_network.weight(from, to)
new_weight = current_weight * delay_factor
@road_network.add_edge(from, to, new_weight)
end
private
def generate_turn_by_turn(path)
instructions = []
path.each_cons(2) do |from, to|
instructions << "Continue to #{to}"
end
instructions
end
end
Project Dependency Management: Software build systems model project dependencies as weighted directed acyclic graphs. Vertices represent tasks or modules, edges represent dependencies, and weights represent build times or priority levels.
class BuildSystem
Task = Struct.new(:name, :build_time, :dependencies)
def initialize
@tasks = {}
@graph = WeightedGraph.new(directed: true)
end
def add_task(name, build_time:, dependencies: [])
@tasks[name] = Task.new(name, build_time, dependencies)
dependencies.each do |dep|
@graph.add_edge(dep, name, @tasks[dep].build_time)
end
end
def critical_path
# Find longest path (critical path) using topological sort
sorted = topological_sort
distances = Hash.new(0)
previous = {}
sorted.each do |task|
@graph.edges_from(task).each do |dependent, weight|
alt = distances[task] + weight
if alt > distances[dependent]
distances[dependent] = alt
previous[dependent] = task
end
end
end
# Find task with maximum distance
end_task = distances.max_by { |_, dist| dist }[0]
path = []
current = end_task
while current
path.unshift(current)
current = previous[current]
end
{
path: path,
total_time: distances[end_task],
tasks: path.map { |t| @tasks[t] }
}
end
private
def topological_sort
in_degree = Hash.new(0)
@graph.vertices.each do |v|
@graph.edges_from(v).each do |neighbor, _|
in_degree[neighbor] += 1
end
end
queue = @graph.vertices.select { |v| in_degree[v] == 0 }
sorted = []
while queue.any?
current = queue.shift
sorted << current
@graph.edges_from(current).each do |neighbor, _|
in_degree[neighbor] -= 1
queue << neighbor if in_degree[neighbor] == 0
end
end
sorted
end
end
# Usage
build = BuildSystem.new
build.add_task('parse', build_time: 5)
build.add_task('compile', build_time: 20, dependencies: ['parse'])
build.add_task('test', build_time: 15, dependencies: ['compile'])
build.add_task('package', build_time: 10, dependencies: ['test'])
critical = build.critical_path
# Identifies longest build chain determining minimum build time
Social Network Analysis: Social platforms model user connections as weighted undirected graphs where edge weights represent interaction frequency, relationship strength, or shared interests. Algorithms identify communities, recommend connections, and detect influential users.
class SocialNetwork
def initialize
@network = WeightedGraph.new(directed: false)
@user_data = {}
end
def add_friendship(user1, user2, interaction_score: 1)
@network.add_edge(user1, user2, interaction_score)
end
def update_interaction(user1, user2, additional_score)
current = @network.weight(user1, user2) || 0
@network.add_edge(user1, user2, current + additional_score)
end
def friend_recommendations(user, count: 5)
# Friends of friends weighted by mutual connection strength
direct_friends = @network.neighbors(user).to_set
recommendations = Hash.new(0)
direct_friends.each do |friend|
@network.neighbors(friend).each do |friend_of_friend|
next if friend_of_friend == user
next if direct_friends.include?(friend_of_friend)
# Weight by strength of intermediate connection
friend_strength = @network.weight(user, friend)
recommendations[friend_of_friend] += friend_strength
end
end
recommendations.sort_by { |_, score| -score }.first(count).to_h
end
def connection_strength(user1, user2)
# Measure strength using shortest path weight
result = dijkstra(@network, user1)
distance = result[:distances][user2]
return 0 if distance == Float::INFINITY
# Convert distance to strength (inverse relationship)
1.0 / distance
end
end
Arbitrage Detection: Financial systems detect arbitrage opportunities in currency exchange by modeling exchange rates as weighted directed graphs. A negative weight cycle indicates a profitable trading sequence.
class CurrencyExchange
def initialize
@rates = WeightedGraph.new(directed: true)
end
def add_rate(from_currency, to_currency, exchange_rate)
# Use negative log to convert multiplication to addition
weight = -Math.log(exchange_rate)
@rates.add_edge(from_currency, to_currency, weight)
end
def find_arbitrage
# Use Bellman-Ford to detect negative cycles
currencies = @rates.vertices
return nil if currencies.empty?
source = currencies.first
distances = Hash.new(Float::INFINITY)
distances[source] = 0
previous = {}
# Relax edges V-1 times
(currencies.size - 1).times do
currencies.each do |from|
@rates.edges_from(from).each do |to, weight|
alt = distances[from] + weight
if alt < distances[to]
distances[to] = alt
previous[to] = from
end
end
end
end
# Check for negative cycle
currencies.each do |from|
@rates.edges_from(from).each do |to, weight|
if distances[from] + weight < distances[to]
# Found negative cycle - reconstruct it
cycle = reconstruct_cycle(previous, to)
return {
cycle: cycle,
profit: calculate_profit(cycle)
}
end
end
end
nil # No arbitrage found
end
private
def reconstruct_cycle(previous, start)
cycle = [start]
visited = Set.new([start])
current = previous[start]
while current && !visited.include?(current)
cycle.unshift(current)
visited.add(current)
current = previous[current]
end
# Trim to actual cycle
cycle_start_index = cycle.index(current)
cycle[cycle_start_index..-1]
end
def calculate_profit(cycle)
amount = 1.0
cycle.each_cons(2) do |from, to|
rate = Math.exp(-@rates.weight(from, to))
amount *= rate
end
amount - 1.0 # Profit percentage
end
end
# Usage
exchange = CurrencyExchange.new
exchange.add_rate('USD', 'EUR', 0.85)
exchange.add_rate('EUR', 'GBP', 0.90)
exchange.add_rate('GBP', 'USD', 1.35)
arbitrage = exchange.find_arbitrage
# Detects if USD -> EUR -> GBP -> USD yields profit
Reference
Graph Representations Comparison
| Representation | Space | Edge Lookup | Iterate Neighbors | Add Edge | Remove Edge | Best For |
|---|---|---|---|---|---|---|
| Adjacency List | O(V + E) | O(degree(v)) | O(degree(v)) | O(1) | O(degree(v)) | Sparse graphs, traversal |
| Adjacency Matrix | O(V²) | O(1) | O(V) | O(1) | O(1) | Dense graphs, frequent lookups |
| Edge List | O(E) | O(E) | O(E) | O(1) | O(E) | Sorting edges, parallel processing |
Shortest Path Algorithms
| Algorithm | Time Complexity | Space | Negative Weights | Negative Cycles | Use Case |
|---|---|---|---|---|---|
| Dijkstra (binary heap) | O((V + E) log V) | O(V) | No | N/A | Non-negative weights, single source |
| Dijkstra (Fibonacci heap) | O(E + V log V) | O(V) | No | N/A | Theoretical optimum |
| Bellman-Ford | O(VE) | O(V) | Yes | Detects | Negative weights, single source |
| Floyd-Warshall | O(V³) | O(V²) | Yes | Detects | All pairs, small graphs |
| Johnson's | O(V² log V + VE) | O(V²) | Yes | Fails if present | All pairs, sparse graphs |
Minimum Spanning Tree Algorithms
| Algorithm | Time Complexity | Space | Approach | Best For |
|---|---|---|---|---|
| Prim (binary heap) | O(E log V) | O(V) | Grows tree from vertex | Dense graphs |
| Kruskal | O(E log E) | O(V) | Sorts edges, union-find | Sparse graphs |
| Borůvka | O(E log V) | O(V) | Parallel-friendly | Parallel processing |
Common Graph Operations
| Operation | Adjacency List | Adjacency Matrix | Edge List |
|---|---|---|---|
| Check if edge exists | O(degree(v)) | O(1) | O(E) |
| Get edge weight | O(degree(v)) | O(1) | O(E) |
| Add vertex | O(1) | O(V²) | O(1) |
| Remove vertex | O(E) | O(V²) | O(E) |
| Find all edges | O(E) | O(V²) | O(E) |
| Graph copy | O(V + E) | O(V²) | O(E) |
Ruby Graph Implementation Patterns
| Pattern | Code | Use Case |
|---|---|---|
| Hash adjacency list | Hash.new { |h, k| h[k] = {} } | General purpose graphs |
| Struct for edges | Struct.new(:from, :to, :weight) | Edge list with sorting |
| Matrix with infinity | Array.new(n) { Array.new(n, Float::INFINITY) } | Dense graphs, Floyd-Warshall |
| Priority queue | RBTree for ordered pairs | Optimized Dijkstra |
| Union-Find | Path compression + union by rank | Kruskal's algorithm |
Weight Types and Transformations
| Weight Meaning | Transformation | Algorithm Consideration |
|---|---|---|
| Distance | None | Standard shortest path |
| Time duration | None | Standard shortest path |
| Cost | None | Standard shortest path |
| Profit | Negate weights | Convert to cost minimization |
| Probability | -log(probability) | Convert multiplication to addition |
| Capacity | Use as-is or invert | Max flow vs shortest path |
| Bandwidth | Invert: 1/bandwidth | Higher bandwidth = lower cost |
Algorithm Selection Guide
| Problem | Graph Type | Weight Constraints | Best Algorithm |
|---|---|---|---|
| Single-source shortest path | Any | Non-negative | Dijkstra |
| Single-source shortest path | Any | Any weights | Bellman-Ford |
| All-pairs shortest path | Dense | Any weights | Floyd-Warshall |
| All-pairs shortest path | Sparse | Any weights | Johnson's |
| Minimum spanning tree | Undirected | Any weights | Prim or Kruskal |
| Negative cycle detection | Any | Any weights | Bellman-Ford |
| Longest path | DAG | Any weights | Topological sort + dynamic programming |
Common Edge Cases
| Scenario | Handling Strategy | Impact |
|---|---|---|
| Self-loops | Allow or reject based on problem | May affect cycle detection |
| Parallel edges | Keep minimum weight or all | Affects graph representation choice |
| Zero-weight edges | Handle normally | Works in all algorithms |
| Negative weights | Use Bellman-Ford | Dijkstra fails |
| Negative cycles | Detect and report | Shortest path undefined |
| Disconnected components | Process separately | Affects reachability |
| Missing vertices | Initialize on demand | Hash.new default helps |
Time Complexity Summary
| Graph Size | Operation | Acceptable Complexity |
|---|---|---|
| V < 100 | All-pairs shortest path | O(V³) Floyd-Warshall |
| V < 1000 | Single-source shortest path | O(V²) simple Dijkstra |
| V < 10000 | Single-source shortest path | O(E log V) heap Dijkstra |
| V > 10000 | Single-source shortest path | O(E + V log V) Fibonacci heap |
| E < V² / log V | Sparse graph algorithms | Adjacency list preferred |
| E > V² / 2 | Dense graph algorithms | Adjacency matrix acceptable |