【问题标题】:Optimizing mutable array state heavy manipulation code优化可变数组状态重操作代码
【发布时间】:2016-02-14 14:02:55
【问题描述】:

我一直在努力及时完成关于hackerrank 的this 练习。 但是由于超时,我的以下 Haskell 解决方案在测试用例 13 到 15 上失败了。

我的 Haskell 解决方案

import           Data.Vector(Vector(..),fromList,(!),(//),toList)
import           Data.Vector.Mutable
import qualified Data.Vector as V 
import           Data.ByteString.Lazy.Char8 (ByteString(..))
import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Lazy.Builder
import Data.Maybe
import Control.Applicative
import Data.Monoid
import Prelude hiding (length)

readInt' = fst . fromJust . L.readInt 
toB []     = mempty
toB (x:xs) = string8 (show x) <> string8 " " <> toB xs

main = do 
  [firstLine, secondLine] <- L.lines <$> L.getContents
  let [n,k] = map readInt' $ L.words firstLine
  let xs = largestPermutation n k $ fromList $ map readInt' $ Prelude.take n $ L.words secondLine
  L.putStrLn $ toLazyByteString $ toB $ toList xs


largestPermutation n k v
  | i >= l || k == 0 = v 
  | n == x           = largestPermutation (n-1) k v
  | otherwise        = largestPermutation (n-1) (k-1) (replaceOne n x (i+1) (V.modify (\v' -> write v' i n) v))
        where l = V.length v 
              i = l - n
              x = v!i

replaceOne n x i v
  | n == h = V.modify (\v' -> write v' i x ) v
  | otherwise = replaceOne n x (i+1) v
    where h = v!i 

我发现的最佳解决方案会不断更新 2 个数组。一个数组作为主要目标,另一个数组用于快速索引查找。

更好的 Java 解决方案

public static void main(String[] args) {
  Scanner input = new Scanner(System.in);
  int n = input.nextInt();
  int k = input.nextInt();
  int[] a = new int[n];
  int[] index = new int[n + 1];
  for (int i = 0; i < n; i++) {
      a[i] = input.nextInt();
      index[a[i]] = i;
  }
  for (int i = 0; i < n && k > 0; i++) {
      if (a[i] == n - i) {
          continue;
      }
      a[index[n - i]] = a[i];
      index[a[i]] = index[n - i];
      a[i] = n - i;
      index[n - i] = i;
      k--; 
  }
  for (int i = 0; i < n; i++) {
      System.out.print(a[i] + " ");
  }
}

我的问题是

  1. 这个算法在 Haskell 中的优雅和快速实现是什么?
  2. 有没有比 Java 解决方案更快的方法来解决这个问题?
  3. 我应该如何在 Haskell 中优雅而高效地处理繁重的数组更新?

【问题讨论】:

  • 请输入签名...
  • 只是想知道您的 Java 解决方案是否通过了现场所有时间的平板测试?
  • @leftaroundabout 抱歉。缺少类型签名是否会降低可读性?我认为更少的代码 = 更具可读性,我很傻。
  • 是的,但类型签名的作用至少与代码一样多。
  • 没有类型签名真的很难阅读代码。

标签: arrays algorithm haskell mutable


【解决方案1】:

您可以对可变数组进行的一项优化是根本不使用它们。特别是,您链接到的问题有一个右折叠解决方案。

这个想法是你折叠列表并贪婪地将具有最大值的项目向右交换并保持已经在Data.Map中进行的交换:

import qualified Data.Map as M
import Data.Map (empty, insert)

solve :: Int -> Int -> [Int] -> [Int]
solve n k xs = foldr go (\_ _ _ -> []) xs n empty k
    where
    go x run i m k
        -- out of budget to do a swap or no swap necessary
        | k == 0 || y == i = y : run (pred i) m k
        -- make a swap and record the swap made in the map
        | otherwise        = i : run (pred i) (insert i y m) (k - 1)
        where
        -- find the value current position is swapped with
        y = find x
        find k = case M.lookup k m of
            Just a  -> find a
            Nothing -> k

在上面,run 是一个函数,它给出了反向索引 i、当前映射m 和剩余的交换预算k,解决了列表的其余部分。 反向索引我的意思是反向的列表索引:n, n - 1, ..., 1。

折叠函数go,通过更新传递到下一步的i、m和k的值,在每一步构建run函数。最后,我们使用初始参数i = n、m = empty 和初始交换预算k 调用此函数。

find 中的递归搜索可以通过维护反向映射来优化,但这已经比您发布的 java 代码快得多。


编辑:以上解决方案,仍然为树访问支付对数成本。这是使用可变 STUArray 和 monadic fold foldM_ 的替代解决方案,它实际上比上面的执行速度更快:

import Control.Monad.ST (ST)
import Control.Monad (foldM_)
import Data.Array.Unboxed (UArray, elems, listArray, array)
import Data.Array.ST (STUArray, readArray, writeArray, runSTUArray, thaw)

-- first 3 args are the scope, which will be curried
swap :: STUArray s Int Int -> STUArray s Int Int -> Int
     -> Int -> Int -> ST s Int
swap   _   _ _ 0 _ = return 0  -- out of budget to make a swap
swap arr rev n k i = do
    xi <- readArray arr i
    if xi + i == n + 1
    then return k -- no swap necessary
    else do -- make a swap, and reduce budget
        j <- readArray rev (n + 1 - i)
        writeArray rev xi j
        writeArray arr j  xi
        writeArray arr i (n + 1 - i)
        return $ pred k

solve :: Int -> Int -> [Int] -> [Int]
solve n k xs = elems $ runSTUArray $ do
    arr <- thaw (listArray (1, n) xs :: UArray Int Int)
    rev <- thaw (array (1, n) (zip xs [1..]) :: UArray Int Int)
    foldM_ (swap arr rev n) k [1..n]
    return arr

【讨论】:

  • 哇,与其他 java 解决方案相比,它几乎是即时的。然而,foldr go (const []) (zip [n,n - 1..] xs) (empty, k) 部分让我有点失望。我应该如何尝试理解那部分?你能添加更多括号@behzad.nouri 吗?
  • @DulguunOtgon this page 或 this one 应该会有所帮助。你可以推foldr 向右倾斜让它再次回到左边:)。该页面解释了这是如何完成的(通过柯里化折叠功能)。
  • @DulguunOtgon 使用可变 STUArrays 使用替代解决方案编辑了答案
  • 我必须在哪些方面熟练才能编写和理解类似于第一个版本的内容?
  • @DulguunOtgon 我在 stackoverflow 上遇到了 foldr 技巧。如果您对书籍、博客和问答网站有足够的了解,那么随着时间的推移,您就会掌握这些技巧。
【解决方案2】:

不完全是 #2 的答案,但有一个左折叠解决方案,需要一次在内存中加载最多 ~K 个值​​。

由于问题涉及排列,我们知道 1 到 N 将出现在输出中。如果 K > 0,至少前 K 项将是 N, N-1, ... N - K,因为我们至少可以负担 K 次交换。此外,我们预计一些 (K/N) 位将处于最佳位置。

这建议了一个算法:

初始化地图/字典并将输入xs扫描为zip xs [n, n-1..]。对于每个(x, i),如果x \= i,我们“递减”K 并更新字典 s.t. dct[i] = x。当K == 0 (out of swaps) 或我们用完输入 (can output {N, N-1, ... 1}) 时,此过程终止。

接下来,如果我们还有更多x &lt;- xs,我们会查看每一个,如果x不在我们的字典中,则打印x,否则打印dct[x]。

只有当我们的字典包含一个循环时,上述算法才能产生最佳排列。在这种情况下,我们使用 |cycle| 交换移动绝对值 >= K 的元素。但这意味着我们将一个元素移动到其原始位置!所以我们总是可以在每个周期保存一个交换(即递增K)。

最后,这给出了内存高效算法。

第0步:得到N,K

步骤1:读取输入排列并输出{N, N-1, ... N-K-E}, N

其中 E = 元素数 X 等于 N -(X 的索引)

第 2 步:从 dict 中删除并计算循环;让cycles = 周期数;如果cycles &gt; 0,让K &lt;- |cycles|,转到步骤1,

否则转到第 3 步。我们可以通过优化 dict 来提高这一步的效率。

第 3 步:按原样输出其余输入。

下面的 Python 代码实现了这个想法,如果使用更好的循环检测,可以做得相当快。当然,数据最好分块读取,不像下面那样。

from collections import deque

n, t = map(int, raw_input().split())

xs = deque(map(int, raw_input().split()))

dct = {}

cycles = True
while cycles:
    while t > 0 and xs:
        x = xs.popleft()
        if x != n:
            dct[n] = x
            t -= 1
        print n,
        n -= 1

    cycles = False
    for k, v in dct.items():
        visited = set()
        cycle = False
        while v in dct:
            if v in visited:
                cycle = True
                break
            visited.add(v)
            v, buf = dct[v], v
            dct[buf] = v
        if cycle:
            cycles = True
            for i in visited:
                del dct[i]
            t += 1
        else:
            dct[k] = v

while xs:
    x = xs.popleft()
    print dct.get(x, x),

【讨论】:

    猜你喜欢
    • 2013-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-25
    • 2017-04-19
    • 2011-02-28
    • 2014-06-14
    • 1970-01-01
    相关资源
    最近更新 更多