什么是备忘录有用,它真的有用吗?

Noc*_*wer 29 generics performance memoization demo

互联网上有一些自动记忆库可用于各种不同的语言; 但不知道它们的用途,使用方法以及它们的工作原理,很难看出它们的价值.使用memoization有什么令人信服的论据,以及memoization特别闪耀的问题域是什么?这里特别感谢不知情的信息.

det*_*tly 22

在我看来,Fibonacci和阶乘计算并不是最好的例子.当你拥有以下内容时,备忘录真正发挥作用:

  1. 有关计算的大量潜在输入,但范围仍然明显受到限制和已知
  2. 您事先知道程序的任何实际使用只会使用一小部分可能的输入来计算(Fibonacci和factorial会失败)
  3. 您不知道在运行时将使用哪些特定输入,因此需要记住哪些特定结果(Fibonacci和factorial也失败了,直到某一点)

显然,如果您确实知道所有可能的输入,并且空间允许,您可以考虑用查找替换该函数(这是我用的,例如,使用已知生成器的嵌入式CRC32实现).

...甚至比#2更好的是,如果对于程序的任何特定运行,您可以立即说"潜在输入的范围将被限制为满足这些条件的子集......".

请注意,其中很多可能是概率性的(或直观的) - 当然,有人可能会尝试所有10 ^ 13种可能的输入来进行魔法计算,但是你知道它们实际上不会.如果他们这样做,记忆的开销实际上对他们没有好处.但你可能会认为这是可以接受的,或者允许在这种情况下绕过备忘录.


这是一个例子,我希望它不是太复杂(或概括)来提供信息.

在我写的一些固件中,程序的一部分从ADC读取,可以是来自的任何数字0x000,0xFFF并计算程序的其他部分的输出.此计算还采用一组用户可调数字,但这些数字仅在程序启动时读取.这个计算在它第一次运行时非常受欢迎.

提前创建查找表是荒谬的.输入域是[ 0x000,...,0xFFF]和(大范围的浮点值)和(另一个大范围......)的笛卡尔积和... ...不,谢谢.

但是当条件变化很快时,没有用户要求或期望设备能够正常工作,并且当事情稳定时,他们愿意更好地工作.因此,我在反映这些要求的计算行为中进行权衡:我希望这个计算在事情稳定时很好而且快速,而我不关心它们何时不是.

给定典型用户期望的"缓慢变化的条件"的定义,该ADC值将稳定到特定值并保持在其稳定值的约0x010内.哪个值取决于条件.

因此,可以为这16个潜在输入记忆计算结果.如果环境条件改变速度快于预期,"最远" ADC从最近被丢弃读取(例如,如果我缓存0x210到0x21F,然后我读0x222,我把0x210结果).

这里的缺点是,如果环境条件变化很大,那么已经慢的计算运行速度会慢一些.我们已经确定这是一个不寻常的用例,但如果有人后来发现实际上,他们确实希望在异常不稳定的条件下操作它,我可以实现绕过备忘录的方法.

  • 使用最近最少使用的缓存可以做得更好.它与现在的情况大致相同,只不过16可以是16. (3认同)
  • 我现在也添加了一个例子. (2认同)
  • @DeadMG - 据推测那需要动态分配内存?我倾向于避免嵌入式系统.:) (2认同)

Gma*_*ell 19

这里流行的阶乘答案是一个玩具答案.是的,memoization对于重复调用该函数很有用,但这种关系是微不足道的 - 在"print factorial(N)for 0..M"的情况下,你只是重用最后一个值.

这里的许多其他示例都只是"缓存".这很有用,但它忽略了memoization这个词给我带来的令人敬畏的算法含义.

更有趣的是,递归函数的单个调用的不同分支遇到相同的子问题,但是以非平凡的模式实际索引到某个缓存实际上是有用的.

例如,考虑整数的n维数组,其绝对值总和为k.例如,对于n = 3,k = 5 [1,-4,0],[3,-1,1],[5,0,0],[0,5,0]将是一些示例.设V(n,k)是给定n,k的可能唯一数组的数量.它的定义是:

V(n,0)=1; V(0,k)=0; V(n,k) = V(n-1,k) + V(n,k-1) + V(n-1,k-1);

对于n = 3,k = 5,该函数给出102.

如果没有记忆,即使是相当适中的数字,这也很快就会变得很慢.如果将处理可视化为树,则每个节点调用V()扩展为三个子节点,你有186,268,135,991,213,676,920,832 V(n,0)= 1叶子计算V(32,32)...天真实现此功能在可用硬件上迅速变得无法计算.

但是树中的许多子分支都是彼此完全相同的,尽管不是像平凡函数那样容易被消除的一些微不足道的方式.通过memoization,我们可以合并所有这些重复的分支.事实上,使用memoization V(32,32)只执行V()1024(n*m)次,这是10 ^ 21倍的加速(随着n,k变大,显然会变大)或者更换对于相当少量的内存.:)我发现这种对算法复杂性的根本改变远比简单缓存更令人兴奋.它可以使棘手的问题变得容易.

