1 条题解
-
0
题目分析
这是一个树形结构上的优化问题。需要选择一名负责人,然后通过交换操作最大化团队人数,同时最小化交换次数。
关键约束:
- 选择负责人后,其子树中所有相同编程语言的员工加入
- 可以通过交换增加团队成员
- 交换必须发生在同一深度的员工之间
- 一个在子树内但语言不同,一个在子树外但语言相同
算法思路
核心思想
使用DSU on Tree(树上启发式合并)+ 深度统计:
- 预处理:计算每个节点的深度、子树大小、DFS序
- 深度分布统计:统计每种语言在每个深度的员工数量
- DSU on Tree:高效统计子树信息
- 贪心计算:对于每个可能的负责人,计算最大团队人数和最少交换次数
关键观察
- 如果选择节点
x作为负责人,深度为d - 那么在深度
d' ≥ d的层,最多能获得min(该语言在深度d'的人数, 该深度总人数)个该语言的员工 - 交换次数 = 总获得人数 - 子树内原有该语言人数
代码详解
数据结构定义
int n, k, a[100100]; // a[i]: 员工i的编程语言(1-indexed) int head[101000], tot; // 邻接表头 int idl[100100], idr[100100]; // DFS序区间 int rk[100100]; // 反向映射:DFS序->节点编号 int deep[100100]; // 深度 int son[100100], siz[100100]; // 重儿子、子树大小 int in[101000]; // 当前子树中每种语言的人数 int mp[100100]; // 当前子树中每个深度的人数 int vis[100100]; // 语言访问标记 int ansx, ansy; // 答案:最大人数、最少交换次数 vector<pair<int, int>> g[101000]; // g[lang]: {(深度, 该深度该语言人数)}第一次DFS:预处理
void dfs(int x, int fa) { idl[x] = ++cnt; // 进入时间 rk[cnt] = x; // 反向映射 siz[x] = 1; deep[x] = deep[fa] + 1; // 记录该语言的深度分布 g[a[x]].push_back({deep[x], 1}); int maxx = 0; for (int i = head[x]; i; i = edge[i].nxt) { int y = edge[i].to; if (y == fa) continue; dfs(y, x); siz[x] += siz[y]; // 找重儿子(子树最大的儿子) if (maxx < siz[y]) { maxx = siz[y]; son[x] = y; } } idr[x] = cnt; // 离开时间 }语言深度统计的压缩
for (int i = 1; i <= k; i++) { sort(g[i].begin(), g[i].end()); // 按深度排序 st.clear(); // 合并相同深度的记录 for (pair<int, int> op : g[i]) if (st.empty() || st.back().x != op.x) st.push_back(op); else st.back().y++; swap(st, g[i]); // 更新为压缩后的结果 }结果:
g[lang]变为按深度排序的列表,每个元素是(深度, 该深度该语言人数)第二次DFS:DSU on Tree
void dfs2(int x, int fa, int op) { // op=1表示保留信息,op=0表示清除信息 vis[a[x]]++; // 标记当前语言被访问 // 1. 先处理轻儿子(清除模式) for (int i = head[x]; i; i = edge[i].nxt) { int y = edge[i].to; if (y == fa || y == son[x]) continue; dfs2(y, x, 0); } // 2. 处理重儿子(保留模式) if (son[x]) dfs2(son[x], x, 1); // 3. 添加当前节点信息 mp[deep[x]]++; // 深度计数+1 in[a[x]]++; // 当前语言计数+1 // 4. 合并轻儿子的信息 for (int i = head[x]; i; i = edge[i].nxt) { int y = edge[i].to; if (y == fa || y == son[x]) continue; // 遍历轻儿子的整个子树 for (int j = idl[y]; j <= idr[y]; j++) { int node = rk[j]; mp[deep[node]]++; in[a[node]]++; } } // 5. 计算结果(以x为负责人) vis[a[x]]--; if (!vis[a[x]]) { // 如果当前语言没有被访问过(避免重复计算) int ans = 0, sum = 0; int lang = a[x]; int depth = deep[x]; // 二分找到深度≥depth的位置 int b = lower_bound(g[lang].begin(), g[lang].end(), make_pair(depth, 0)) - g[lang].begin(); // 计算最大可能人数 for (int i = b; i < g[lang].size(); i++) { int dep = g[lang][i].x; int cnt_lang = g[lang][i].y; // 该语言在该深度的人数 int cnt_total = mp[dep]; // 该深度的总人数 ans += min(cnt_lang, cnt_total); // 最多能获得的人数 } // 交换次数 = 总获得人数 - 子树内原有该语言人数 sum = ans - in[lang]; // 更新答案 if (ansx < ans) { ansx = ans; ansy = sum; } else if (ansx == ans && ansy > sum) { ansy = sum; // 人数相同,取更少交换次数 } } // 6. 如果是轻儿子,清除信息 if (!op) { for (int i = idl[x]; i <= idr[x]; i++) { int node = rk[i]; mp[deep[node]]--; in[a[node]]--; } } }算法原理
DSU on Tree 的优势
普通的暴力做法需要对每个节点遍历其子树,复杂度
O(n²)。DSU on Tree 通过:
- 重儿子保留:重儿子的信息不清除,直接继承
- 轻儿子合并:轻儿子的信息单独计算后合并
- 复杂度优化:从
O(n²)降到O(n log n)
最大人数的计算逻辑
对于负责人
x(深度为d,语言为L):-
在深度
d' ≥ d的层:- 该层总共有
mp[d']个员工(在x的子树中) - 整个公司有
cnt_lang个语言L的员工在该层 - 最多能获得
min(mp[d'], cnt_lang)个语言L的员工
- 该层总共有
-
为什么?因为:
- 最多只能有
mp[d']个位置(子树内的位置) - 最多只有
cnt_lang个该语言的员工 - 通过交换,可以用子树外的该语言员工替换子树内的其他语言员工
- 最多只能有
最少交换次数的计算
in[lang]:子树内原有该语言人数ans:最终能获得的总人数- 交换次数 =
ans - in[lang]
解释:需要把
ans - in[lang]个该语言的员工从子树外交换进来复杂度分析
时间复杂度
- 第一次DFS:
O(n) - 语言统计压缩:
O(n log n)(排序) - 第二次DFS:
O(n log n)(DSU on Tree) - 总复杂度:
O(n log n)
空间复杂度
- 邻接表:
O(n) - 各种数组:
O(n) - 语言深度统计:
O(n)
样例分析
样例3执行过程
输入:
9 3 0 0 2 1 2 0 2 1 2 4 8 1 0 4 1 0 7算法步骤:
- DFS预处理,计算深度、重儿子等
- 统计每种语言的深度分布
- DSU on Tree计算每个节点作为负责人的结果
- 发现以节点4为负责人最优:
- 最大人数:4
- 最少交换次数:2
关键技巧
- DFS序转换:将子树操作转化为区间操作
- 重链剖分:实现DSU on Tree的基础
- 深度分层统计:处理交换的深度约束
- 语言压缩:合并相同深度的记录,优化查询
- 贪心选择:在人数相同时选择交换次数少的方案
- 1
信息
- ID
- 5709
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者