赞
踩
递归函数非递归化
by Sachin Malhotra
由Sachin Malhotra
In order to understand recursion, you must first understand recursion.
为了了解递归,您必须首先了解递归。
Crazy, isn’t it ?
疯了,不是吗?
Well, I hope that by the end of this article you will feel much more confident about what recursion is and mainly, how we can come up with a recursive solution to a problem.
好吧,我希望到本文结尾时,您对什么是递归(主要是如何递归解决问题)感到更有信心。
How do you explain recursion to a 4 year old? This is a pretty famous interview question, and there are loads of answers available on the web. We won’t answer this question as it is too mainstream.
您如何解释递归到4岁的孩子? 这是一个非常著名的面试问题,网上有很多答案。 我们不会回答这个问题,因为它太主流了。
If you are as clever as I am ??, you would explain recursion to someone one year younger than you. Have them explain recursion to someone one year younger than them. Continue until you have a 5 year old explaining recursion to a 4 year old. Done. [Source: reddit].
如果您像我一样聪明,可以向比您小一岁的人解释递归。 让他们向比他们小一岁的人解释递归。 继续直到您有5岁的孩子解释递归到4岁的孩子。 做完了 [来源:re ddit]。
In programming terms, recursion is
用编程的术语来说,递归是
A function calling itself.
调用自身的函数。
The above function does no useful work as such, but it does demonstrate recursion. The recursive relation above would be
上面的函数本身没有做任何有用的工作,但确实演示了递归。 上面的递归关系是
T(N) = T(N - 1) + O(1)
This simply means that the execution for the call to random_function(n)
cannot proceed until the call to random_function(n-1)
is completed and so on.
这仅表示在对random_function(n-1)
的调用完成之前,无法继续执行对random_function(n)
的调用。
Essentially, we delay the execution of the current state of the function until another call to the same function has completed and returned it’s result.
本质上,我们将函数当前状态的执行延迟到对同一函数的另一次调用完成并返回其结果为止。
The compiler keeps on saving the state of the function call now and then moves onto the next function call and so on. So, the compiler saves function states onto a stack and uses that for computations and backtracking.
编译器现在继续保存函数调用的状态,然后移至下一个函数调用,依此类推。 因此,编译器将函数状态保存到堆栈中,并将其用于计算和回溯。
Essentially, if a problem can be broken down into similar subproblems which can be solved individually, and whose solutions can be combined together to get the overall solution, then we say that there might exist a recursive solution to the problem.
本质上,如果一个问题可以分解为类似的子问题,可以单独解决,并且可以将其解决方案组合在一起以获得整体解决方案,那么我们说可能存在该问题的递归解决方案。
Instead of clinging to this seemingly old definition of recursion, we will look at a whole bunch of applications of recursion. Then hopefully things will be clear.
我们将不关注递归的这个看似古老的定义,而是研究递归的一整套应用程序。 然后希望一切都会好起来的。
Let us see how we can find out the factorial of a number. Before that, let’s see what the factorial of a number represents and how it is calculated.
让我们看看如何找出数字的阶乘。 在此之前,让我们看看数字的阶乘代表什么以及如何计算。
factorial(N) = 1 * 2 * 3 * .... * N - 1 * N
Simply put, the factorial of a number is just the product of terms from 1 to the number N multiplied by one another.
简而言之,一个数的阶乘只是从1到N的项相乘的乘积。
We can simply have a for
loop from 1 to N and multiply all the terms iteratively and we will have the factorial of the given number.
我们可以简单地使用一个从1到N的for
循环,然后迭代所有项,然后得到给定数字的阶乘。
But, if you look closely, there exists an inherent recursive structure to the factorial of a number.
但是,如果仔细观察,数字的阶乘存在固有的递归结构。
factorial(N) = N * factorial(N - 1)
It’s like offloading the computation to another function call operating on a smaller version of the original problem. Let’s see how this relation would unfold to verify if the solution here matches the one provided by the for
loop.
这就像将计算转移到另一个函数调用上一样,该函数在原始问题的较小版本上运行。 让我们看看这种关系将如何展开以验证此处的解决方案是否与for
循环提供的解决方案匹配。
So, it is clear from the two figures above that the recursive function that we defined earlier,
因此,从以上两个图清楚可见,我们之前定义的递归函数,
factorial(N) = N * factorial(N - 1)
is indeed correct. Have a look at the Python code snippet used to find the factorial of a function, recursively.
的确是正确的。 递归查看一下用于查找函数阶乘的Python代码片段。
This example was pretty simple. Let us consider a slightly bigger but standard example to demonstrate the concept of recursion.
这个例子很简单。 让我们考虑一个更大但标准的示例来演示递归的概念。
You must be already familiar with the famous fibonacci sequence. For those of you who have’t heard about this sequence or seen an example before, lets have a look.
您必须已经熟悉著名的斐波那契数列。 对于那些以前从未听说过此序列或没有看过示例的人,让我们看看。
1 1 2 3 5 8 13 .....
Let us look at the formula for calculating the n^th fibonacci number.
让我们看一下计算第n个斐波纳契数的公式。
F(n) = F(n - 1) + F(n - 2)where F(1) = F(2) = 1
Clearly, this definition of the fibonacci sequence is recursive in nature, since the n^th fibonacci number is dependent upon the previous two fibonacci numbers. This means dividing the problem into smaller subproblems, and hence recursion. Have a look at the code for this:
显然,由于第n个斐波那契数取决于前两个斐波那契数,因此斐波那契序列的定义本质上是递归的。 这意味着将问题分为较小的子问题,从而将其递归。 看一下代码:
Every recursive problem must have two necessary things:
每个递归问题都必须具有两个必要的条件:
The recursion tree shows us that the results obtained from processing the two subtrees of the root N can be used to compute the result for the tree rooted at N. Similarly for other nodes.
递归树向我们显示,通过处理根N的两个子树获得的结果可用于计算以N为根的树的结果。对于其他节点也是如此。
The leaves of this recursion tree would be fibonacci(1)
or fibonacci(2)
both of which represent the base cases for this recursion.
此递归树的叶子将是fibonacci(1)
或fibonacci(2)
,它们都代表此递归的基本情况。
Now that we have a very basic grasp of recursion, what a recurrence relation is, and the recursion tree, let’s move onto something more interesting.
既然我们对递归有了一个非常基本的了解,那么递归关系是什么,以及递归树,让我们继续进行一些有趣的事情。
Examples!
例子!
I strongly believe in solving umpteen number of examples for any given topic in programming to become a master of that topic. The two examples we considered (Factorial of a number and the Fibonacci sequence) had well defined recurrence relations. Let us look at a few examples where the recurrence relation might not be so obvious.
我坚信要为编程中的任何给定主题解决无数个示例,以使其成为该主题的精通者。 我们考虑的两个示例(数的比例和斐波那契数列)具有明确的递归关系。 让我们看几个例子,其中递归关系可能不太明显。
To keep things simple for this example, we will only consider a binary tree. So, a binary tree is a tree data structure in which each node has at most two children. One node of the tree is designated as the root of the tree, for example:
为了使此示例简单,我们将仅考虑二叉树。 因此,二叉树是一种树数据结构,其中每个节点最多具有两个子节点。 树的一个节点被指定为树的根,例如:
Let’s define what we mean by the height of the binary tree.
让我们定义二叉树高度的含义。
Height of the tree would be the length of the longest root to leaf path in the tree.
树的高度将是树中最长的根到叶路径的长度。
So, for the example diagram displayed above, considering that the node labelled as A
as the root of the tree, the longest root to leaf path is A → C → E → G → I
. Essentially, the height of this tree is 5
if we count the number of nodes and 4
if we just count the number of edges on the longest path.
因此,对于上面显示的示例图,假设标记为A
的节点为树的根,则根到叶路径的最长路径为A → C → E → G → I
。 本质上,如果我们计算节点数,那么这棵树的高度是5
,如果我们只计算最长路径上的边数,则树的高度是4
。
Now, forget about the entire tree and just focus on the portions highlighted in the diagram below.
现在,忘记整个树,只关注下图中突出显示的部分。
The above figure shows us that we can represent a tree in the form of its subtrees. Essentially, the structure to the left of node A and the structure to the right of A is also a binary tree in itself, just smaller and with different root nodes. But, they are binary trees nonetheless.
上图显示了我们可以用子树的形式表示树。 本质上,节点A左侧的结构和A右侧的结构本身也是一棵二叉树,只是较小,并且具有不同的根节点。 但是,它们仍然是二叉树。
What information can we get from these two subtrees that would help us find the height of the main tree rooted at A ?
我们可以从这两个子树中获得什么信息,这将有助于我们找到以A为根的主树的高度?
If we knew the height of the left subtree, say h1
, and the height of the right subtree, say h2
, then we can simply say that the maximum of the two + 1
for the node A would give us the height of our tree. Isn’t that right?
如果我们知道左子树的高度(例如h1
)和右子树的高度(例如h2
,那么我们可以简单地说节点A maximum of the two + 1
的maximum of the two + 1
将给我们树的高度。 是不是?
Formalizing this recursive relation,
正式建立这种递归关系,
height(root) = max(height(root.left), height(root.right)) + 1
So, that’s the recursive definition of the height of a binary tree. The focus is on binary here, because we used just two children of the node root
represented by root.left
and root.right.
But, it is easy to extend this recursive relation to an n-ary tree. Let’s take a look at this in code.
因此,这就是二叉树高度的递归定义。 这里的重点是二进制文件,因为我们仅使用了以root.left
和root.right.
表示的节点root
两个子节点root.right.
但是,很容易将此递归关系扩展到n元树。 让我们在代码中看一下。
The problem here was greatly simplified because we let recursion do all the heavy lifting for us. We simply used optimal answers for our subproblems to find a solution to our original problem.
因为我们让递归为我们完成了所有繁重的工作,所以这里的问题被大大简化了。 我们只是使用最优 我们的子问题的答案,可以找到我们原来的问题的解决方案。
Let’s look at another example that can be solved on similar lines.
让我们看另一个可以在相似的行上解决的示例。
Here again, we will consider a binary tree for simplicity, but the algorithm and the approach can be extended to any kind of tree essentially.
再次在这里,为简单起见,我们将考虑使用二叉树,但是该算法和方法本质上可以扩展到任何种类的树。
The problem is itself very self explanatory. Given the root of a binary tree, we need to determine the total number of nodes in the tree. This question and the approach we will come up with here are very similar to the previous one. We just have to make minuscule changes and we will have the number of nodes in the binary tree.
问题本身是非常自我解释的。 给定二叉树的根,我们需要确定树中节点的总数。 这个问题和我们将在此处提出的方法与上一个非常相似。 我们只需要进行微小的更改,我们将拥有二叉树中的节点数。
Take a look at the diagram below.
看一下下图。
The diagram says it all. We already know that a tree can be broken down into smaller subtrees. Here again, we can ask ourselves,
该图说明了一切。 我们已经知道一棵树可以分解为较小的子树。 再一次,我们可以问自己,
What information can we get from these two subtrees that would help us find the number of nodes in the tree rooted at A?
我们可以从这两个子树中获得什么信息,这将有助于我们找到以A为根的树中的节点数?
Well, if we knew the number of nodes in the left subtree and the number of nodes in the right subtree, we can simply add them up and add one for the root node and that would give us the total number of nodes.
好吧,如果我们知道左子树中的节点数和右子树中的节点数,我们可以简单地将它们加起来并为根节点添加一个,这样就可以得出节点总数。
Formalizing this we get,
正式化我们得到的,
number_of_nodes(root) = number_of_nodes(root.left) + number_of_nodes(right) + 1
If you look at this recursion and the previous one, you will find that they are extremely similar. The only thing that is varying is what we do with the information we obtained from our subproblems and how we combined them to get some answer.
如果查看此递归和上一个递归,您会发现它们非常相似。 唯一变化的是我们如何处理从子问题中获得的信息,以及如何将它们组合起来以获得答案。
Now that we have seen a couple of easy examples with a binary tree, let’s move onto something less trivial.
现在,我们已经看到了一些带有二叉树的简单示例,让我们继续进行一些比较简单的事情。
Given an array of numbers like
给定像这样的数字数组
4 2 8 9 1 5 2
we need to come up with a sorting technique that sorts them either in ascending or descending order. There are a lot of famous sorting techniques out there for this like Quick Sort, Heap Sort, Radix Sort and so on. But we are specifically going to look at a technique called the Merge Sort.
我们需要提出一种排序技术,以升序或降序对其进行排序。 那里有很多著名的排序技术,例如快速排序 , 堆排序 , 基数排序等等。 但是,我们将专门研究一种称为“合并排序”的技术。
It’s possible that a lot of you are familiar with the Divide and Conquer paradigm, and this might feel redundant. But bear with me and read on!
你们中的许多人可能都熟悉分而治之范式 ,这可能会觉得多余。 但是,请忍受我,继续读下去!
The idea here is to break it down into subproblems.
这里的想法是将其分解为子问题。
That’s what the article is about right ? ?
那就是这篇文章是对的吗? ?
What if we had two sorted halves of the original array. Can we use them somehow to sort the entire array?
如果我们将原始数组分为两半怎么办。 我们可以使用它们以某种方式对整个数组进行排序吗?
That’s the main idea here. The task of sorting an array can be broken down into two smaller subtasks:
这是这里的主要思想。 排序数组的任务可以分为两个较小的子任务:
Now, the beauty about recursion is, you don’t need to worry about how we will get two sorted halves and what logic will go into that. Since this is recursion, the same method call to merge_sort
would sort the two halves for us. All we need to do is focus on what we need to do once we have the sorted haves with us.
现在,递归的好处在于,您不必担心我们将如何得到两个半部分,以及将采用什么逻辑。 由于这是递归,因此对merge_sort
的相同方法调用将为我们分两半。 我们需要做的就是把精力集中在一旦拥有分类好的食物之后就要做的事情。
Let’s go through the code:
让我们看一下代码:
At this point, we trusted and relied on our good friend recursion and assumed that left_sorted_half
and right_sorted_half
would in fact contain the two sorted halves of the original array.
在这一点上,我们信任并依靠我们的好朋友递归,并假设left_sorted_half
和right_sorted_half
实际上将包含原始数组的两个已排序的一半。
So, what next?
那么,接下来呢?
The question is how to combine them somehow to give the entire array.
问题是如何以某种方式将它们组合在一起以提供整个阵列。
The problem now simply boils down to merging two sorted arrays into one. This is a pretty standard problem and can be solved by what is known as the “two finger approach”.
现在,问题简单地归结为将两个排序的数组合并为一个。 这是一个非常标准的问题,可以通过所谓的“两指方法”解决。
Take a look at the pseudo code for better understanding.
请看一下伪代码以更好地理解。
let L and R be our two sorted halves. let ans be the combined, sorted array
l = 0 // The pointer for the left halfr = 0 // The pointer for the right halfa = 0 // The pointer for the array ans
while l < L.length and r < R.length { if L[l] < R[r] { ans[a] = L[l] l++ } else { ans[a] = R[r] r++ }}
copy remaining array portion of L or R, whichever was longer, into ans.
Here we have two pointers (fingers), and we position them at the start of the individual halves. We check which one is smaller (that is, which value pointed at by the finger is smaller), and we add that value to our sorted combined array. We then advance the respective pointer (finger) forward. In the end we copy the remaining portion of the longer array and add it to the back of the ans
array.
在这里,我们有两个指针(手指),我们将它们放置在各半部分的开头。 我们检查哪一个较小(即,手指指向的哪个值较小),然后将该值添加到排序后的组合数组中。 然后,我们将各个指针(手指)向前移动。 最后,我们复制较长数组的其余部分,并将其添加到ans
数组的后面。
So, the combined code for merge-sort is as follows:
因此,合并排序的组合代码如下:
We will do one final question using recursion and trust me, it’s a tough one and a pretty confusing one. But before moving onto that, I will iterate the steps I follow whenever I have to think of a recursive solution to a problem.
我们将使用递归来做最后一个问题,请相信我,这是一个艰难而又令人困惑的问题。 但是在继续之前,每当我不得不考虑递归解决问题时,我都会迭代我遵循的步骤。
2. Once you have the subproblems figured out, think about what information from the call to the subproblems can you use to solve the task at hand. For example, the factorial of N — 1
to find the factorial of N
, height of the left and right subtrees to find the height of the main tree, and so on.
2.确定了子问题后,请考虑可以使用从调用到子问题的哪些信息来解决手头的任务。 例如, N — 1
的阶乘可找到N
的阶乘,左和右子树的高度可找到主树的高度,依此类推。
3. Keep calm and trust recursion! Assume that your recursive calls to the subproblems will return the information you need in the most optimal fashion.
3.保持冷静并信任递归! 假定您对子问题的递归调用将以最佳方式返回您所需的信息。
4. The final step in this process is actually using information we just got from the subproblems to find the solution to the main problem. Once you have that, you’re ready to code up your recursive solution.
4.此过程的最后一步实际上是使用我们从子问题中获得的信息来找到主要问题的解决方案。 一旦有了这些,就可以编写递归解决方案了。
Now that we have all the steps lined up, let’s move on to our final problem in this article. It’s called Sum of Distances in a Tree.
现在我们已经排好了所有步骤,让我们继续本文中的最后一个问题。 称为树中的距离总和。
Let’s look at what the question is asking us to do here. Consider the following tree.
让我们看看这个问题在要求我们在这里做什么。 考虑下面的树。
In the example above, the sum of paths for the node A (the number of nodes on each path from A
to every other vertex in the tree) is 9. The individual paths are mentioned in the diagram itself with their respective lengths.
在上面的示例中,节点A的路径总和(从A
到树中每个其他顶点的每条路径上的节点数)为9。图中的各个路径及其长度也被提及。
Similarly, consider the sum of distances for the node C.
类似地,考虑节点C的距离之和。
C --> A --> B (Length 2)C --> A (Length 1)C --> D (Length 1)C --> E (Length 1)C --> D --> F (Length 2)Sum of distances (C) = 2 + 1 + 1 + 1 + 2 = 7
This is known as the sum of distances as defined for just a single node A or C. We need to calculate these distances for each of the nodes in the tree.
这就是仅针对单个节点A或C定义的距离之和。我们需要为树中的每个节点计算这些距离。
Before actually solving this generic problem, let us consider a simplified version of the same problem. It says that we just need to calculate the sum of distances for a given node, but we will only consider the tree rooted at the given node for calculations.
在实际解决此通用问题之前,让我们考虑一下同一问题的简化版本。 它说的是,我们只需要计算给定节点的距离之和,但是我们仅考虑以给定节点为根的树以进行计算。
So, for the node C, this simplified version of the problem would ask us to calculate:
因此,对于节点C,此问题的简化版本将要求我们计算:
C --> D (Length 1)C --> E (Length 1)C --> D --> F (Length 2)Simplified Sum of Distances (C) = 1 + 1 + 2 = 4
This is a much simpler problem to tackle recursively and would prove to be useful in solving the original problem.
这是一个更简单的递归解决问题,将证明对解决原始问题很有用。
Consider the following simple tree.
考虑下面的简单树。
The nodes B and C are the children of the root (that is, A).
节点B和C是根节点(即A)的子节点。
We are trying to see what information can we use from subproblems (the children) to compute the answer for the root A
.
我们试图查看可以从子问题(子问题)中使用哪些信息来计算根A
的答案。
Note: here we simply want to calculate the sum of paths for a given node X to all its successors in its own subtree (the tree rooted at the node X).
注意 :这里我们只想计算给定节点X到其自身子树(根于节点X的树)中所有后继路径的路径总和。
There are no downwards going paths from the node B, and so the sum of paths is 0 for the node B
in this tree. Let’s look at the node C
. So this node has 3 different successors in F, D and E
. The sum of distances are as follows:
从节点B开始没有向下的路径,因此该树中节点B
的路径总和为0。 让我们看一下节点C
因此,该节点在F, D and E
具有3个不同的后继者。 距离的总和如下:
C --> D (Path containing just 1 edge, hence sum of distances = 1)C --> D --> F (Path containing 2 edges, hence sum of distances = 2)C --> E (Path containing just 1 edge, hence sum of distances = 1)
The sum of all the paths from the node C
to all of its decedents is 4, and number of such paths going down is 3.
从节点C
到其所有后代的所有路径的总和为4,此类路径断开的数量为3。
Note the difference here. The sum_of_distances
here counts the number of edges in each path — with each edge repeating multiple times, probably because of their occurrence on different paths — unlike number_of_paths
, which counts, well, the number of paths ?.
注意这里的区别。 这里的sum_of_distances
计算每条路径中的边数-每条边重复多次(可能是因为它们出现在不同的路径上)-与number_of_paths
不同,后者很好地计算了路径数?。
If you look closely, you will realize that the number of paths going down is always going to be the number of nodes in the tree we are considering (except the root). So, for the tree rooted at C, we have 3 paths, one for the node D, one for E, and one for F. This means that the number of paths from a given node to the successor nodes is simply the total number of descendent nodes since this is a tree. So, no cycles or multiple edges.
如果仔细观察,您会发现下降的路径数始终是我们正在考虑的树中的节点数(根除外)。 因此,对于以C为根的树,我们有3条路径,一条为节点D,一条为E,一条为F。这意味着从给定节点到后继节点的路径数仅是C的总数。后代节点,因为这是一棵树。 因此,没有周期或多个边缘。
Now, consider the node A. Let us look at all the new paths that are being introduced because of this node A. Forget the node B for now and just focus on the child node C corresponding to A. The new sets of paths that we have are:
现在,考虑节点A。让我们看一下由于该节点A而引入的所有新路径。暂时忘记节点B,仅关注与A对应的子节点C。有:
A --> C (Path containing just 1 edge, hence sum of distances = 1)A --> (C --> D) (Path containing 2 edges, hence sum of distances = 2)A --> (C --> E) (Path containing 2 edges, hence sum of distances = 2)A --> (C --> D --> F) (Path containing 3 edges, hence sum of distances = 3)
Except for the first path A → C
, all the others are the same as the ones for the node C, except that we have simply changed all of them and incorporated one extra node A
.
除了第一条路径A → C
,其他所有路径都与节点C的路径相同,只是我们简单地更改了所有路径并合并了一个额外的节点A
If you look at the diagram above you will see a tuple of values next to each of the nodes A, B, and C.
如果查看上面的图,您将在节点A,B和C的每一个旁边看到一个元组值。
(X, Y) where X is the number of paths originating at that node and going down to the decedents. Y is the sum of distances for the tree rooted at the given node.
Since the node B doesn’t have any further children, the only path it is contributing to is the path A -->
; B to
A's tuple of (5, 9) above. So let’s talk about C.
由于节点B没有其他子节点,因此它贡献的唯一路径是路径A -->
; B to
A的(5,9)元组。 因此,让我们谈谈C。
C had three paths going to its successors. Those three paths (extended by one more node for A) also become three paths from A to its successors, among others.
C有三条通往其后继者的道路。 这三个路径(为A增加了一个节点)也变成了从A到其后继者的三个路径。
N-Paths[A] = (N-Paths[C] + 1) + (N-Paths[B] + 1)
That is the exact relation we are looking for as far as the number of paths (= number of successor nodes in the tree) are concerned. The 1 is because of the new path from the root to it’s child, that is A -->
; C in our case.
就路径数(=树中的后继节点数)而言,这就是我们正在寻找的确切关系。 1是因为从根到它的孩子的新路径,即A -->
; 在我们的情况下为C。
N-Paths[A] = 3 + 1 + 0 + 1 = 5
As far as the sum of distances is concerned, take a look at the diagram and the equations we just wrote. The following formula becomes very clear:
就距离总和而言,请看一下我们刚刚编写的图表和方程式。 以下公式变得非常清楚:
Sum-Dist[A] = (N-Paths[C] + 1 + Sum-Dist[C]) + (N-Paths[B] + 1 + Sum-Dist[B])
Sum-Dist[A] = (3 + 1 + 4 + 0 + 1 + 0) = 9
The main thing here is N-Paths[C] + Sum-Dist[C]
. We sum these up because all of the paths from C to its descendants ultimately become the paths from A to its descendants — except that they originate at A and go through C, and so each of the path lengths are increased by 1. There are N-Paths[C]
paths in all originating from C and their total length is given by Sum-Dist[C]
.
这里最主要的是N-Paths[C] + Sum-Dist[C]
。 我们总结一下,因为所有从C到其后代的路径最终都变为从A到其后代的路径-除了它们起源于A并经过C,因此每个路径长度都增加了1。有N-Paths[C]
所有源自C的N-Paths[C]
路径及其总长度由Sum-Dist[C]
。
Hence the tuple corresponding to A = (5, 9). The Python code for the algorithm we discussed above is as follows:
因此,对应于A =(5,9)的元组。 我们上面讨论的算法的Python代码如下:
If you look at the code above closely, you’ll see this:
如果您仔细查看上面的代码,将会看到以下内容:
# Prevents the recursion from going into a cycle. self.visited[vertex] = 1
The comment says that this visited
dictionary is for preventing the recursion from entering a cycle.
评论说,此visited
词典用于防止递归进入循环。
If you’ve paid attention til now, you know that we are dealing with a tree
here.
如果您一直关注到现在,您就会知道我们正在处理一tree
。
The definition of a tree data structure doesn’t allow cycles to exist. If a cycle exists in the structure, then it is no longer a tree, it becomes a graph. In a tree, there is exactly one path between any two pair of vertices. A cycle would mean there is more than one path between a pair of vertices. Look at the figures below.
树数据结构的定义不允许循环存在。 如果结构中存在循环,则它不再是树,而是图。 在树中,任意两对顶点之间只有一条路径。 一个循环意味着一对顶点之间的路径不止一个。 看下面的数字。
The structure on the left is a tree. It has no cycles in it. There is a unique path between any two vertices.
左侧的结构是一棵树。 它没有循环。 任何两个顶点之间都有一条唯一的路径。
The structure on the right is a graph, there exists a cycle in the graph and hence there are multiple paths between any pair of vertices. For this graph, it so happens that any pair of vertices have more than one path. This is not necessary for every graph.
右边的结构是一个图,图中存在一个循环,因此任何一对顶点之间都有多条路径。 对于此图,碰巧任何一对顶点都有多个路径。 并非每个图形都必需。
Almost always, we are given the root
node of the tree. We can use the root node to traverse the entire tree without having to worry about any cycles as such.
几乎总是给我们树的root
。 我们可以使用根节点遍历整个树, 而不必担心这样的循环。
However, if you’ve read the problem statement clearly, it does not state anything about root of the tree.
但是,如果您已经清楚地阅读了问题说明,那么它并没有说明有关树的根的任何内容。
That means that there is no designated root for the tree given in the question. This could mean that a given tree can be visualized and processed in so many different ways depending upon what we consider as the root. Have a look at multiple structures for the same tree but with different root nodes.
这意味着问题中给出的树没有指定的根。 这可能意味着可以根据我们认为的根源,以多种不同方式可视化和处理给定的树。 查看同一棵树但具有不同根节点的多个结构。
So many different interpretations and parent child relationships are possible for a given unrooted tree.
对于给定的无根树,可能有许多不同的解释和父子关系。
So, we start with the node 0
and do a DFS traversal of the given structure. In the process we fix the parent child relationships. Given the edges in the problem, we construct an undirected graph-like structure which we convert to the tree structure. Taking a look at the code should clear up some of your doubts:
因此,我们从节点0
开始,对给定的结构进行DFS遍历。 在此过程中,我们修复了父子关系。 给定问题的边缘,我们构造了无向图样结构,然后将其转换为树形结构。 看一下代码应该可以消除您的一些疑问:
Every node would have one parent. The root won’t have any parent, and the way this logic is, the node 0
would become the root of our tree. Note that we are not doing this process separately and then calculating the sum of distances downwards
. Given a tree, we were trying to find, for every node, the simplified sum of distances for the tree rooted at that node.
每个节点都有一个父节点。 根将没有任何父节点,按照这种逻辑,节点0
将成为我们树的根。 请注意,我们不会单独进行此过程,而是sum of distances downwards
计算sum of distances downwards
之sum of distances downwards
。 给定一棵树,我们试图为每个节点找到根于该节点的树的距离的简化总和。
So, the conversion from the graph to the tree happens in one single iteration along with finding out the sum of distances downwards for each and every node.
因此,从图到树的转换是在一次迭代中进行的,同时找出每个节点向下的距离总和。
I posted the code again so that the visited
dictionary makes much more sense now. So, one single recursion doing all that for us. Nice!
我再次发布了代码,以便现在visited
字典更有意义。 因此,一次递归可以为我们做所有的事情。 真好!
Now that we have our tree structure defined, and also the values of sum of distances going downward
defined for us, we can use all of this information to solve the original problem of Sum of Distances in a Tree.
现在,我们已经定义了树结构,并且还为我们定义了sum of distances going downward
和的值,我们可以使用所有这些信息来解决树中距离之和的原始问题。
How do we do that? It’s best to explain this algorithm with the help of an example. So we will consider the tree below and we will dry run the algorithm for a single node. Let’s have a look at the tree we will be considering.
我们该怎么做? 最好在一个示例的帮助下解释该算法。 因此,我们将考虑下面的树,并对单个节点进行空运行算法。 让我们看看我们将要考虑的树。
The node for which we want to find the sum of distances is 4
. Now, if you remember the simpler problem we were trying to solve earlier, you know that we already have two values associated with each of the nodes:
我们要查找距离之和的节点是4
。 现在,如果您还记得我们之前试图解决的更简单的问题,您就会知道我们已经有两个与每个节点关联的值:
distances_down
Which is the sum of distances for this node while only considering the tree beneath.
distances_down
是仅考虑下面的树时此节点的距离之和。
number_of_paths_down
which is the number of paths / nodes in the tree rooted at the node under consideration.
number_of_paths_down
,这是从所考虑的节点为根的树中的路径/节点的数量。
Let’s look at the annotated version of the above tree. The tree is annotated with tuples (distances_down, number_of_paths_down)
.
让我们看一下上面树的带注释的版本。 该树用元组(distances_down, number_of_paths_down)
注释。
Let’s call the value we want to compute for each node as sod
which means sum of distances, which is what the question originally asks us to compute.
我们将要为每个节点计算的值称为sod
,这意味着距离之和,这就是问题最初要求我们计算的值。
Let us assume that we have already computed the answer for the parent node of 4
in the diagram above. So, we now have the following information for the node labelled 2
(the parent node) available:
让我们假设我们已经在上图中计算了4
的父节点的答案。 因此,我们现在可以获得以下标记为2
的节点(父节点)的信息:
(sod, distances_down, number_of_paths_down)
= (13, 4, 3)
(sod, distances_down, number_of_paths_down)
= (13, 4, 3)
(sod, distances_down, number_of_paths_down)
(13, 4, 3)
Let’s rotate the given tree and visualize it in a way where 2
is the root of the tree essentially.
让我们旋转给定的树并将其可视化,其中2
实质上是树的根。
Now, we want to remove the contribution of the tree rooted at 4
from sod(2)
. Let us consider all of the paths from the parent node 2
to all other nodes except the ones in the tree rooted at 4
.
现在,我们要从sod(2)
删除以4
为根的树的贡献。 让我们考虑从父节点2
到除以4
为根的树中的其他节点以外的所有其他节点的所有路径。
2 --> 5 (1 edge)2 --> 1 (1 edge)2 --> 1 -->7 (2 edges)2 --> 1 --> 7 --> 9 (3 edges)2 --> 1 --> 7 --> 10 (3 edges)
Number of nodes considered = 6Sum of paths remaining i.e. sod(2) rem = 1 + 1 + 2 + 3 + 3 = 10
Let’s see how we can use the values we already have calculated to get these updated values.
让我们看看如何使用已经计算出的值来获取这些更新的值。
* N = 8 (Total number of nodes in the tree. This will remain the same for every node. )* sod(2) = 13
* distances_down[4] = 1* number_of_paths_down[4] = 1
* (distances_down[4] does not include the node 4 itself)N - 1 - distances_down[4] = 8 - 1 - 1 = 6
* sod(2) - 1 - distances_down[4] - number_of_paths_down[4] = 13 - 1 - 1 - 1 = 10
If you remember this from the function we defined earlier, you will notice that the contribution of a child
node to the two values distances_down and number_of_paths_down
is n_paths + 1
and n_paths + s_paths + 1
respectively. Naturally, that is what we subtract to obtain the remaining tree.
如果您从我们之前定义的函数中记住了这一点,则会注意到child
节点对两个值distances_down and number_of_paths_down
的n_paths + 1
分别为n_paths + 1
和n_paths + s_paths + 1
。 自然地,这就是我们减去以获得剩余的树。
sod(4)
represents the sum of edges on all the paths originating at the node 4
in the tree above. Let’s see how we can find this out using the information we have calculated till now.
sod(4)
表示所有树上始于节点4
的路径上的边之和。 让我们看看如何使用到目前为止所计算出的信息找出答案。
distances_down[4]
represents the answer for the tree rooted at the node 4
but it only considers paths going to its successors, that is all the nodes in the tree rooted at 4
. For our example, the successor of 4
is the node 6
. So, that will directly add to the final answer. Let’s call this value own_answer
. Now, let’s account for all the other paths.
distances_down[4]
表示针对以节点4
为根的树的答案,但它仅考虑去往其后继节点的路径,即树中以4
为根的所有节点。 对于我们的示例的后继4
是节点6
。 因此,这将直接增加最终的答案。 我们将此值own_answer
。 现在,让我们考虑所有其他路径。
4 --> 2 (1 edge)4 --> 2 --> 5 (1 + 1 edge)4 --> 2 --> 1 (1 + 1 edge)4 --> 2 --> 1 -->7 (1 + 2 edges)4 --> 2 --> 1 --> 7 --> 9 (1 + 3 edges)4 --> 2 --> 1 --> 7 --> 10 (1 + 3 edges)own_answer = 1
sod(4) = 1 + 1 + 2 + 2 + 3 + 4 + 4 = 17
sod(4) = own_answer + (N - 1 - distances_down[4]) + (sod(2) - 1 - distances_down[4] - number_of_paths_down[4]) = 1 + 6 + 10 = 17
Before you go bonkers and start doing this, let’s look at the code and bring together all of the things we discussed in the example above.
在开始笨拙地开始执行此操作之前,让我们看一下代码,并将上面示例中讨论的所有内容汇总在一起。
The recursive relation for this portion is as follows:
此部分的递归关系如下:
Yes, indeed you did!
是的,确实的你做到了!
Consider the following example tree:
考虑以下示例树:
The question asks us to find the sum of distances for all the nodes in the given tree. So, we would do something like this:
这个问题要求我们找到给定树中所有节点的距离总和。 因此,我们将执行以下操作:
for i in range(N): ans.append(find_distances(N))
But, if you look at the tree above, the recursive call for the node 5
would end up calculating the answers for all the nodes in the tree. So, we don’t need to recalculate the answers for the other nodes again and again.
但是,如果您查看上面的树,则对节点5
的递归调用最终将计算出树中所有节点的答案。 因此,我们不需要一次又一次地为其他节点重新计算答案。
Hence, we end up storing the already calculated values in a dictionary and use that in further calculations.
因此,我们最终将已经计算出的值存储在字典中,并将其用于进一步的计算中。
Essentially, the recursion is based on the parent of a node, and multiple nodes can have the same parent. So, the answer for the parent should only be calculated once and then be used again and again.
本质上,递归基于一个节点的父节点,并且多个节点可以具有相同的父节点。 因此,对于父母的答案应该只计算一次,然后一次又一次地使用。
If you’ve managed to read the article this far (not necessarily in one stretch ?), you’re awesome ?.
如果您到目前为止已经读完了这篇文章(不一定是一口气?),那您真棒吗?
If you found this article helpful, share as much as possible and spread the ?. Cheers!
如果您发现本文有帮助,请尽可能多地分享并传播?。 干杯!
翻译自: https://www.freecodecamp.org/news/recursion-demystified-99a2105cb871/
递归函数非递归化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。