Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions data_structures/graphs/bfs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'set'

##
# This class represents the result of a breadth-first search performed on an unweighted graph.
#
# It exposes:
# - the set of visited nodes
# - a hash of distances by node from the search root node
# (only for visited nodes, 0 for the search root node);
# - a hash of parent nodes by node
# (only for visited nodes, nil for the search root node).

class GraphBfsResult
attr_reader :visited
attr_reader :parents
attr_reader :distances

def initialize(visited, parents, distances)
@visited = visited
@parents = parents
@distances = distances
end
end

##
# Performs a breadth-first search for the provided graph, starting at the given node.
# Returns the search result (see GraphBfsResult).
# Nodes are consumed using the provided consumers upon being first seen, or being completely visited
# (nothing, by default).
#
# The algorithm has a time complexity of O(|V| + |E|), where:
# - |V| is the number of nodes in the graph;
# - |E| is the number of edges in the graph.

def bfs(graph, start_node, seen_node_consumer: method(:do_nothing_on_node), visited_node_consumer: method(:do_nothing_on_node))
seen = Set[]
visited = Set[]
parents = { start_node => nil }
distances = { start_node => 0 }

seen.add(start_node)
seen_node_consumer.call(start_node)
q = Queue.new
q.push(start_node)
until q.empty?
node = q.pop
for neighbor in graph.neighbors(node)
unless seen.include?(neighbor)
seen.add(neighbor)
distances[neighbor] = distances[node] + 1
parents[neighbor] = node
seen_node_consumer.call(neighbor)
q.push(neighbor)
end
end
visited.add(node)
visited_node_consumer.call(node)
end

GraphBfsResult.new(visited, parents, distances)
end

private
def do_nothing_on_node(node)
end
89 changes: 89 additions & 0 deletions data_structures/graphs/bfs_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require 'minitest/autorun'
require_relative 'bfs'
require_relative 'unweighted_graph'

class TestBfs < Minitest::Test
def test_bfs_visits_single_graph_node
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
graph.add_edge(:u, :v)

bfs_result = bfs(graph, :w)

assert bfs_result.visited.to_set == [:w].to_set
assert bfs_result.parents == {
:w => nil
}
assert bfs_result.distances == {
:w => 0
}
end

def test_bfs_visits_graph_fully
graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x], directed: false)
graph.add_edge(:u, :v)
graph.add_edge(:u, :w)
graph.add_edge(:w, :x)

bfs_result = bfs(graph, :u)

assert bfs_result.visited.to_set == [:u, :v, :w, :x].to_set
assert bfs_result.parents == {
:u => nil,
:v => :u,
:w => :u,
:x => :w
}
assert bfs_result.distances == {
:u => 0,
:v => 1,
:w => 1,
:x => 2
}
end

def test_bfs_visits_graph_partially
graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x, :y, :z], directed: false)
graph.add_edge(:u, :v)
graph.add_edge(:w, :x)
graph.add_edge(:x, :y)
graph.add_edge(:y, :z)

bfs_result = bfs(graph, :x)

assert bfs_result.visited.to_set == [:w, :x, :y, :z].to_set
assert bfs_result.parents == {
:w => :x,
:x => nil,
:y => :x,
:z => :y
}
assert bfs_result.distances == {
:w => 1,
:x => 0,
:y => 1,
:z => 2
}
end

def test_bfs_visits_with_seen_node_consumer
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
graph.add_edge(:u, :v)
graph.add_edge(:u, :w)

seen_order = []
bfs(graph, :w, seen_node_consumer: ->(node) { seen_order.append(node) })

assert seen_order == [:w, :u, :v]
end

def test_bfs_visits_with_visited_node_consumer
graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false)
graph.add_edge(:u, :v)
graph.add_edge(:u, :w)

visited_order = []
bfs(graph, :w, visited_node_consumer: ->(node) { visited_order.append(node) })

assert visited_order == [:w, :u, :v]
end
end
2 changes: 1 addition & 1 deletion data_structures/graphs/unweighted_graph_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_add_edge_adds_edge_to_directed_unweighted_graph
assert graph.neighbors(:v).empty?
end

def test_add_edge_adds_edge_to_directed_unweighted_graph
def test_add_edge_adds_edge_to_undirected_unweighted_graph
graph = UnweightedGraph.new(nodes: [:u, :v], directed: false)
graph.add_edge(:u, :v)

Expand Down
2 changes: 1 addition & 1 deletion data_structures/graphs/weighted_graph_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_add_edge_adds_edge_to_directed_weighted_graph
assert graph.edges(:v).empty?
end

def test_add_edge_adds_edge_to_directed_weighted_graph
def test_add_edge_adds_edge_to_undirected_weighted_graph
graph = WeightedGraph.new(nodes: [:u, :v], directed: false)
graph.add_edge(:u, :v, 2)

Expand Down