在识别指数运行时间时,有一些模式。例如,如果在数组中的每个元素处,指针可以进行一步,两步或三步,我们在进行memoization之前会看一个O(3 ^ N)数组,因为每个元素有三个函数调用。
然而,我对于在记忆后识别运行时间背后的模式有点困惑。一般来说,这是什么关系?我知道memoizing正在做什么 - 只是摆脱重复的子句,但在面试设置中我不想草拟出一棵树并划掉所有重复的子句来直观运行时。有什么想法吗?
编辑:
例如,下面的问题暴力是O(3 ^ N),并且在记忆之后是O(n ^ 3)并且我不确定如何直觉或如果存在我缺少的基础模式。< / p>
一只青蛙正在过河。这条河分为x个单位和 每个单位可能存在也可能不存在石头。青蛙可以跳上一个 石头,但它不能跳入水中。
给出一个按升序排序的石头位置列表(单位) 命令,确定青蛙是否能够通过登陆过河 最后一块石头。最初,青蛙在第一块石头上并假设 第一次跳跃必须是1个单位。
如果青蛙的最后一次跳跃是k个单位,那么它的下一个跳跃必须是 k - 1,k或k + 1个单位。请注意,青蛙只能跳进去 前进方向
方法#1暴力[超出时间限制]
In the brute force approach, we make use of a recursive function canCrosscanCross which takes the given stone array, the current position and the current jumpsize as input arguments. We start with currentPosition=0 and jumpsize=0. Then for every function call, we start from the currentPosition and check if there lies a stone at (currentPostion + newjumpsize), where, the newjumpsize could be jumpsize, jumpsize+1 or jumpsize-1. In order to check whether a stone exists at the specified positions, we check the elements of the array in a linear manner. If a stone exists at any of these positions, we call the recursive function again with the same stone array, the currentPosition and the newjumpsize as the parameters. If we are able to reach the end of the stone array through any of these calls, we return true to indicate the possibility of reaching the end.
Java
public class Solution {
public boolean canCross(int[] stones) {
return can_Cross(stones, 0, 0);
}
public boolean can_Cross(int[] stones, int ind, int jumpsize) {
for (int i = ind + 1; i < stones.length; i++) {
int gap = stones[i] - stones[ind];
if (gap >= jumpsize - 1 && gap <= jumpsize + 1) {
if (can_Cross(stones, i, gap)) {
return true;
}
}
}
return ind == stones.length - 1;
}
}
Complexity Analysis
Time complexity : O(3^n)
Recursion tree can grow upto 3^n
Space complexity : O(n). Recursion of depth n is used.
记忆后:
public class Solution {
public boolean canCross(int[] stones) {
int[][] memo = new int[stones.length][stones.length];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
return can_Cross(stones, 0, 0, memo) == 1;
}
public int can_Cross(int[] stones, int ind, int jumpsize, int[][] memo) {
if (memo[ind][jumpsize] >= 0) {
return memo[ind][jumpsize];
}
for (int i = ind + 1; i < stones.length; i++) {
int gap = stones[i] - stones[ind];
if (gap >= jumpsize - 1 && gap <= jumpsize + 1) {
if (can_Cross(stones, i, gap, memo) == 1) {
memo[ind][gap] = 1;
return 1;
}
}
}
memo[ind][jumpsize] = (ind == stones.length - 1) ? 1 : 0;
return memo[ind][jumpsize];
}
}
复杂性分析
Time complexity : O(n^3)
Memorization will reduce time complexity to O(n^3).
Space complexity : O(n^2)
memo matrix of size n^2 is used.
答案 0 :(得分:2)
所以,它有用我想如果你举一个具体的例子,所以我确切地知道你在说什么。但是,我想我可以猜到。 :)
从渐近分析,记忆化和可证实的#34;如果它意味着您可以对最终发生的子查询数量设置更小的限制,则会有所帮助。
经典例子:Fibonacci数字。
假设你有一个像这样的天真实现:
int fib(int n) {
if (n < 2) {
return 1;
} else {
return fib(n-2) + fib(n-1);
}
}
每次调用发生时,都会生成两个子调,并且参数仅减少一个常量。所以,你应该能够证明运行时是2^{O(n)}
。
(更多细节:当我呼叫fib(n)
时,在每个分支处选择一个子分支,直到它达到最低点为止,至少会发生n/2
个分支。所以&#39;至少2^{n/2}
。实际上,&#34;通常&#34;,意思是,如果我随机选择其中一个分支,n
平均变小1.5。所以有更多像{ {1}}子句。并且,不超过2^{2n/3}
。)
记忆时会发生什么?
这意味着你创建了一个长度为2^n
的缓冲区,并在那里缓存任何子调用的结果。 n
每次都需要在进行计算之前检查缓存。
关键是,现在不是让fib
子函数有效,而是最多有2^n
个子函数可以工作。我们创建一个不会立即从缓存中提取结果的子查询的次数最多是...缓存的大小。
与动态编程类似。当您为大型旧表分配然后递归计算值时,运行时间基本上就是表的大小。
所以,在虚拟案例中,你应该得到指数级的提升。运行时间与n
相似,而不是n
。
怎么正式说?假设我有这样的(假的)代码:
2^n
std::vector<boost::optional<int>> cache;
int fib(int n) {
if (n < 2) {
return 1;
} else if (cache[n]) {
return *cache[n];
} else {
cache[n] = fib(n-2) + fib(n-1);
return *cache[n];
}
}
需要多长时间才能运行?
第一点:第三个分支,其中值尚未缓存,只能为fib(n)
的每个值发生一次。 (因为在任何后续运行中,它都将被缓存。)
如下:对于任何m&lt; n,n
最多被调用两次,一次来自fib(m)
子句(计算fib(n-2)
时),一次来自fib(m+2)
子句({{1}时)正在计算中)。这两个案例是唯一可以直接调用fib(n-1)
的案例,而且每个案例只执行一次。
因此,对fib的调用总数为fib(m+1)
。我们执行的添加次数为fib(m)
,因为我们必须为每个要填充的缓存成员执行一次添加。
因此,与2 * n
次加成费用相比,运行时间将是n
次加法费用,加上n
次表查询费用。
我想在面试环境中,您认为添加和表查找是单位成本操作。如果你真的想拥有可以为任意大整数计算n
的代码,你就不能使用2^n
,你需要使用大整数,大小将是关于fib
位。然后,我认为加法就像int
。因此,我认为你最终会以log n
渐渐地结束。