E1. PermuTree (easy version)
E1. PermuTree (easy version)
This is the easy version of the problem. The differences between the two versions are the constraint on and the time limit. You can make hacks only if both versions of the problem are solved.
You are given a tree with vertices rooted at vertex .
For some permutation of length , let be the number of pairs of vertices such that . Here, denotes the lowest common ancestor of vertices and .
Find the maximum possible value of over all permutations of length .
A permutation of length is an array consisting of distinct integers from to in arbitrary order. For example, is a permutation, but is not a permutation ( appears twice in the array), and is also not a permutation ( but there is in the array).
Input
The first line contains a single integer ().
The second line contains integers () indicating that there is an edge between vertices and .
Output
Output the maximum value of .
Examples
input
5 1 1 3 3
output
4
input
2 1
output
0
input
6 1 2 2 1 5
output
7
input
4 1 1 1
output
2
Note
The tree in the first test:
One possible optimal permutation is with suitable pairs of vertices:
, since and ,
, since and ,
, since and ,
, since and .
The tree in the third test:
The tree in the fourth test:
解题思路
对于所有合法的数对,按照其最近公共祖先进分类可得到类。将最近公共祖先固定,分别求当时,满足的数对的最大数目。问题等价于考虑以为根节点的子树,并且和分别在以所有儿子所构成的不同子树中,满足合法数对的最大数量(当没有儿子或只有一个儿子,那么不存在合法的数对,因为不等式不成立,或者是最近公共祖先不是)。
因此在固定了最近公共祖先为后,假设节点有个儿子,那么我们只用关心在以其个儿子所构成的各个子树中,有多少个节点满足(那么剩下的节点自然满足大于)。假设以第个儿子为根的子树大小是,并且有个节点满足,那么所有以为最近公共祖先的合法数对的数量就是
事实上对于任意的满足的,都可以找到与之对应的排列。先定义集合表示往以为根的子树中所有节点所表示的位置填入的数,集合的大小就是子树的大小。表示节点的儿子数量。构造的方法如下:
- 先从根节点开始,此时。
- 假设根节点为,的儿子分别为,对应的最优值是。均为空集。
- 对于从到的每一个,依次从中选出前个最小的数放到中,并从中删除。
- 从中选出最小的数作为,并从中删除。
- 对于从到的每一个,依次从中选出前个最小的数放到中,并从中删除。
- 对于每一个儿子,重复上述过程,直到不存在儿子。
我们现在要做的是最大化上述表达式的值。定义状态表示固定最近公共祖先(以为根的子树)后,考虑和在前个儿子所构成的子树中,并且,所合法数对的最大值。根据第个子树中有多少个节点满足来进行状态划分,定义,状态转移方程为
其中限制是根据不等式组得到。则表示数对均在前个子树时所得到的最大数量。表示在第个子树,在前个子树的数对数量。表示在前个子树,在第个子树的数对数量。
因此最近公共祖先为的合法数对的最大数量就是。又因为以不同节点为最近公共祖先的合法数对的最大值是相互独立的,因此最终答案就是将分别求到的最大值累加,即
看上去总的时间复杂度好像是,实际上应该是,这里先贴代码,下面再补充证明。参考01背包的空间优化,这里也可以将优化为。
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 5010; 5 6 int head[N], e[N], ne[N], idx; 7 int sz[N]; 8 int f[N]; 9 10 void add(int v, int w) { 11 e[idx] = w, ne[idx] = head[v], head[v] = idx++; 12 } 13 14 int dfs(int u) { 15 sz[u] = 1; 16 int ret = 0; 17 for (int i = head[u]; i != -1; i = ne[i]) { 18 ret += dfs(e[i]); 19 sz[u] += sz[e[i]]; 20 } 21 memset(f, 0, sizeof(f)); 22 for (int i = head[u], s = 0; i != -1; i = ne[i]) { 23 for (int j = s + sz[e[i]]; j >= 0; j--) { 24 for (int k = max(0, j - s); k <= min(sz[e[i]], j); k++) { 25 f[j] = max(f[j], f[j - k] + k * (s - (j - k)) + (sz[e[i]] - k) * (j - k)); 26 } 27 } 28 s += sz[e[i]]; 29 } 30 return ret + *max_element(f, f + sz[u]); 31 } 32 33 int main() { 34 int n; 35 scanf("%d", &n); 36 memset(head, -1, sizeof(head)); 37 for (int i = 2; i <= n; i++) { 38 int x; 39 scanf("%d", &x); 40 add(x, i); 41 } 42 printf("%d", dfs(1)); 43 44 return 0; 45 }
当最近公共祖先固定后,看状态转移的代码:
1 for (int i = head[u], s = 0; i != -1; i = ne[i]) { 2 for (int j = s + sz[e[i]]; j >= 0; j--) { 3 for (int k = max(0, j - s); k <= min(sz[e[i]], j); k++) { 4 f[j] = max(f[j], f[j - k] + k * (s - (j - k)) + (sz[e[i]] - k) * (j - k)); 5 } 6 } 7 s += sz[e[i]]; 8 }
可以知道计算量大约为
其中表示总循环次数。表示最里面那层循环的代码的总计算量。考虑每一个节点都作为最近公共祖先的情况,那么整一个dp的计算量大概就是
这里简单证明一下。通过几何意义进行解释,现在有一个由个节点,没有任何边的无向图。可以把理解成在以构成的子树中,其中以第个儿子为根的子树中的每一个点与前个儿子为根的每一个子树中的所有节点连一条边,这样就会有条边。每一条边只会出现一次(因为每次都在子树内部连边),而一个由个节点构成的无向图最多有条边,因此上界就是。
因此时间复杂度为。
参考资料
Codeforces Round #890 (Div. 2) Editorial:https://codeforces.com/blog/entry/119058
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17611803.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效