因为python数字自然是bignums你可以在python中实现这个公式,使用字典和元组键只有9行的memoization.试一试,没有备忘录就试一试.

  • 呃,很抱歉没有更清楚地区分.当我开始写作时,斐波纳契的答案没有解决,你实际上并没有以某种方式尝试解释你的网格示例.否则我可能不会回应.:)虽然在我看来,#1投票回答的数字仍然不是最有趣或最准确的.阶乘真的是一个玩具,我认为我证明了这个说法.我对"缓存"和记忆化之间区别的观点是复杂性的变化 - 你用"荒谬"到"易处理"的区别.干杯. (3认同)
  • 你说"这里的许多其他算法都只是缓存",然后"更有趣的是......"然后你继续描述......缓存?然后你在调用其他答案'玩具'(包括使用斐波那契的那些)之后描述一个类似于斐波纳契(或我的网格问题)的示例序列.很高兴你接受了你的回答,但我个人认为你没有带来任何新的东西.:) (2认同)

Ste*_*hen 13

Memoization是存储子问题的答案的技术,因此程序不需要在以后重新解决相同的子问题.

使用动态规划解决问题通常是一项重要的技术.

想象一下,枚举从网格左上角到网格右下角的所有路径.很多路径彼此重叠.您可以记住为网格上的每个点计算的解决方案,从右下角,从右上角开始构建.这使计算时间从"荒谬"变为"易处理".

另一个用途是:列出数字0到100的阶乘.你不想计算100!使用100 * 99 * ... * 1.你已经计算出99!,所以重用答案,并简单地乘以100倍的答案99!.您可以在每个步骤(从1到100)中记住答案,以节省大量的计算量.

对于数据点,我的网格解决问题(问题来自编程挑战):

Memoized:

real  0m3.128s
user  0m1.120s
sys   0m0.064s
Run Code Online (Sandbox Code Playgroud)

非记忆(我杀了,因为我厌倦了等待......所以这是不完整的)

real 24m6.513s
user 23m52.478s
sys   0m6.040s
Run Code Online (Sandbox Code Playgroud)


Jas*_*yon 11

Memoization在可以重用子问题的解决方案的问题上闪耀.简单来说,它是一种缓存形式.我们以阶乘函数为例.

3!它本身就是一个问题,但它也是n的子问题!其中n> 3,例如4! = 4 * 3! 计算阶乘的函数可以通过memoization执行得更好,因为它只会计算3!一次并将结果存储在哈希表中.每当遇到3时!再次,它将在表中查找值而不是重新计算它.

子问题解决方案可以重复使用的任何问题(越频繁越好)是使用memoization的候选者.

  • @Jonathan:10岁怎么样??50!? (7认同)
  • @Jonathan:当然,3!更快地重新计算.是20!`?是'100!`?是'200!`?我随时都会把'O(1)`. (7认同)
  • @Jonathan:你有这个疯狂的想法,我们提倡备忘作为所有问题的解决方案.使用bignum,不再有整数溢出!严肃点.这些都是小问题.我(我们)认识到记忆的限制,并且知道我们不能无限地增长缓存......但是感谢你指出它. (2认同)
  • @Jonathan Allen - 不使用由醉酒狒狒编写的记忆实施来减轻这种危险,这种狒狒具有限制存储数据量等先进功能. (2认同)

Dre*_*all 7

记忆交换空间的时间.

当应用于本质上是多递归的问题时,记忆可以将指数时间(或更差)转变为线性时间(或更好).成本通常是O(n)空间.

经典的例子是计算Fibonacci序列.教科书定义是递归关系:

F(n)= F(n-1)+ F(n-2)

天真地实现,它看起来像这样:

int fib(int n) {
  if (n == 0) {
    return 0;
  }
  else if (n == 1) {
    return 1;
  }
  else {
    return fib(n-1) + fib(n-2);
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以看到运行时随n呈指数增长,因为每个部分和都是多次计算的.

通过memoization实现,它看起来像这样(笨拙但功能齐全):

int fib(int n) {
  static bool initialized = false;
  static std::vector<int> memo;

  if (!initialized) {
    memo.push_back(0);
    memo.push_back(1);
    initialized = true;
  }

  if (memo.size() > n) {
    return memo[n];
  }
  else {
    const int val = fib(n-1) + fib(n-2);
    memo.push_back(val);
    return val;
  }
}
Run Code Online (Sandbox Code Playgroud)

在我的笔记本电脑上定时这两个实现,对于n = 42,天真版本需要6.5秒.memoized版本需要0.005秒(所有系统时间 - 也就是说,它的I/O限制).对于n = 50,memoized版本仍然需要0.005秒,并且naive版本最终在5分7秒之后完成(更不用说它们都溢出了32位整数).

  • @Jonathan:这是一个简单的例子.对于生产代码,你会去BigInts并添加一个包含n> 0的一次性检查的包装器,但除此之外我没有看到问题.显然你不会长时间使用天真版本(我想你很快会发现自己的记忆).减轻... (2认同)