使用 Haskell 查找 LCS 的性能问题

如何解决使用 Haskell 查找 LCS 的性能问题

这是一个经典的编程问题https://en.wikipedia.org/wiki/Longest_common_subsequence_problem

JS 实现通过了所有测试,但 Haskell 实现了消耗过多内存并被杀死。

我做错了什么?

-- TOP TO BottOM
commonChild s1 s2 = L.foldl g l1 l ! n
    where
    n  = length s1
    l1 = arr $ replicate (n + 1) 0
    l  = [ [(x,i,y,j) | (y,j) <- zip s2 [1..]] 
           | (x,i) <- zip s1 [1..]]
    g a = L.foldl (\a' (x,j) -> let x' = if x == y
                                             then 1 + a ! (j - 1) 
                                             else max (a ! j) (a' ! (j - 1)) 
                                    in a' // [(j,x')]) 
                  l1

arr l = array (0,length l-1) $ zip [0..] l
function lcs(a,b) {
  let n = a.length
  let a1 = []
  for (let i = 0; i <= n; i++) {
    a1.push(0)
  }
  for (let i = 0; i < b.length; i++) {
    let a2 = [0]
    for (let j = 0; j < n; j++) {
      let x = b[i] == a[j] ? 1 + a1[j] : Math.max(a1[j+1],a2[j])
      a2.push(x)
    }
    a1 = a2
  }
  return a1[n]
}

console.log(lcs("SHINCHAN","NOHaraAA"))

https://repl.it/@leonbobster/LCS#main.hs

https://www.hackerrank.com/challenges/common-child/problem

解决方法

您对 // 中的 Data.Array 的使用真的会影响您的表现。如果您阅读 the docs,它会说“构造一个与第一个参数相同的数组,只是它已被正确参数中的关联更新”,这意味着每次调用它时,您都在构造一个全新的阵列。这与您的 js 实现非常不同,后者只是附加。

您可能认为数组是获得性能提升的明显选择,但这是常规旧列表可以正常工作的时候之一。与其在折叠的每次迭代中生成一个新数组,每个数组都比前一个元素多一个新元素,不如直接将其 cons 到一个列表中。考虑子函数 g 的以下定义:

g a = arr . reverse . L.foldl (inner a) [0]
inner a a'@(z:_) (x,i,y,j) =
  let x' = if x == y
            then 1 + a ! (j - 1)
            else max (a ! j) z
  in x':a'

注意:我上面所做的更改都是关于选择更好的数据结构,但请参阅@chi 的答案以了解更多提高性能的方法,这些方法与协商惰性/严格性和执行特定于 GHC 的工作有关东西。

,

我稍微修改了你的代码

  • 添加类型签名
  • 使用 foldl'
  • 使用 bang 模式来强制严格
  • 使用 -O2 编译(避免 GHCi)

这是修改后的代码(删除了长测试字符串):

{-# LANGUAGE BangPatterns #-}
{-# OPTIONS -Wall -O2 #-}
module Main where


import qualified Data.List as L 
import Data.Array

commonChild :: Eq a => [a] -> [a] -> Int
commonChild s1 s2 = L.foldl' g l1 l ! n
    where
        n = length s1
        l1 = arr $ replicate (n + 1) 0
        l = [[(x,j) | (y,j) <- zip s2 [1..]] | (x,i) <- zip s1 [(1::Int)..]]
        g a = L.foldl' (\ !a' (!x,!_i,!y,!j) -> let 
           ! x' = if x == y
               then 1 + a ! (j - 1) 
               else max (a ! j) (a' ! (j - 1)) in a' // [(j,x')]) l1 
 
arr :: [e] -> Array Int e
arr l = array (0,length l-1) $ zip [0..] l

s1test :: String
s1test = "UBBJXJGKLXGXTFBJ..." -- omitted

s2test :: String
s2test = "WZFPTGLCXK..." -- omitted

main :: IO ()
main = do
    print $ commonChild "SHINCHAN" "NOHARAAA"
    print $ commonChild s1test s2test

以上代码运行时使用的 RAM 不足 6MB,并在 3 分 10 秒内完成打印 31417 作为输出。

相比之下,当我终止时,原始代码使用了 12GB 以上的 RAM。

应该还有更多的改进空间。 Data.Array 中的数组可能很慢,因为每次数组更新都必须重新创建一个新数组。当命令式算法不能轻易转换成一个好的函数式算法时,也许最好暂时接受命令式的一面并开始使用 STUArray 及其相关函数,编写一些代码来精确地模仿您发布的命令式算法.使用runST,您仍然可以实现纯函数式接口并公开类似的类型

commonChild ::
   ( Eq a,forall s. MArray (STUArray s) a (ST s)    -- requires some extension
   ) => [a] -> [a] -> Int

(或者干脆放弃多态而使用 String -> String -> Int)。

,

阅读维基百科对该算法的描述,让我非常直接地实现了仅使用列表的实现;没有数组:

{-# LANGUAGE BangPatterns #-}

-- Calculate the next row from the character along the
-- left edge,the string along the top edge,and the previous
-- row.
makeRow :: Char -> String -> [Int] -> [Int]
makeRow match = go 0 0
  where
    -- The first arguments are the values in the arguments
    -- to the upper left and immediate left of the current
    -- cell.
    go :: Int -> Int -> Char -> String -> [Int] -> [Int]
    go !up_left !left (c:cs) (l:ls) =
      cur : go l cur cs ls
      where
        !cur
          | c == match = 1 + up_left
          | otherwise = max l left
    go _ _ _ _ = []

commonChild s1 = go (repeat (0 :: Int))
  where
    go ls [] = last ls
    go ls (c:cs) = go (makeRow c s1 ls) cs

这足够快,可以通过所有测试,而且比处理数组要简单得多。可以通过多种方式改进恒定因素,但这是一个很好的起点。我尝试改进的第一种方法是将 [Int] 到处都替换为类似于

data IntList = Cons !Int IntList | Nil

这为每个元素节省了两个字的内存和一个指针间接。在许多情况下,切换到未装箱数组(至少对于 Int 列表)应该会带来进一步的改进,但会更加烦人。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?