We are given a tree with nodes (). On each node there is a number of "genies" (). For a pair of nodes and , let denote the total number of genies on all nodes on the path from to (inclusive).
We would like to process a series of queries (at most queries will be given). There are two types of queries:
- "Update ": Set (the number of genies on node ) to the new value
- "Count ": Print the total number of genies on all nodes on the path from node to node (inclusive). That is, print
For each of the second type of query, print the result.
- Tree Problem
- Make into a rooted tree. Pick an arbitrary root.
- Queries ==> data structures
- Paths on a tree.
- Lowest Common Ancestor (LCA) Data Structure
- Heavy-Light Decomposition
- Fenwick Trees (Binary Indexed Trees)
- Fenwick Tree + Heavy-Light Decomposition
- DFS Ordering (Pre-Order Traversal) Array
IdeaPaths on Rooted Trees + LCA
This section describes a great deal of experience and intuition needed to correctly model the problem. If you have never heard of "Lowest Common Ancestor" before then you should read this section in detail first. A more advanced reader should at least view the Key Observations and Lemmas of this section.
The first rule of dealing with trees: Pick an Arbitrary Root. It turns out that dealing with rooted trees is much easier than dealing with un-rooted trees. This is true because rooted trees admit a recursive "optimal sub-structure". In simple words: Every rooted tree is made up of smaller rooted sub-trees, so solving problems on an entire tree can often be solved recursively or with dynamic programming. Knowing this makes many tree problems a lot easier to handle. We will apply this here. (NOTE: Be careful. There may be many problems where this "rule" doesn't apply.)
The above "rule" about trees is often a good one to keep in mind! Let's consider it a learning point for the reader!
Suppose we pick an arbitrary node to be the root of the tree (without loss of generality, let's choose node 0). Now, consider a pair of nodes and . Then there is some node such that the path from to is the concatenation of the path from to and from to , where is an "ancestor" of both and (according to the rooted tree).
Such a node is called the "lowest common ancestor" (or LCA) of and .
We will use to denote the ancestor of and whose depth in the tree is maximum. This corresponds to the node as described in the previous Key Observation.
Many problems on trees -- such as the current problem -- relate to "paths" in trees. Whenever we have such a problem on a rooted-tree, it is often beneficial to break down paths into "simple" paths from nodes to ancestors only (i.e.: paths that only travel directly down the tree, or directly up the tree, but do not otherwise "bend".)
These kinds of simpler paths are useful mostly because there are many data structures and techniques that exploit the "parent-child" relationship or the "ancestor-descendant" relationship. For example, I know of at least two techniques that allows one to efficiently "update all descendants of a node" or "update all ancestors of a node" (I will actually describe them below because they can be used to solve this problem). Lastly, there is a very efficient data structure to compute the "Lowest Common Ancestor" of any two nodes of a rooted tree (Google "Lowest Common Ancestor" and you should find out about it; I believe this was invented by Robert Tarjan!) And it is not too hard to see that the Key Observation from above is true for all rooted trees: every path from a node to node can be broken down into a path from to and a path from to , where .
Altogether, the existence of these techniques and data structures makes it highly desirable to convert our original definition of a path into equivalent definitions using the LCA.
This is something you learn from experience. However, let's call it another learning point!
There is a classic data structure that enables you to efficiently compute for any pair of nodes . And any path from to can be broken down into paths from to and to . So, most query problems that deal with paths on a trees can be solved by reducing the problem in a way that exploits the LCA data structure.
For any pair of nodes and , let be the lowest common ancestor of and . Then:
This basically follows from the key observation above. We break the path into a path from to and from to . We have to subtract (the number of genies on node ) to avoid double-counting.
Let's take this idea one step further. But first, some intuition.
Suppose we were only given an array of genies (rather than a tree). How would we solve our original problem?
Well, what we could do is compute the "Prefix Sum" array. That is, if we have an array and we want to be able to quickly compute the sum for some pair , the we can create a new array defined so that:
is called the "Prefix Sum" array. Then, to compute any sum , we simply take:
Although there are a couple of corner cases to handle, this works in general, and gives us an -time method to quickly find the sum of any contiguous sub-array of the original array . In particular, we pre-compute the answers for queries and we answer the questions for by subtracting particular entries of the pre-computed data.
In the case of our given problem, we are not given an array, but we are given a tree. And we are not concerned about summing contiguous sub-arrays, but we instead want to find the sum of numbers along a path. From our earlier arguments, we only have to worry about paths of the form where is an ancestor of . Now, is there any way to "generalize" the "Prefix Sum" array idea from arrays to trees like this?
In this case, the answer is YES! We have the following Key Observation.
Let and be nodes where is an ancestor of . Then:
If we know the answer from to and from to , then the total amount of genies from to () must be the total amount of genies from to , minus the number of genies on the path from to (excluding the node itself). This yields the formula.
The above lemma basically says that, if we can quickly compute for all nodes , then we can quickly compute for any node and ancestor . This is a tree-like analogue to using Prefix Sums for arrays.
Is this helpful? We shall see in the next Idea Section how to construct a data structure that easily allows us to compute the total amount of genies from a node to the root (i.e.: ). By using this with the ideas above, we can therefore compute for any node and ancestor . Lastly, by combining this with the LCA data structure, this will allow us to compute for any arbitrary pair of nodes and .
IdeaPre-Order Traversal + Fenwick TreeAC
This Idea Section describes an advanced technique needed to solve this problem. If you are Intermediate or Advanced reader, but you were unable to solve this problem, then this section is crucial. If you are a beginner, please read the previous section first.
In the last Idea Section, we saw how to model this problem using the Lowest Common Ancestor (LCA). In particular, to answer the query it suffices to answer the queries and where is the LCA of and . We took this one step further and noted that, to answer , it suffices to know and .
In this section, we show how to efficiently answer the latter question: How do we quickly answer queries of the form for any node .
First, notice that, if the problem were "static" (i.e.: the numbers were not changing), we could probably solve this by performing a depth first search (dfs) at the beginning, and pre-computing the answers. In particular, at any point in the dfs, if we are at node , we would keep track of the sum of the number of genies along the path from the root (node ) to node . When we traverse another edge, we add to the sum, and when we "backtrack" out of the edge, we subtract . This traversal would maintain the correct number at each node.
Although this problem is not static, we can still draw a great deal of intuition from this special case. In particular, we can still (theoretically) perform this dfs at the beginning. Then, when an "Update" query comes, we notice that we only have to update the descendants of the changing node.
(See the Related Problems section at the bottom of this page.)
We formalize this with the following key observation.
Suppose we were to pre-compute the answer for each node . When an "Update " query is given (to change the number of genies on node to become value ), we notice that will only change for those nodes who are descendants of . All other nodes will have their sum value stay the same. Moreover, the value of those that are children of changes uniformly (i.e.: by the same amount) -- exactly by the amount: (i.e.: the new value of node , minus the old value). Note, the term "descendant" also includes itself (that is, is a descendant of itself, technically).
Based on the above observation, we have simplified the problem to the following question: "How can we efficiently increment a value for all children/descendants of a node ?"
If we can quickly answer this above question, we are pretty much done. Particularly, we store the values for all nodes in the tree. Then, whenever an "Update " query comes in, we simply update (increment or decrement) the "stored values" for and all its descendants by the amount in order to maintain the correct answer. Whenever a "Count " query comes in, we find , and our answer will be , which is altogether equal to:
since , and is computed similarly. Notice that this latter formula only requires knowing and , which are all "stored" values.
So how do we answer this question?
There is a "trick" that can be used to quickly "update (increment/decrement) all descendants" of a node in a tree. First, the reader must be familiar with "Binary Indexed Trees" (also known as "Fenwick Trees"). The reader should also be fairly comfortable with tree-traversals: for example, you should know about Depth-First-Search (dfs) and Pre-Order-Traversal.
Let be the array defined by the "Pre-Order-Traversal" of the tree. In particular, is defined recursively. (the root node). Then, it is followed by the Pre-Order-Traversal arrays of the sub-trees of node 0. It can be constructed by running a particular depth-first-search on the tree.
(Note: This is fairly hard to describe. If the reader does not understand the concept of "Pre-Order-Traversal" array, please Google it.)
Consider a particular node . In the Pre-Order-Traversal array, the node and all of its descendants will appear consecutively, in a contiguous sub-array of .
Omitted. But it can be easily proven by induction on the sub-trees, or by inspecting the algorithm used to construct the Pre-Order-Traversal array.
This lemma is very important, in particular, we notice that we can "update" the stored value for all descendants of if we know how to update the values in contiguous sub-arrays. This gives us the following key observation.
Suppose we have the array representing the Pre-Order-Traversal array. Let be another array so that denotes the current known value of . That is, let denote the current sum of all genies from the node of the Pre-Order-Traversal, up to the root of the tree. Then, originally, we can compute (by some kind of depth first search maybe?). And whenever an "Update " query comes in, we only need to update a contiguous range of the array.
We can use Fenwick Trees to update a contiguous range of an array. By constructing the Fenwick Tree for the array , and by knowing which node corresponds to which index (according to the Pre-Order-Traversal array ), this allows us to quickly answer the question we were interested in!!!
We now have enough information to solve the entire problem. The reader may be a bit confused because we made several simplifications! Here we present the final complete algorithm to solve this problem.
We are given a tree , an initial value for each node , and a series of queries of the forms: "Update " and "Count " (as described in the problem statement). Here is the final pseudocode:
<code class="python">
# Pre-define some functions
Initialize(T):
Treat T as a rooted tree, and let node 0 be the root
Construct the LCA (lowest common ancestor) data structure on T
# PreOrderTraversal(i,A) populates the array A with the
# pre-order-traversal for the sub-tree rooted in node i
def PreOrderTraversal(i,A):
A.push(i)
for each child j of i:
PreOrderTraversal(j,A)
# lca(i,j) returns the lowest common ancestor of i and j
def lca(i,j):
...
# If B is an array, update(B,i,j,v) increments B[i..j] by value v (using a Fenwick Tree)
def update(B,i,j,v);
...
### MAIN ALGORITHM BEGINS HERE
solve(T,v[]):
let N = |T| #(the number of nodes in T)
let A[0..N-1] be an array
let B[0..N-1] be an array
# initialization
call Initialize(T)
call PreOrderTraversal(0,A)
for all i = 0..N-1
Set B[i] = v[i]
for each query:
if query is type "Update i,new_v":
Let L be the index (integer) so that A[L] = i (the index corresponding to node i in A)
Let R be the largest index such that A[R] is a descendant of i
# Note: A[L..R] now precisely contains the sub-tree rooted in i
Let v_change := v[i] - new_v
update(B,L,R, v_change)
Set v[i] = new_v
else (query is type "Count i,j"):
Let c := lca(i,j)
Let sum_ic = B[i] - B[c] + v[c] # sum(i,c) = sum(i,0) - sum(i,c) + v[c]
Let sum_jc = B[j] - B[c] + v[c] # sum(j,c) = ... (similarly)
Let ans = sum_ic + sum_jc - v[c] # sum(i,j) = sum(i,c) + sum(j,c) - v[c]
Print (ans)
</code>Calling should solve the problem.
An implementation detail: the array will probably not be stored "explicitly". We can use data structures such as the Fenwick Tree to "simulate" such an array. (See the Related Problems section at the bottom of this page to see how to implement a "dynamic array")
Just a quick note on the time/space complexity: all of the data structures use memory altogether (maybe at worst, depending on how you implement the array / data structure). Each query takes time in total: to find (if necessary), and to update or access the array (assuming is implemented with a Fenwick Tree).
Hence, the overall algorithm takes where is the number of nodes and is the number of queries.
Transforming the "tree" into an "array" was a neat trick. In particular, we used the "Pre-Order-Traversal" of the tree to define an ordering of the nodes such that all descendants of a node appear contiguously in the array. This made it really easy for us to process queries of the form: "Update all descendants of ". This is a trick that can often be used whenever we have queries of this form!
All in all, this gives us our final algorithm, and the solution to the problem!!
Review
Summary
This is a beautiful illustration of many Data Structures on Trees. To solve this problem, you need knowledge of:
- The Lowest Common Ancestor data structure
- Prefix Sums and Range Queries
- Pre-Order-Traversal
- Fenwick Trees
These ideas allow you to (eventually) reduce the problem into a "range query" problem on a simple array. There are quite a few non-trivial "reductions" (problem transformations) needed to get there, but in the end, it is quite a neat solution!
First, we assume the tree is rooted, and that node is the root of the tree. This makes the problem easier to conceptualize and solve (because there are many data structures and techniques to solve problems on rooted trees).
The next "reduction" is to notice that, for rooted trees, the query (finding the number of genies on the path from to ) can be reduced to finding the answer to queries and , where is an "ancestor" of both and (it is actually the "Lowest Common Ancestor" or ). These types of queries are easier to handle (because they are always concerning parents and children, or ancestors and descendants, rather than arbitrary nodes / paths). And the well-known LCA data structure makes it a routine task to find whenever we need to!
The precise formula is:
(where is the current number of genies on node ). The last subtraction of is to avoid double-counting it.
Next, we notice that queries of the form (where is an ancestor of ) can be reduced to finding the queries and , where is the root of the tree. There are only queries of the form , so this is much more manageable.
The precise formula is:
So, putting together the above two formulas, we can see that all queries can be solved by:
(where this formula only depends on queries from nodes to the root.)
So we are only concerned with answering queries of the form . Now, let's pretend we already know for all nodes . If an "update" query comes on some node for some value , which of the answers will change? Well, for a node , if is a child or descendant of node , then will change (because is on the path from to ). And if is NOT a descendant of , then will NOT change. So, when an update query comes, we want to update precisely all of the nodes in the sub-tree of . And when a "count" query comes, we just want to lookup the answer for the given node.
We can actually do this fairly efficiently. Consider the "Pre-Order-Traversal" array of the tree. That is, is a "permutation" of the numbers through defined recursively by a depth-first-search.
The key observation about the array is that, for a given node , all of its descendants will appear consecutively (next to each other) in this array. So, when an "update" query comes in, since we would like to update all descendants of the node , it is equivalent to updating all the nodes in a contiguous sub-array of . So we do precisely that. We construct another array so that (the number of genies on the path from node to node 0). Whenever we want to update some node , we find its index in , and we find the indices of all its children in . This defines a range of indices. We then increment/decrement the corresponding values in the array: with a range update.
Lastly, in order to efficiently perform these range updates and range queries on the array, we must avoid "explicitly" storing the data. Instead, we construct a Fenwick Tree to represent . A Fenwick Tree can support range updates and single-point queries (which is precisely what we need) in time. This is sufficient.
As long as we can maintain the array with a Fenwick Tree, we can answer a query using the formula above:
where and can be computed from the Fenwick Tree. This yields an overall solution.
-
Assume the tree is rooted at node . Without loss of generality.
-
For a pair of nodes and , let denote . Then . This shows that we can reduce queries on arbitrary paths to queries on "ancestor-descendant" paths.
-
For a pair of nodes and (where is an ancestor of ). Then . This shows that we can reduce all queries into those dealing with the root node.
-
Suppose we somehow "store" for all nodes . When an "update" query comes for a specific node, we have to update exactly those nodes that are descendants of the changing node. Moreover, we increment/decrement all of these nodes by the same amount. This can be seen by inspection. If is changing, then changes for some node if and only if is in the sub-tree of .
-
Consider the Pre-Order-Traversal array of the tree. For any node , all the descendants of (including itself) will appear consecutively in this array. This makes the problem of "updating a sub-tree" into the problem of "updating a contiguous sub-array". The latter problem is much easier to solve.
-
Suppose we "relabel" the nodes according to their Pre-Order-Traversal, and we construct an array so that . Then to "update" a node, we simply have to update a contiguous range in . This can be done with a Fenwick Tree. This was the last step in the algorithm. Above we do not "relabel" the nodes, but instead of use new labelling (according to the Pre-Order-Traversal) as indices into the array (these are equivalent definitions).
-
The entire problem can be solved in time per query.
-
This problem requires a lot of prior knowledge. We needed to recognize that we should transform the tree into a rooted tree. We need to know about the Lowest Common Ancestor, Fenwick Trees, Prefix Sums, and Pre-Order-Traversal Array -- all non-trivial data structures. This was not an easy problem to solve without knowing about all of these things.
-
Turning the tree into a rooted tree is an example of a problem transformation.
-
Arbitrary queries were too hard to solve. Instead we repeatedly simplified our queries. By using the LCA of and (denoted by ), we could simplify into queries of the form and . Taking this one step further, we were able to simplify all the queries into forms , and . This made our task much easier to solve.
-
Although not explicitly stated above, it is helpful to clearly write down the formulas that we get after each simplification. This helps to avoid bugs later on!
-
The First Rule of Tree Problems: Pick an arbitrary root! This appears very often because "rooted" trees have much more structure and symmetry than un-rooted trees. Often-times the root doesn't matter. Sometimes the root matters, or sometimes you have to "try" all nodes as the root.
-
Whenever you encounter problems that relate to "paths on a rooted tree", it is very likely that the LCA data structure will be useful. Why? Because every path from to can be partitioned into two paths, one from to and one from to where is the LCA of and . And these kinds of paths are easier to deal with.
-
Whenever you encounter problems that relate to querying/updating "sub-trees of a rooted tree", it might be possible that the Pre-Order-Traversal Array will be useful! Why? Because the Pre-Order-Traversal Array has the property that all nodes in a given sub-tree occur consecutively in the array. This makes it easier to perform range-updates, range-queries, and even "splitting" and "merging" sub-trees (i.e.: if we treat them like linked lists)! Arrays are much easier to deal with than trees (see some of the Related Problems below).
-
Trees / Querying Paths on a Tree / Lowest Common Ancestor (LCA)
-
Trees / Querying Sub-Trees of a Tree / DFS Ordering / Pre-Order-Traversal
-
Data Structures / Range Queries / Fenwick Trees and Segment Trees
-
Prefix Sums / Prefix Sums on a Tree / Sums of Ancestor Paths
-
Range Queries on a Tree / Heavy-Light Decomposition (HLD)
Static Version of the Problem. What if there were no "update" queries. What if you were simply given the number of genies at the beginning, and you had to only answer queries of the form ? Can you find a way to answer all queries in time with pre-processing?
Range Updates and Point Queries. Given an array , we want to support "range updates" (i.e.: increment by a value ) and "point queries" (i.e.: find the value of for some ). Explain how one would use a Fenwick Tree to solve this problem. (Note: This is a simplified version of the problem we faced here.)
Point Updates and Range Queries. Given an array , we want to support "point updates" (i.e.: increment by a value ) and "range queries" (i.e.: find the value of ). Explain how one would use a Fenwick Tree to solve this problem. How is this similar/different to the previous problem?
Range Updates and Range Queries. Given an array , we want to support "range updates" (i.e.: increment by a value ) and "range queries" (i.e.: find the value of ). How would you solve this problem? Can you still use Fenwick Trees to solve it? Explain how you would use Segment Trees or Range Trees to solve this problem.
Update all parents of a node. We used the Pre-Order-Traversal array (along with a Fenwick Tree) to allow us to "update an entire sub-tree" and to "query a single node". What if we wanted to support the following queries: a) "increment a node and all it's ancestors/parents by a value ", and b) "count the value of any node "? How might we solve this problem? Is there any way to reduce this problem to the previous one? Can we still use the Pre-Order-Traversal array? Can we still use Fenwick Trees? Can we rephrase this problem in terms of sub-trees?
Heavy-Light Decomposition. Learn about the Heavy-Light Decomposition Data Structure. This data structure decomposes a tree into disjoint paths that go from ancestors to descendants; each path can be treated as an "array" (which you can use a Fenwick Tree on). Can you solve the current problem using Heavy-Light Decomposition?
SPOJ 14932. Lowest Common Ancestor (LCA) Practice with LCA
(More to come later.)