- Published on
Codeforces Round 890 - E1. Permutree
- Authors
- Name
- Wong Yen Hong
Problem URL: https://codeforces.com/contest/1856/problem/E1
For some reasons, this problem had surprisingly many solves during the contest. It seems like many people are aware of the dp on tree trick. Anyway, let's jump straight into the problem and examine the obscure time complexity of the solution.
Abridged Statement
You're given a tree of nodes, and an array where denotes the parent of node . Let's define as the score of a permutation of to on the tree. Score in this context is defined as the number of pairs such that where denotes the lowest common ancestor of node and . Our task is to find the maximum among all the possible permutation to the tree.
More about the lowest common ancestor (LCA) of two nodes can be found in here.
Example
Assuming the input of our tree is as follows:
5
1 1 3 3
The given input can be visualized as follows:
Assume :
The score will be , because there are pairs of node that satisfy :
- since and
- since and
- since and
- since and
Solution
Let's first focus in finding the number of pairs for an individual node that satisfies where , let's also call this the score of node .
For example, let's try to find the score of node in the following tree.
Specifically, we will find the number of pairs that has and . The number beside each of the node represents .
We could manually count the scores, but that will only help in understanding the problem and won't be helping in finding the solution. Hence, we'll focus on finding a more efficient and systematic way of counting the number of pairs that satisfy the condition. To do this, we will gather some observations.
The first observation is that for any two nodes in the subtree of a children (of node ) won't have node as their LCA. For example, node and node belongs in the subtree of node (children of node ), the LCA of node and node is node instead of . This is pretty obvious, because the common ancestor (not necessarily lowest) of two nodes in the subtree of node won't be higher than node .
Thus, we can further abstract our tree to the following way.
The abstracted tree only contains the necessary information of the subtree of each children now where denotes the number of nodes in the subtree (including itself) and denotes the number of nodes in the subtree that has . For example, in the subtree of node , the set of nodes in that subtree are which has a size of and the set of nodes that has are which has a size of .
*The term "subtree" in this blog will simply refer to the subtree with the children as the root.
To find the score, we can use the formula below:
where denotes the number of children/subtrees for node . This is basically equivalent to pairing every node that has with every node where such that they are in a different subtree. We can use the same formula to find the number of pairs for any node as LCA. To find we can sum the scores of each LCA using the above formula.
Now we know how to find the score for any node as LCA assuminng we have pre-assigned a permutation to the tree. But, that's not our main objective for this problem, we're tasked to maximize the value instead of counting based off a pre-defined .
So, how do we find the optimal that maximizes ?
The question is - Do we even need to find it? Do we even need to know about to find our answer? Based on the previous calculation, the only things that we need to calculate the score of each LCA is just and of each subtree.
It turns out we don't really need to know the permutation to count the answer.
For each LCA, we can simply assign the value of for each subtree of its children where (because the total number of nodes in the subtree that is smaller than cannot be bigger than the total nodes in the subtree) and there will always be a valid that will give the same set of and . To see why this is true, we will demonstrate in the same example above.
Assuming we don't know the permutation and we know the value of for each subtree of children and the target value of . One way to achieve is as follows:
Generally, for LCA , we can set where denotes the -th smallest number of the available numbers. For each of the subtree , we set of the nodes in the subtree with distinct value smaller than .
Now, we can use Dynamic Programming (DP) to maximize the value of each LCA .
Let's define the DP state as follows:
= the maximum scores for the first subtrees (children of ) such that there are nodes that is
To understand the DP transition, let's first look at the difference betweeen the score for children and children first.
The equation can be derrived by cancelling off each similar term on both side of the subtract. And it leads us to the DP transition below
In each transition, we're basically finding the maximum score for the first subtrees if we set .
Finally, we just need to sum up the DP answer for each LCA.
But wait a minute, isn't this DP transition ? Well, hold your horses for a moment. I will prove the time complexity in the analysis below.
Source Code
Source Code
#include <bits/stdc++.h>
#define all(x) begin(x),end(x)
#define fir first
#define sec second
#define sz(x) x.size()
#define pb push_back
using namespace std;
using ll = long long;
using vi = vector<int>;
using pi = pair<int,int>;
using pdb = pair<double,double>;
using pll = pair<ll,ll>;
using vll = vector<ll>;
using ull = unsigned long long;
const double EPS = (1e-6);
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
void setio(string s){
freopen((s + ".in").c_str(),"r",stdin);
freopen((s + ".out").c_str(),"w",stdout);
}
pll dfs(int node, vector<vi>& adj){
ll score = 0;
vi desc(sz(adj[node]));
int total = 0;
for(int i{}; i < sz(adj[node]); i++){
pll cur = dfs(adj[node][i], adj);
score += cur.fir;
total += cur.sec;
desc[i] = cur.sec;
}
vll dp(total+1);
vll dp1(total+1);
int cur = 0;
ll mx = 0;
for(ll i{}; i < sz(adj[node]); i++){
for(ll j{}; j <= cur; j++){
for(ll k{}; k <= desc[i]; k++){
dp[j+k] = max(dp[j+k], dp1[j] + k * (cur - j) + j * (desc[i] - k));
mx = max(mx, dp[j+k]);
}
}
cur += desc[i];
swap(dp, dp1);
fill(all(dp), 0);
}
return {mx+score, total + 1};
}
void solve(){
int n;
cin >> n;
vector<vi> adj(n);
for(int i{1}; i <= n-1; i++){
int p;
cin >> p;
p--;
adj[p].pb(i);
}
cout << dfs(0, adj).fir << '\n';
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
while(t--){
solve();
}
return 0;
}
Time Complexity Analysis
Before going into this section, you can have a read on the visual proof of the complexity of certain tree DPs in this article.
Now let's observe our DP transition in pseudocode.
for i in range(0, total children of root):
for j in range(0, total nodes in the first i-1 subtrees):
for k in range(0, total nodes in the ith subtree):
// transition
How do we match this to the one we see in the article?
Although it has different transition, but they're essentially the same thing! For each batch of nodes in the -th subtree, we're matching all of them to the previous nodes. The total number of operations is essentially the total number of pairs in the subtree. Therefore, the time complexity of this transition is , because the total pairs of nodes is . can easily pass in 2 seconds.