Home > Blockchain >  BFS algorithm, tracking the path
BFS algorithm, tracking the path

Time:02-21

Anyone here familiar with BFS and tracking? I have written the algorithm for the first time and it finds the shortest path, but tracing the shortest path is the part i'm stuck at. The list below is a list of previous indexes (y, x), and when it reaches (0, 5) there are two paths, one path to the left and another to the right, i only want to include the path that leads to the destination, however i have no clue how to make it work. I keep track of previous node, but once we get to (0, 5) the setting of previous starts messing up, because there are two paths. Because of this i can't backtrace from the destination.

How have you kept track of previous and made it work? I have read so many articles but still havent found anything that really explains it to me.

Any help is greatly appreciated

[
(0, 0), 
(1, 0), 
(2, 0), 
(2, 1), 
(2, 2), 
(2, 3), 
(3, 3), 
(4, 3), 
(5, 3), 
(5, 4), 
(5, 5), 
(4, 5), 
(3, 5), 
(2, 5), 
(1, 5), 
(0, 5), <-- starts to mess up here becuase there are two paths to take, left and right
(0, 6), <-- to the right
(0, 4), <-- to the left
(0, 7), <-- to the right
(0, 3), <-- to the left
(0, 8), <-- to the right
(1, 7), <-- to the left and one down
(0, 2), <-- to the left
(0, 9)  <-- to the right (also happens to be the destination)
]

Code:

use std::collections::VecDeque;

fn bfs(arr: [[i32; 10]; 10], target: i32) -> bool {
    let mut visited = [[false, false, false, false, false, false, false, false, false, false]; 10];
    let mut queue: VecDeque<(i32, i32)> = VecDeque::new();
    queue.push_back((0, 0));

    let mut previous_nodes = Vec::new();
    let mut previous = (-1, -1);
    while !queue.is_empty() {
        let (y, x) = queue.pop_front().unwrap();

        if visited[y as usize][x as usize] == true {
            continue;
        }

        visited[y as usize][x as usize] = true;
        previous_nodes.push(previous);
        previous = (y, x);

        
        print!("{}[2J", 27 as char);
        for y in 0..visited.len() {
            for x in 0..visited.len() {
                if arr[y][x] == target && visited[y][x] == true {
                    print!("X ");
                } else if visited[y][x] == true {
                    print!("0 ");
                } else if arr[y][x] == 3 {
                    print!("# ");
                } else {
                    print!(". ");
                }
            }
            print!("\n");
        }
        print!("\n");
        

        if arr[y as usize][x as usize] == target {
            for entry in previous_nodes {
                println!("{:?}", entry);
            }
            return true;
        }

        if x   1 < arr.len() as i32 && arr[y as usize][(x   1) as usize] != 3 {
            queue.push_back((y, x   1));

        }

        if y   1 < arr.len() as i32 && arr[(y   1) as usize][x as usize] != 3 {
            queue.push_back((y   1, x));

        }

        if x - 1 >= 0 && arr[y as usize][(x - 1) as usize] != 3 {
            queue.push_back((y, x - 1));

        }

        if y - 1 >= 0 && arr[(y - 1) as usize][x as usize] != 3 {
            queue.push_back((y - 1, x));
        }
    }
    false
}

fn main() {
    let data = [
        [0, 3, 0, 0, 0, 0, 0, 0, 0, 1],
        [0, 3, 3, 3, 3, 0, 3, 0, 3, 3],
        [0, 0, 0, 0, 3, 0, 3, 0, 0, 0],
        [3, 3, 3, 0, 3, 0, 3, 3, 3, 0],
        [0, 0, 3, 0, 3, 0, 3, 0, 3, 0],
        [0, 0, 3, 0, 0, 0, 3, 0, 3, 0],
        [0, 3, 3, 3, 3, 3, 3, 0, 3, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 3, 3, 3, 3, 3, 3, 3, 3, 3],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    ];

    let b = bfs(data, 1);

    println!("Found: {}", b);
}


CodePudding user response:

First off, I started with a little bit of code cleanup to make it a bit more concise and made a rust playground to experiment with. There are two main approaches to solving this problem. Either you keep track of the path taken in the queue or the visited nodes. The easiest approach for you would likely be to simply adapt your code to have the visited nodes array point to the previous node in the path from each visited node. playground link

