Contribution Technique on Trees
Watch the video for a high level overview before reading the blog.
You are given a rooted tree. How do you find out the distance between any 2 nodes?
Denote
But what if you don’t know how to compute LCA. Can you still solve this problem? It’s difficult. But what if I ask you to compute the distance between nodes
But what if the tree had a special structure. Assume that the nodes of the tree are numbered in DFS order. In this special tree, can you find out the distance between node
What properties does this special tree have?
Consider the subtree of node
The distance between node
But how do you identify the last leaf in the subtree? Note that the DFS order assigns label as soon as it encounters a node. Therefore, the last leaf in the subtree must have the highest label.
Define
It is clear that the distance between
But, for each
This is the first form of Contribution Technique where iterating one quantity over the other improved the time complexity from
Now we know the list of nodes whose successor is somewhere far in another subtree. But is this information useful to us? Not really, because we still don’t know how far. Can we also figure out how far using Contribution Technique?
Let’s consider a path between
It is a combination of an upward path and a downward path. Notice that a downward path always has length 1.
This time, instead of processing nodes and marking
Consider any edge
Therefore, each edge
Consider the edge
Therefore, each edge
So, if we simply iterate over each edge, and increase the edge count for the originating node of the upward and downward path by 1, by the end of the process, for each node, we will have the total number of edges that it encounters on its path to the successor.
This is the second form of Contribution Technique. Notice how focusing on edges instead of nodes helped us to compute the answer faster.
void process_edge(int p, int c) {
int prev = (c - 1 + n) % n;
for (int v : {prev, mx[c]}) {
d[v]++;
}
}
void find_distances() {
for (int c = 1; c < n; c++) {
process_edge(parent[c], c);
}
for (int i = 0; i < n; i++) {
cout << d[i] << " ";
}
cout << "\n";
}
Let’s consider the weighted version of the DFS order tree. Assume each edge has a non-negative weight and none of the weights are revealed yet. But you do know that the total sum of weight on all edges is
For each edge, what will be the maximum distance possible between
Now, what happens when I reveal some of the weights, such that
Hence, we need a way to compute the number of free edges in each path, and the total sum of assigned edges in each path. Since every edge participates in exactly 1 upward and 1 downward path, it is easy to update this contribution.
void add_free_edge(int p, int c) {
int prev = (c - 1 + n) % n;
for (int v : {prev, mx[c]}) {
free_edges[v]++;
}
}
void remove_free_edge(int p, int c, long long w) {
int prev = (c - 1 + n) % n;
for (int v : {prev, mx[c]}) {
free_edges[v]--;
assigned[v] += w;
}
}
void reveal(long long remain) {
for (int c = 1; c < n; c++) {
add_free_edge(parent[c], c);
}
int cnt = n - 1;
while (cnt--) {
int x;
long long y;
cin >> x >> y;
x--;
remain -= y;
remove_free_edge(parent[x], x, y);
long long ans = 0;
for (int i = 0; i < n; i++) {
ans += assigned[i];
if (free_edges[i]) {
ans += remain;
}
}
cout << ans << " ";
}
cout << endl;
}
But what if the problem asked you to compute the sum of distance between successor?
In the Contribution Technique, our updates happen in
Can we optimize it somehow?
Yes, recall the trick that we use to do Topological Sorting via Kahn’s Algorithm. Instead of iterating over the vertices to figure out which new vertices have indegree 0, we rely on the fact that indegree is a decreasing function, so every time we delete a vertex from the graph, we immediately check if the neighbor’s indegree has become zero. If it has, we take it it in the queue, because its indegree can never increase.
We can use the same idea here. Notice that once a path has lost all the free edges, it will never be able to gain it back. So, we can keep 2 sets of vertices. The
Then, the answer is sum of the assigned edges of each path in the locked set + sum of assigned edges of each path in free set + remain*free_set_size
All of these quantities can be stored and updated in a variable. Once the last edge of a path is locked, you transfer the path from
void add_free_edge(int p, int c) {
int prev = (c - 1 + n) % n;
for (int v : {prev, mx[c]}) {
free_edges[v]++;
}
}
void remove_free_edge(int p, int c, long long w) {
int prev = (c - 1 + n) % n;
for (int v : {prev, mx[c]}) {
free_edges[v]--;
assigned[v] += w;
unlocked_sum += w;
if (free_edges[v] == 0) {
unlocked_count--;
locked_sum += assigned[v];
unlocked_sum -= assigned[v];
}
}
}
void reveal(long long remain) {
for (int c = 1; c < n; c++) {
add_free_edge(parent[c], c);
}
unlocked_count = n, locked_sum = 0, unlocked_sum = 0;
int cnt = n - 1;
while (cnt--) {
int x;
long long y;
cin >> x >> y;
x--;
remain -= y;
remove_free_edge(parent[x], x, y);
long long ans = locked_sum + unlocked_sum + unlocked_count * remain;
cout << ans << " ";
}
cout << endl;
}
The last problem that we discussed is actually a 2000 Rated problem on Codefroces, which appeared as Problem E. Iris and the Tree in Codeforces Round 969. Even though the problem statement might look intimidating at first, with Contribution technique, the solution is pretty simple and elegant.
Make sure to upsolve the problem, first in