1
votes

I am implementing a function in ruby using BFS, and I would like to know how to print the shortest path in an undirected graph, between a start value and end value

Graph In this example the graph has ten vertices. Each node represents a vertex and it has an array that stores adjacent nodes (edges).

$ irb
graph.to_s
1. 1 -> [2 3 4 5 8 9 10]
2. 2 -> [1 4 5 6 7 8 10]
3. 3 -> [1 4 6]
4. 4 -> [1 2 3 6 7 8 9 10]
5. 5 -> [1 2 8 9 10]
6. 6 -> [2 3 4 7 8 9 10]
7. 7 -> [2 4 6 8 9]
8. 8 -> [1 2 4 5 6 7 9 10]
9. 9 -> [1 4 5 6 7 8]
10. 10 -> [1 2 4 5 6 8]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Expected Output

2,1,9 or 2,4,9, etc.

BFS

def bfs_shortest_path(graph, start=2, search=9)
    if graph.nodes[start].nil? || graph.nodes[search].nil?
        return nil
    end
    visited = Set.new
    search_queue = Queue.new
    search_queue.enq(start)
    while !search_queue.empty? do      
        current_node_key = search_queue.deq  
        current_node = graph.nodes[current_node_key]         
        visited.add(current_node_key)
        if current_node.value == search
            return current_node # I would like to return the PATH Eg. 2,1,9
        end
        adjacent_nodes_array = current_node.adjacent_nodes.map{|x| x.value}
        adjacent_nodes_array.each do |value|
            if !visited.include?(value)                   
                search_queue.enq(value)
                graph.nodes[value].concat_to_path(current_node.path_from_start)                
            end
        end        
    end
end

Node

class Node
    attr_reader :value
    attr_reader :adjacent_nodes
    def initialize(value)
        @value = value
        @adjacent_nodes = []
    end

    def add_edge(node)
        @adjacent_nodes.push(node)
    end

    def to_s
        "#{@value} -> [#{@adjacent_nodes.map(&:value).sort.join(" ")}]"
    end
end

Graph

class Graph
    attr_reader :nodes    
    def initialize
        @nodes = {}
    end

    def add_node(node)        
        @nodes[node.value] = node
    end

    def add_edge(node1,node2)
        if @nodes[node1.value].adjacent_nodes.map(&:value).include? (node2.value)        
            puts "#{node1.value} and #{node2.value} already have an edge"
        elsif node1.value == node2.value
            puts "node1.value == node2.value"
        else
            @nodes[node1.value].add_edge(@nodes[node2.value])
            @nodes[node2.value].add_edge(@nodes[node1.value])
        end
    end

    def to_s
        @nodes.keys.sort.each_with_index do |key,index|
            puts "#{index + 1}. #{@nodes[key].to_s}" 
        end
    end
end

Generating a Graph

def generate_random_graph
    g = Graph.new
    [*1..10].shuffle.each do |node_value|
        g.add_node(Node.new(node_value))
    end
    40.times do 
        key1 = g.nodes.keys.sample
        key2 = g.nodes.keys.sample
        g.add_edge(g.nodes[key1],g.nodes[key2])
    end
    return g
end

Test

graph = generate_random_graph    
graph.to_s
bfs_shortest_path(graph,2,9)
2
Can you post the Graph structure too?. Meaning the node initialization part you would have written!Maruthi Adithya
You should record that a vertex has been visited when you enqueue it. (That includes setting the start vertex to visited before the loop.) And at the same time record the parent vertex, i.e. the current_node_key that caused the vertex to be enqueued. Then when you find the end vertex, you can follow the parents back to the start to recreate the path. Note that you can use the parents list to keep track of which vertices have been visited. A vertex that has a parent has been visited.user3386109
@MaruthiAdithya Done Thank youipegasus
@ggorlen Done. Via function generate_random_graph. Thank youipegasus
@ggorlen Perfect! Thank youipegasus

2 Answers

1
votes

Thanks for the comments

Working Version

class Node
    attr_reader :value
    attr_reader :adjacent_nodes
    attr_reader :path_from_start
    def initialize(value)
        @value = value
        @adjacent_nodes = []
        @path_from_start = []
    end

    def add_edge(node)
        @adjacent_nodes.push(node)
    end

    def add_to_path(value)
        @path_from_start.push(value)
    end

    def concat_to_path(value_array)
        @path_from_start.concat(value_array)
    end

    def to_s
        "#{@value} -> [#{@adjacent_nodes.map(&:value).sort.join(" ")}]"
    end