fn bfs(arr: [[i32; 10]; 10], target: i32) -> Option<Vec<(i32, i32)>> {
    let mut visited = [[None; 10]; 10];
    let mut queue: VecDeque<(i32, i32)> = VecDeque::new();
    queue.push_back((0, 0));

    // Put some filler into the first location
    visited[0][0] = Some((0, 0));
    
    while let Some((y, x)) = queue.pop_front() {
    
        // Prind debug info
        println!("\nExpanding ({}, {})", x, y);
        for y in 0..visited.len() {
            for x in 0..visited.len() {
                if arr[y][x] == target && visited[y][x].is_some() {
                    print!("X ");
                } else if visited[y][x].is_some() {
                    print!("0 ");
                } else if arr[y][x] == 3 {
                    print!("# ");
                } else {
                    print!(". ");
                }
            }
            println!();
        }

        // Check if this position is the target
        if arr[y as usize][x as usize] == target {
            let mut path_taken = Vec::new();
            path_taken.push((y, x));
            
            let mut prev_x = x;
            let mut prev_y = y;
            while prev_x != 0 || prev_y != 0 {
                let (py, px) = visited[prev_y as usize][prev_x as usize].unwrap();
                path_taken.push((py, px));
                prev_y = py;
                prev_x = px;
            }
            
        
            return Some(path_taken.into_iter().rev().collect())
        }

        // Iterate over adjacent offsets
        for (dx, dy) in &[(1, 0), (0, 1), (-1, 0), (0, -1)] {
            // Check if offset is within bounds
            if x   dx < 0
                || y   dy < 0
                || (y   dy) as usize >= arr.len()
                || (x   dx) as usize >= arr[(y   dy) as usize].len()
            {
                continue;
            }

            // Check if offset points to valid location
            if arr[(y   dy) as usize][(x   dx) as usize] == 3 {
                continue;
            }
            
            if visited[(y   dy) as usize][(x   dx) as usize].is_some() {
                continue;
            } 

            visited[(y   dy) as usize][(x   dx) as usize] = Some((y, x));
            queue.push_back((y   dy, x   dx));
        }
    }
    None
}

However, I personally prefer the approach of keeping track of the path taken in the queue to reduce the memory requirement for small paths. While it is not as true to your original question, my favorite version of this would be to write BFS in a way that better represents how it described mathematically using type parameters. playground link

fn bfs<N, F, R>(start: N, end: N, expand: F) -> Option<SearchPath<N>>
    where N: Copy   Eq   Hash,
          F: Fn(N) -> R,
          R: IntoIterator<Item=N> {
    let mut visited = HashSet::new();
    let mut queue = VecDeque::new();
    
    queue.push_back(SearchPath(start, None));
    visited.insert(start);
    
    while let Some(SearchPath(node, path)) = queue.pop_front() {
        if node == end {
            return Some(SearchPath(node, path))
        }
        
        let path = Rc::new(SearchPath(node, path.clone()));
        
        for edge in expand(node) {
            if !visited.contains(&edge) {
                visited.insert(edge);
                queue.push_back(SearchPath(edge, Some(path.clone())));
            }
        }
    }
    
    None
}

#[derive(Clone, PartialEq, Eq)]
pub struct SearchPath<N> (N, Option<Rc<SearchPath<N>>>);

impl<N: Debug> Debug for SearchPath<N> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match &self.1 {
            Some(v) => write!(f, "{:?} -> {:?}", v, &self.0),
            None => write!(f, "{:?}", &self.0)
        }
    }
}

Going further down the rabbit hole, we can do even more with this if we add some more type parameters. It may be a bit harder to read now, but what this lets us do is implement a bunch of different search approaches from graph theory using search as a base. Essentially, this new function search boils down the the core components of many search methods.

/// A general purpose graph search.
///  - start: Initial value to use in search queue
///  - expand: A function that takes a path, expands the edges of the top node,
///       then places the next elements in the queue according to the current search
///       approach. Additional ordering constraints may also be applied.
///  - next_node: A helper function which takes a path and evaluates if the search goal
///       has been reached. If the goal has been reached, None is returned and the current
///       path is returned. Otherwise, the top node in the given path is returned so
///       that it can be expanded.
fn search<N, P, F, R>(start: P, expand: F, next_node: R) -> Option<P>
where
    N: Eq   Hash,
    F: Fn(&P, &mut VecDeque<P>),
    R: Fn(&P) -> Option<N>,
{
    let mut visited = HashSet::new();
    let mut queue = VecDeque::new();

    queue.push_back(start);
    while let Some(path) = queue.pop_front() {
        let node = match next_node(&path) {
            Some(v) => v,
            None => return Some(path),
        };

        if visited.contains(&node) {
            continue;
        }
        visited.insert(node);

        expand(&path, &mut queue);
    }
    None
}

#[derive(Clone, PartialEq, Eq)]
pub struct WeightedSearchPath<N>(i32, N, Option<Rc<SearchPath<N>>>);

/// An example using search to find the most efficient path with weighted graph edges 
fn weighted_search<N, F, R>(start: N, end: N, expand: F) -> Option<WeightedSearchPath<N>>
where
    N: Copy   Eq   Hash,
    F: Fn(N) -> R,
    R: IntoIterator<Item=(i32, N)>,
{
    search(
        WeightedSearchPath(0, start, None),
        |WeightedSearchPath(cost, node, path), queue| {
            let path = Rc::new(SearchPath(*node, path.clone()));
            for (weight, edge) in expand(*node) {
                queue.push_back(WeightedSearchPath(cost   weight, edge, Some(path.clone())));
            }
            
            queue.make_contiguous().sort_by_key(|x| x.0);
        },
        |WeightedSearchPath(_, node, _)| {
            if *node == end {
                return None;
            }
            Some(*node)
        },
    )
}
  • Related