【问题标题】:Scala: dynamic programming recursion using iteratorsScala:使用迭代器的动态编程递归
【发布时间】:2018-11-01 17:49:28
【问题描述】:

学习如何在 Scala 中进行动态编程,我经常发现自己处于一种需要递归处理项目数组(或其他可迭代项)的情况。当我这样做时,我倾向于编写这样的繁琐函数:

def arraySum(array: Array[Int], index: Int, accumulator: Int): Int => {
  if (index == array.length) {
    accumulator
  } else {
    arraySum(array, index + 1, accumulator + array(index)
  }
}
arraySum(Array(1,2,3), 0, 0)

(暂时忽略我可以在数组上调用sum 或调用.reduce(_ + _),我正在尝试学习编程原理。)

但这似乎是我传递了很多变量,而将数组传递给每个函数调用究竟有什么意义?这似乎不干净。

所以我想到了用迭代器来做这件事,而不用担心传递索引:

def arraySum(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
  try {
    val nextInt = iter.next()
    arraySum(iter)(accumulator + nextInt)
  } catch {
    case nee: NoSuchElementException => accumulator
  }
}
arraySum(Array(1,2,3).toIterator)

这似乎是一个更清洁的解决方案。但是,当您需要使用动态编程来探索一些结果空间并且您不需要在每次函数调用时调用迭代器时,这就会崩溃。例如

def explore(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
  if (someCase) {
    explore(iter)(accumulator)
  } else if (someOtherCase){
    val nextInt = iter.next()
    explore(iter)(accumulator + nextInt)
  } else {
    // Some kind of aggregation/selection of explore results
  }
}

我的理解是 iter 迭代器在这里的作用是通过引用传递,所以当这个函数调用 iter.next() 时,它会更改传递给函数的所有其他递归调用的 iter 实例。所以为了解决这个问题,现在我在每次调用explore 函数时都克隆迭代器。例如:

def explore(iter: Iterator[Int])(implicit accumulator: Int = 0): Int = {
  if (someCase) {
    explore(iter)(accumulator)
  } else if (someOtherCase){
    val iterClone = iter.toList.toIterator
    explore(iterClone)(accumulator + iterClone.next())
  } else {
    // Some kind of aggregation/selection of explore results
  }
}

但这似乎很愚蠢,当我有多个迭代器可能需要或可能不需要在多个 else if 情况下进行克隆时,愚蠢会升级。处理这种情况的正确方法是什么?如何优雅地解决这些问题?

【问题讨论】:

  • 我不确定您为什么要谈论“动态编程”。看起来您在普通回溯方面遇到了问题。嗯,是的,可变结构的回溯很烦人,因为您要么必须恢复突变结构的状态,要么创建克隆......如果您需要回溯,更喜欢不可变的参数,有什么问题?列表对此特别有用,因为它们是不可变的,并且有点像它们自己的迭代器。不太清楚问题是什么。
  • 应该mySumarraySum 吗?通常在处理递归时会进行头/尾拆分。强制索引传递似乎你在这里强行使用了错误的方法。
  • @Andrey 如果我使用列表或其他不可变结构,那么我必须传递累加器和索引,对吗?我觉得没有索引会有一个更优雅的解决方案。我引用“动态编程”是因为当程序探索结果空间时,上述迭代器解决方案变得不优雅(即需要大量克隆)。
  • @Logister 我会说坚持头/尾可拆分列表,非常熟悉这样做,他们更普遍地应用它。我从来没有发现递归迭代一个可变/昂贵的复制结构是好的。在处理可变性时,坚持迭代方法几乎总是更干净、更容易。只是我的2美分。即使在 Clojure 中,我也会在处理可变 Java 结构时使用 doseq
  • 致癌,安德烈,我刚刚查找了列表的头部/尾部方法......我觉得自己像个白痴。如果你们中的任何一个人想写下一个答案,说明我应该研究这些方法,我会将其标记为答案并给你加分。

标签: scala recursion functional-programming dynamic-programming backtracking


【解决方案1】:

假设您要编写一个回溯递归函数,该函数需要一些复杂的数据结构作为参数,以便递归调用接收数据结构的略微修改版本。您有多种选择:

  1. 克隆整个数据结构,修改它,传递给递归调用。这非常简单,但通常非常昂贵。
  2. 就地修改可变结构,将其传递给递归调用,然后在回溯时还原修改。您必须确保递归函数的每个可能调用始终准确地恢复数据结构的原始状态。这种方法效率更高,但很难实施,因为它很容易出错。
  3. 将结构细分为一个大的不可变部分和一个小的可变部分。例如,您可以传递一个索引(或一对索引),显式指定数组的某个切片,以及一个永不变异的数组。然后,您可以“克隆”并仅保存可变部分,并在回溯时恢复它。如果它有效,它既简单又快速,但它并不总是有效,因为子结构很难用几个整数索引来描述。
  4. 尽可能依赖持久的不可变数据结构。

我想详细说明最后一点,因为这是在 Scala 和一般函数式编程中执行此操作的首选方式。

这是您使用第三种策略的原始代码:

def arraySum(array: Array[Int], index: Int, accumulator: Int): Int = {
  if (index == array.length) {
    accumulator
  } else {
    arraySum(array, index + 1, accumulator + array(index))
  }
}

如果您要使用List 而不是Array,您可以将其重写为:

@annotation.tailrec
def listSum(list: List[Int], acc: Int): Int = list match {
  case Nil => acc
  case h :: t => listSum(t, acc + h)
}

这里,h :: t 是将列表解构为headtail 的模式。 请注意,您不再需要显式索引,因为访问列表的尾部t 是一个常量时间操作,因此只有相关的剩余子列表被传递给listSum 的递归调用。

这里没有回溯,但是如果递归方法会回溯,使用列表会带来另一个好处:提取子列表几乎是免费的(恒时操作),但它仍然保证不可变,所以你可以通过它进入递归调用,而不必关心递归调用是否修改它,因此您不必做任何事情来撤消递归调用可能已经完成的任何修改。这是持久不可变数据结构的优势:相关列表可以共享它们的大部分结构,同时从外部看起来仍然是不可变的,因此不可能仅仅因为您可以访问该列表的尾部而破坏父列表中的任何内容.对于可变数组的视图,情况并非如此。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-22
    • 2011-11-09
    • 2017-10-10
    • 2021-10-05
    • 1970-01-01
    • 2013-06-19
    • 2020-05-19
    • 2016-09-22
    相关资源
    最近更新 更多