如何使用FP在Scala中实现广度优先搜索

时间:2016-12-27 14:52:44

标签: scala functional-programming breadth-first-search

我想知道如何使用功能编程在Scala中实现Breadth-first search

这是我的第一个不纯的代码:

  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
  }

虽然我只使用本地可变性(var和可变Queue),但它并不是纯粹的功能。

我想出了另一个版本:

  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)

两种解决方案都有更好的方法吗? 是否可以使用cats / scalaz来移除一些样板?

4 个答案:

答案 0 :(得分:11)

函数式编程的一个好处是你可以利用懒惰将数据结构的遍历与搜索部分分开。这使得可重复使用的单一责任代码成为可能:

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))
}

现在你可以通过breadth_first_traverse(root, f) find (_ == 16)执行BFS,或者使用Stream类中的任何其他功能在懒惰的广度优先Stream上进行有用的临时“查询”树。

答案 1 :(得分:3)

基于Karl Bielefeldt给出的答案,这是另一个解决方案(不涉及任何队列,只使用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)
}

答案 2 :(得分:1)

这是未经测试的,但我觉得有效:

  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)
  }

诀窍是保留一个剩余要检查的Seq,并且,如果当前元素不匹配,请使用我们必须检查的附加节点的子节点的剩余部分来调用自己

答案 3 :(得分:0)

广度优先搜索不可避免地取决于正在搜索的数据类型。根据维基百科的说法,经典的解决方案包括跟踪已经搜索过的内容,这样您就不会陷入无限循环。

Scala 强加了一个附加要求,即迭代的主要工具是递归函数,最好是尾递归函数。

因此,这里是使用上述方法的解决方案。

首先有一个 Map,其中人名作为字符串,值是一组其他字符串,表示与第一人有联系的其他人。

因此,如果“Fred”认识“Mary”,而“Mary”又认识“John”,那么您会期望“Mary”出现在“Fred”的名字列表中,而“John”出现在“Mary”的列表中。

考虑到这一点,这里是一个经过全面测试的实现(由 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)
  }