12

I'm wondering how to implement a Breadth-first search in Scala, using functional programing.

Here is my first, impure, code :

  def bfs[S](init: S, f: S => Seq[S], finalS: S => Boolean): Option[S] = {
    val queue = collection.mutable.Queue[S]()

    queue += init
    var found: Option[S] = None

    while (!queue.isEmpty && found.isEmpty) {
      val next = queue.dequeue()
      if (finalS(next)) {
        found = Some(next)
      } else {
        f(next).foreach { s => queue += s }
      }
    }
    found
  }

Although I use only local mutability (a var and a mutable Queue), it's not purely functional.

I come up with another version :

  case class State[S](q: Queue[S], cur: S)

  def update[S](f: S => Seq[S])(s: State[S]) : State[S] = {
    val (i, q2) = s.q.dequeue
    val q3 = f(i).foldLeft(q2) { case (acc, i) => acc.enqueue(i)}
    State(q3, i)
  }

  def bfs2[S](init: S, f: S => Seq[S], finalS: S => Boolean): Option[S] = {
    val s = loop(State[S](Queue[S]().enqueue(init), init), update(f) _, (s: State[S]) => s.q.isEmpty || finalS(s.cur))
    Some(s.cur)
  }

  def loop[A](a: A, f: A => A, cond: A => Boolean) : A =
    if (cond(a)) a else loop(f(a), f, cond)

Is there a better way for both solutions ? Is it possible to use cats/scalaz to remove some boilerplate ?

Yann Moisan
  • 8,161
  • 8
  • 47
  • 91
  • Just use an (immutable) `List` instead of `Queue`. And get rid of `State` - the `cur` thingy is always the top of the queue anyway - just pass the `List` of work around as you descend the tree. – Dima Dec 27 '16 at 15:10
  • 1
    Isn't a `List` a stack instead of a queue? – Jasper-M Dec 27 '16 at 15:40
  • Well, depends on which end you you pull the data out of it. You can use an immutable `Queue` instead, which is a bit more efficient, but is also list-based. Or something like `IndexedSeq` to get constant-time access to the last element. – Dima Dec 27 '16 at 16:42
  • I'm I wrong or you're focusing only on algorithm for tree ? You example code will infinitely loop for general graph (with cycles) and is not optimal for DAG – Juh_ Sep 07 '17 at 13:01

4 Answers4

13

One nice thing about functional programming is you can take advantage of laziness to separate the traversal of your data structure from the searching part. This makes for very reusable, single responsibility code:

import scala.collection.immutable.Queue

def breadth_first_traverse[Node](node: Node, f: Node => Queue[Node]): Stream[Node] = {
  def recurse(q: Queue[Node]): Stream[Node] = {
    if (q.isEmpty) {
      Stream.Empty
    } else {
      val (node, tail) = q.dequeue
      node #:: recurse(tail ++ f(node))
    }
  }

  node #:: recurse(Queue.empty ++ f(node))
}

Now you can do a BFS by breadth_first_traverse(root, f) find (_ == 16) or use any other function in the Stream class to do useful ad hoc "queries" on a lazy breadth-first flattened Stream of your tree.

Karl Bielefeldt
  • 47,314
  • 10
  • 60
  • 94
  • Just out of curiosity, is there an advantage of using a `Stream` over an `Iterator` in this case? – Jasper-M Dec 27 '16 at 19:48
  • 2
    I don't know how they compare performance-wise. I chose `Streams` because `Iterators` are mutable and the implementation wouldn't be as concise. – Karl Bielefeldt Dec 27 '16 at 20:29
  • `f` depends on the structure being traversed. It takes a parent `Node` as an argument and returns a `Seq` containing all its children. – Karl Bielefeldt Feb 28 '17 at 05:38
  • This looks like it could have exponential running time. For example, if you're traversing a [lollipop graph](https://en.wikipedia.org/wiki/Lollipop_graph), you start in the "sugary part" and the thing you're searching for is at the very end of the stick, you add the rest of the sugary part, each of those adds the sugary part again (n^2 stream elements), each of those adds another sugary part (n^3).... about n^{n/2} iterations before you finally see the end of the stick in the stream. – Chris Jones Jul 13 '18 at 17:39
  • 2
    This example was written with a tree in mind. You'd have to adjust for arbitrary graphs. – Karl Bielefeldt Jul 13 '18 at 19:48
7

Building upon the answer given by Karl Bielefeldt, here's another solution (that doesn't involve any queue and just uses Streams).

def bfs[T](s: Stream[T], f: T => Stream[T]): Stream[T] = {
    if (s.isEmpty) s
    else s.head #:: bfs(s.tail append f(s.head), f)
}
Angad Singh
  • 71
  • 1
  • 4
  • Passing the "queue" directly into the recursive call itself results in a much cleaner solution IMO. This is easier to read and see what it is doing than Karl's solution above. I'd rename `s` to `nodes`, but it's a quibble. – Cory Klein Oct 03 '18 at 21:51
2

This is untested, but i think works:

  def bfs[S](init: S, f: S => Seq[S], finalS: S => Boolean): Option[S] = {
    def bfshelper(q: Seq[S], f: S => Seq[S], finalS: S => Boolean): Option[S] = q match {
      case Seq()               => None
      case h +: t if finalS(h) => Some(h)
      case h +: t              => bfshelper(t ++ f(h), f, finalS)
    }
    bfshelper(Seq(init), f, finalS)
  }

the trick is to keep a Seq of what remains to be checked, and, if the current element isn't a match, call ourselves with the remains of what we had to check with the children of this node appended

The Archetypal Paul
  • 41,321
  • 20
  • 104
  • 134
0

A breadth first search inevitably depends on the kind of data that is being searched. According to WikiPedia, the classic solution involves keeping track of what has already been searched so you don't get into an infinite loop.

Scala imposes an addition requirement, which is that the main tool for iterating is recursive functions, and preferably tail recursive functions.

Therefore, here is a solution using the above.

First there is a Map with people's names as Strings and the value is a Set of other strings representing other people who have a connection with the first person.

Therefore if "Fred" knows "Mary" who knows "John", you would expect "Mary" to appear in "Fred"'s list of names, and "John" to appear in "Mary"'s list.

With that in mind, here is a fully tested implementation (Courtesy of RockTheJvm)

  def socialConnection(network: Map[String, Set[String]],
                       a: String, b: String): Boolean = {
    @tailrec
    def bfs(target: String,
            consideredPeople: Set[String],
            discoveredPeople: Set[String]): Boolean = {
      if (discoveredPeople.isEmpty) false
      else {
        val person = discoveredPeople.head
        if (person == target) true
        else if(consideredPeople.contains(person))
          bfs(target, consideredPeople, discoveredPeople.tail)
        else bfs(target, consideredPeople + person,
          discoveredPeople.tail ++ network(person))
      }
    }
    bfs(b, Set(), network(a) + a)
  }