题解:洛谷 P3605 Promotion Counting
【题目来源】
洛谷:P3605 [USACO17JAN] Promotion Counting P - 洛谷
【题目描述】
奶牛们又一次试图创建一家创业公司,还是没有从过去的经验中吸取教训——牛是可怕的管理者!
为了方便,把奶牛从 \(1\sim n\) 编号,把公司组织成一棵树,1 号奶牛作为总裁(这棵树的根节点)。除了总裁以外的每头奶牛都有一个单独的上司(它在树上的 “双亲结点”)。
所有的第 \(i\) 头牛都有一个不同的能力指数 \(p_i\),描述了她对其工作的擅长程度。如果奶牛 \(i\) 是奶牛 \(j\) 的祖先节点,那么我们把奶牛 \(j\) 叫做 \(i\) 的下属。
不幸地是,奶牛们发现经常发生一个上司比她的一些下属能力低的情况,在这种情况下,上司应当考虑晋升她的一些下属。你的任务是帮助奶牛弄清楚这是什么时候发生的。简而言之,对于公司的中的每一头奶牛 \(i\),请计算其下属 \(j\) 的数量满足 \(p_j > p_i\)。
【输入】
输入的第一行包括一个整数 \(n\)。
接下来的 \(n\) 行包括奶牛们的能力指数 \(p_1,p_2 \dots p_n\)。保证所有数互不相同。
接下来的 \(n-1\) 行描述了奶牛 \(2 \sim n\) 的上司的编号。再次提醒,1 号奶牛作为总裁,没有上司。
【输出】
输出包括 \(n\) 行。输出的第 \(i\) 行应当给出有多少奶牛 \(i\) 的下属比奶牛 \(i\) 能力高。
【输入样例】
5
804289384
846930887
681692778
714636916
957747794
1
1
2
3
【输出样例】
2
0
1
0
0
【核心思想】
-
问题分析:给定一棵以 \(1\) 为根的树,每个节点有权值 \(p_i\)(互不相同)。对每个节点 \(i\),求其子树中权值大于 \(p_i\) 的节点数量。这是一个树状数组 + DFS序/子树统计问题,关键在于利用 DFS 的进入-离开时间戳,通过树状数组统计子树范围内的权值分布。
-
算法选择:
- 权值离散化:将大范围的权值 \(p_i\) 映射到 \(1\) 到 \(n\) 的连续整数,便于树状数组处理
- DFS + 树状数组:在 DFS 过程中,利用树状数组维护当前已访问节点的权值分布
- 差分统计:进入节点 \(x\) 时记录"当前全局比 \(p_x\) 大的数量",离开时再记录一次,两次差值即为子树内比 \(p_x\) 大的数量(因为子树节点在两次记录之间被访问并加入树状数组)
-
关键步骤:
- 读取输入:\(n\),权值 \(p[1..n]\),树结构(父节点关系)
- 离散化:排序去重后将 \(p_i\) 映射到 \([1, n]\)
- 建树:根据父节点关系建立有向边
- DFS(从根节点 \(1\) 开始):
- 进入时:\(ans[x] \mathrel{-}= query(n) - query(p[x])\)(记录当前全局比 \(p_x\) 大的数量)
- 递归子节点:遍历所有子节点并 DFS
- 离开时:\(ans[x] \mathrel{+}= query(n) - query(p[x])\)(再次记录,差值即为子树新增的比 \(p_x\) 大的数量)
- 加入树状数组:\(add2(p[x], 1)\)(将当前节点权值加入统计)
- 输出 \(ans[1..n]\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),离散化 \(O(n \log n)\),DFS 中每个节点两次树状数组操作 \(O(\log n)\)
- 空间复杂度:\(O(n)\),树结构、树状数组和答案数组
-
树状数组 + DFS 的核心思想:
- 进入-离开差分:利用 DFS 的时间顺序,子树内所有节点都在"进入"和"离开"之间被访问。两次查询的差值恰好是子树内满足条件的节点数
- 树状数组动态维护:树状数组维护的是"当前已处理路径上的权值分布",DFS 回溯时自然体现子树范围
- 离散化降维:将大权值映射到小范围,使树状数组空间可控
- 适用于"子树权值统计 + 动态维护"类问题,核心是利用 DFS 序和差分思想将子树问题转化为区间统计
【算法标签】
提高+ #树状数组
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 最大节点数
// 邻接表存储树结构
int h[N]; // 头节点数组,h[i]表示节点i的第一条边
int e[N]; // 边数组,e[i]存储边的终点
int ne[N]; // 下一个边数组,ne[i]存储下一条边的索引
int idx; // 边索引计数器
int n; // 节点数量
int p[N]; // p[i]: 节点i的权值
int b[N]; // b[]: 用于离散化的临时数组
int tr[N]; // 树状数组,用于统计权值出现次数
int ans[N]; // ans[i]: 节点i的答案(逆序对数量)
/**
* 添加有向边 a -> b
* @param a 起点
* @param b 终点
*/
void add(int a, int b)
{
e[++idx] = b; // 存储终点
ne[idx] = h[a]; // 新边指向原链表头
h[a] = idx; // 更新链表头
}
/**
* 计算lowbit:获取x的最低位的1
* @param x 输入数值
* @return x的最低位的1所代表的值
*/
int lowbit(int x)
{
return x & -x; // 利用补码性质
}
/**
* 树状数组单点更新操作
* @param x 更新位置
* @param c 增加的值
*/
void add2(int x, int c)
{
// 树状数组标准更新操作
for (int i = x; i <= n; i += lowbit(i))
tr[i] += c;
}
/**
* 树状数组前缀和查询操作
* @param x 查询位置
* @return 前x个位置的和
*/
int query(int x)
{
int res = 0;
// 树状数组标准查询操作
for (int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
/**
* 深度优先搜索(DFS)计算每个节点的逆序对数量
* 使用树状数组统计当前路径上的权值分布
* @param x 当前节点
*/
void dfs(int x)
{
// 进入节点x时:记录当前比p[x]大的权值数量
// query(n) - query(p[x]) 表示比p[x]大的权值数量
ans[x] -= (query(n) - query(p[x]));
// 递归处理所有子节点
for (int i = h[x]; i; i = ne[i])
dfs(e[i]);
// 离开节点x时:再次统计比p[x]大的权值数量
// 两次统计的差值就是x的子树中比p[x]大的节点数量
ans[x] += (query(n) - query(p[x]));
// 将当前节点的权值加入树状数组
add2(p[x], 1);
}
int main()
{
// 输入节点数量
cin >> n;
// 读入每个节点的权值
for (int i = 1; i <= n; i++)
{
cin >> p[i];
b[i] = p[i]; // 复制权值用于离散化
}
// 对权值进行排序,准备离散化
sort(b + 1, b + n + 1);
// 离散化:将权值映射到1~n的连续整数
for (int i = 1; i <= n; i++)
p[i] = lower_bound(b + 1, b + n + 1, p[i]) - b;
// 读入树结构(n-1条边)
for (int i = 2; i <= n; i++)
{
int x;
cin >> x; // 输入父节点
add(x, i); // 添加边 x -> i
}
// 从根节点1开始深度优先搜索
dfs(1);
// 输出每个节点的答案
for (int i = 1; i <= n; i++)
cout << ans[i] << endl;
return 0;
}
【运行结果】
5
804289384
846930887
681692778
714636916
957747794
1
1
2
3
2
0
1
0
0
浙公网安备 33010602011771号