使用函数式编程解决动态编程问题

时间:2017-07-23 18:08:26

标签: scala functional-programming dynamic-programming purely-functional

在获得基本想法之后,以命令式方式编写动态编程(DP)问题非常简单,至少对于更简单的DP问题。它通常涉及某种形式的表格,我们根据某些公式迭代填充。这几乎就是实现自下而上的DP。

让我们将Longest Increasing Subsequence(LIS)作为一个简单而常见的问题示例,可以通过自下而上的DP算法解决。

C ++实现很简单(未经测试):

// Where A is input (std::vector<int> of size n)
std::vector<int> DP(n, 0);
for(int i = 0; i < n; i++) {
    DP[i] = 1;
    for(int j = 0; j < i; j++) {
        if (A[i] > A[j]) {
            DP[i] = std::max(DP[i], DP[j] + 1);
        }
    }
}

我们刚刚描述了“如何填充DP”,这正是命令式编程所在。

如果我们决定描述“DP是什么”,这是一种更加考虑它的FP方式,它会变得有点复杂。 以下是此问题的示例Scala代码:

// Where A is input
val DP = A.zipWithIndex.foldLeft(Seq[Int]()) {
    case (_, (_, 0)) => Seq(1)
    case (d, (a, _)) =>
      d :+ d.zipWithIndex.map { case (dj, j) =>
        dj + (if (a > A(j)) 1 else 0)
      }.max
}

说实话,这个Scala实现似乎并不那么惯用。它只是命令式解决方案的翻译,增加了不变性。

我很好奇,处理这类事情的一般FP方法是什么?

2 个答案:

答案 0 :(得分:1)

我对DP知之甚少,但我希望有一些观察结果可以促成对话。

首先,您的示例Scala代码似乎不能解决LIS问题。我插入了Wikipedia page上找到的Van der Corput序列,但没有得到指定的结果。

解决问题这是我提出的解决方案。

def lis(a: Seq[Int], acc: Seq[Int] = Seq.empty[Int]): Int =
  if      (a.isEmpty)         acc.length
  else if (acc.isEmpty)       lis(a.tail, Seq(a.head))   max lis(a.tail)
  else if (a.head > acc.last) lis(a.tail, acc :+ a.head) max lis(a.tail, acc)
  else                        lis(a.tail, acc)

lis(Seq(0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15)) // res0: Int = 6

这可以调整为返回子序列本身,并且我确定可以调整它以获得更好的性能。

至于备忘录,根据需要自行推出并不难。这是记忆任何arity-2函数的基本概要。

def memo[A,B,R](f: (A,B)=>R): ((A,B)=>R) = {
  val cache = new collection.mutable.WeakHashMap[(A,B),R]
  (a:A,b:B) => cache.getOrElseUpdate((a,b),f(a,b))
}

有了这个,我可以创建一个通常称为方法/功能的备忘版本。

val myfunc = memo{(s:String, i:Int) => s.length > i}
myfunc("bsxxlakjs",7)  // res0: Boolean = true

注意:以前建议使用WeakHashMap,以便缓存可以在内存受到挑战的环境中删除较少使用的元素。我不知道是否仍然如此。

答案 1 :(得分:1)