end

class Graph
    attr_reader :nodes    
    def initialize
        @nodes = {}
    end

    def add_node(node)        
        @nodes[node.value] = node
    end

    def add_edge(node1,node2)
        if @nodes[node1.value].adjacent_nodes.map(&:value).include? (node2.value)        
            puts "#{node1.value} and #{node2.value} already have an edge"
        elsif node1.value == node2.value
            puts "node1.value == node2.value"
        else
            @nodes[node1.value].add_edge(@nodes[node2.value])
            @nodes[node2.value].add_edge(@nodes[node1.value])
        end
    end

    def to_s
        @nodes.keys.sort.each_with_index do |key,index|
            puts "#{index + 1}. #{@nodes[key].to_s}" 
        end
    end
end


def generate_random_graph
    g = Graph.new
    [*1..10].shuffle.each do |node_value|
        g.add_node(Node.new(node_value))
    end
    40.times do 
        key1 = g.nodes.keys.sample
        key2 = g.nodes.keys.sample
        g.add_edge(g.nodes[key1],g.nodes[key2])
    end
    return g
end

def bfs(graph, start_node_value=2, search_value=9)
    if graph.nodes[start_node_value].nil? || graph.nodes[search_value].nil?
        return nil
    end
    visited = Set.new
    search_queue = Queue.new
    search_queue.enq(graph.nodes[start_node_value])    
    while !search_queue.empty? do                
        current_node = search_queue.deq        
        visited.add(current_node)
        if current_node.value == search_value
            return current_node
        end
        current_node.adjacent_nodes.each do |node|
            if !visited.include?(graph.nodes[node.value])                
                search_queue.enq(graph.nodes[node.value])
            end
        end        
    end
end

def bfs_shortest_path(graph, start=2, search=9)
    if graph.nodes[start].nil? || graph.nodes[search].nil?
        return nil
    end
    visited = Set.new
    visited.add(start)
    search_queue = Queue.new
    search_queue.enq(start)
    while !search_queue.empty? do      
        current_node_key = search_queue.deq  
        current_node = graph.nodes[current_node_key]                 
        current_node.add_to_path(current_node.value)
        if current_node.value == search
            return current_node.path_from_start
        end
        adjacent_nodes_array = current_node.adjacent_nodes.map{|x| x.value}
        adjacent_nodes_array.each do |value|
            if !visited.include?(value)                   
                search_queue.enq(value)
                visited.add(value)
                graph.nodes[value].concat_to_path(current_node.path_from_start)                
            end
        end        
    end
end


def test_graph
    graph = generate_random_graph    
    graph.to_s
    bfs_shortest_path(graph,2,9)
end
1
votes

To keep a history of where you've been so you can reconstruct a path, instead of a visited set, use a hash. The hash tracks which node is the predecessor of each visited node. When you push a neighbor onto the queue, add the current node we're moving away from as the parent/predecessor by came_from[neighbor] = current.

This hash also works for eliminating cycles as visited does.

When you reach the goal, you can rebuild the path by repeatedly keying into this came_from hash until you run out of predecessors. Reverse the array (linear time) and return it as the final path.

Here's a minimal, runnable example you can adapt to your class structure:

def reconstruct_path(tail, came_from)
  path = []

  while !tail.nil?
    path << tail
    tail = came_from[tail]
  end

  path.reverse
end

def bfs(graph, start, goal)
  q = Queue.new
  q.enq(start)
  came_from = {start => nil}
  
  while !q.empty?
    curr = q.deq
    
    if graph.key? curr
      return reconstruct_path(goal, came_from) if curr == goal

      graph[curr].each do |neighbor|
        if !came_from.key?(neighbor)
          came_from[neighbor] = curr
          q.enq(neighbor)
        end
      end
    end
  end
end

graph = {
    "A" => ["B", "C"],
    "B" => ["A", "F"],
    "C" => ["D"],
    "D" => ["E", "F"],
    "E" => [],
    "F" => []
}
=begin

+----+
v    |
A--->B--->F
|         ^
V         |
C--->D----+
     |
     v
     E
   
=end
p bfs(graph, "A", "F") # => ["A", "B", "F"]
p bfs(graph, "A", "E") # => ["A", "C", "D", "E"]
p bfs(graph, "B", "E") # => ["B", "A", "C", "D", "E"]