【问题标题】:Slow recursion even with memoization in R即使在 R 中进行记忆,递归也很慢
【发布时间】:2020-12-28 12:17:37
【问题描述】:

我正在尝试解决问题#14 of Project Euler。 所以主要目标是找到 Collat​​z 序列的长度。

首先我解决了常规循环的问题:

compute <- function(n) {
    result <- 0
    max_chain <- 0
    hashmap <- 1
    for (i in 1:n) {
        chain <- 1
        number <- i
        while (number > 1) {
            if (!is.na(hashmap[number])) {
                chain <- chain + hashmap[number]
                break
            }
            if (number %% 2 == 0) {
                chain <- chain + 1
                number <- number / 2
            } else {
                chain <- chain + 2
                number <- (3 * number + 1) / 2
            }
        }
        hashmap[i] <- chain
        if (chain > max_chain) {
            max_chain <- chain
            result <- i
        }
    }
    return(result)
}

n = 1000000 只需 2 秒。 我决定将while循环替换为递归

len_collatz_chain <- function(n, hashmap) {
    get_len <- function(n) {
        if (is.na(hashmap[n])) {
            hashmap[n] <<- ifelse(n %% 2 == 0, 1 + get_len(n / 2), 2 + get_len((3 * n + 1) / 2))
        }
        return(hashmap[n])
    }
    get_len(n)
    return(hashmap)
}

compute <- function(n) {
    result <- 0
    max_chain <- 0
    hashmap <- 1
    for (i in 1:n) {
        hashmap <- len_collatz_chain(i, hashmap)
        print(length(hashmap))
        if (hashmap[i] > max_chain) {
            max_chain <- hashmap[i]
            result <- i
        }
    }
    return(result)
}

此解决方案有效,但运行速度很慢。 n = 10000 差不多 1 ​​分钟。 我想原因之一是R每次调用函数len_collatz_chain时都会创建hashmap对象。

我知道 Rcpp 包,是的,第一个解决方案工作正常,但我不明白我错在哪里。 有什么建议吗?

例如,我的 Python 递归解决方案在 1 秒内使用 n = 1000000

def len_collatz_chain(n: int, hashmap: dict) -> int:
    if n not in hashmap:
        hashmap[n] = 1 + len_collatz_chain(n // 2, hashmap) if n % 2 == 0 else 2 + len_collatz_chain((3 * n + 1) // 2, hashmap)
    return hashmap[n]

def compute(n: int) -> int:
    result, max_chain, hashmap = 0, 0, {1: 1}
    for i in range(2, n):
        chain = len_collatz_chain(i, hashmap)
        if chain > max_chain:
            result, max_chain = i, chain
    return result

【问题讨论】:

  • 我相信这是 R 中的所有开销堆栈问题。如果你想坚持使用 R,我敢打赌你使用你声称的 Rcpp
  • 或者你可以使用 Reticulate 包在 R 中运行你的 python 代码

标签: r recursion memoization collatz


【解决方案1】:

R 代码和 Python 代码之间的主要区别在于,在 R 中,您使用向量作为哈希图,而在 Python 中,您使用字典,并且哈希图作为函数参数多次传输。

在 Python 中,如果您将 Dictionary 作为函数参数,则只有对实际数据的 引用 才会传输到被调用函数。这很快。被调用函数与调用者处理相同的数据。

在 R 中,向量在用作函数参数时被复制。这可能会很慢,但在被调用函数无法更改调用者数据的意义上更安全。

这是 Python 在您的代码中速度如此之快的主要原因。

但是,您可以稍微更改 R 代码,以便不再将 hashmap 作为函数参数传输:

len_collatz_chain <- local({
  
  hashmap <- 1L
  
  get_len <- function(n) {
    if (is.na(hashmap[n])) {
      hashmap[n] <<- ifelse(n %% 2 == 0, 1 + get_len(n / 2), 2 + get_len((3 * n + 1) / 2))
    }
    hashmap[n]
  }
  
  get_len
})


compute <- function(n) {
  result <- rep(NA_integer_, n)
  
  for (i in seq_len(n)) {
    result[i] <- len_collatz_chain(i)
  }
  result
}

compute(n=10000)

这使得 R 代码更快。 (不过 Python 可能会更快)。

请注意,我还删除了 R 代码中的 return 语句,因为它们不是必需的,并在调用堆栈中添加了一层。

【讨论】:

  • 如果我是对的,在您的解决方案中,hashmap 对象将在 for 循环中每次创建。但是这样记忆就不起作用了。
  • 不,你错了。 hashmap 对象只会在我的解决方案中创建一次,甚至会在不同的compute() 调用之间持续存在。进行测试并拨打compute(n=10000) 两次。第二次答案是即时的,因为hashmap 甚至在其关闭的调用中仍然存在。所以记忆是有效的。
  • 如果你仔细看我的代码,你会发现len_collatz_chain 是函数get_len 加上它的闭包。它返回函数get_len。所以每次调用len_collatz_chain(),实际调用的函数就是get_len()hashmap 对象仅在 len_collatz_chain 初始化时创建一次,但不是在每次调用 get_len 时创建,因此不是在 for 循环的每次迭代中! :)
猜你喜欢
  • 2014-03-23
  • 2012-09-20
  • 1970-01-01
  • 1970-01-01
  • 2013-01-07
  • 2012-11-12
  • 2012-09-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多