P1008 商路照影 官方题解
Gioush OJ · P1008 商路照影
商路照影
【题意简述】
给定一棵有根二叉树,第 个节点的权值为 。对于每个节点 ,统计满足 在 的左儿子子树内, 在 的右儿子子树内,并且 的有序点对 数量。
显然,每一个合法点对的中转点 都是唯一的。若记左儿子子树内权值小于 的节点数量为 ,右儿子子树内权值大于 的节点数量为 ,那么节点 对答案的贡献就是 。
【数据点 1∼81\sim 81∼8】
这部分 。
对于每个节点 ,分别遍历其左儿子子树与右儿子子树,统计权值小于和大于 的节点数量即可,时间复杂度为 。
【数据点 9∼129\sim 129∼12】
特殊性质保证二叉树按照编号自然连接,因此整棵树的高度为 。
考虑对于每个节点维护其子树内所有权值组成的有序序列。得到两个儿子的序列以后,可以通过二分查询出 ,再使用归并排序的方法合并两个儿子的序列,并插入 。
每个权值在树的每一层至多参与一次归并,总时间复杂度为 。
【正解一:按权值扫描】
首先对原树进行一次 DFS,求出每个节点的 DFN 序与子树大小。这样任意一个节点 的子树都对应 DFN 序上的连续区间。
于是,求左儿子子树中权值小于 的节点数量,可以转化为一个二维偏序问题:节点的 DFN 序需要落在一段区间中,权值需要小于给定值。
将所有节点按照权值从小到大排序,并使用树状数组维护已经加入节点的 DFN 序。处理节点 时,树状数组中只保留权值小于 的节点,那么在左儿子子树对应的 DFN 区间上查询区间和,得到的就是 。
由于题目要求严格小于,所以相同权值的节点必须放在同一组中。对于一组权值相同的节点,应该先完成这一组的所有查询,再将这一组节点的 DFN 序加入树状数组。
同理,将所有节点按照权值从大到小处理,树状数组中只保留权值大于 的节点,在右儿子子树对应的区间上查询即可得到 。相同权值仍然需要先查询、后加入。
最后计算 即可。
这是一种较自然的二维偏序维护方式:把权值这一维作为扫描顺序,把 DFN 这一维放进树状数组中维护。
排序的时间复杂度为 ,树状数组操作的总时间复杂度为 ,空间复杂度为 。
【正解二:按 DFN 扫描】
也可以将二维偏序的两个维度交换。
我们仍然先 DFS 得到每个节点的 DFN。设节点 的左儿子为 ,右儿子为 。如果 非空,那么 子树在 DFN 序上对应一段区间
要求左儿子子树内权值小于 的节点数量,即
将权值离散化后,严格小于 等价于离散化权值不超过 。于是可以把这次询问拆成两个前缀:
其中 表示在 DFN 前缀 中,权值排名不超过 的节点数量。
右儿子子树内权值大于 的节点数量同理。若总权值排名数为 ,则
因此每个右儿子区间同样可以拆成两个 DFN 前缀。
具体实现时,将所有形如 的询问按照 从小到大排序。然后按 DFN 从小到大扫描节点,每扫到一个节点,就把它的权值排名加入树状数组。此时树状数组维护的是“当前 DFN 前缀中,各权值排名出现了多少次”。处理询问 时,树状数组中恰好已经加入了 DFN 不超过 的节点,查询权值前缀和即可。
代码中的 Q 结构体正是在记录这些前缀询问:
DFN表示前缀右端点 ;Value表示权值阈值 ;Times表示区间差分中的 或 ;From=0表示统计左儿子子树内的小于部分;From=1表示统计右儿子子树内的大于部分。
这一做法与正解一维护的是同一个二维偏序,只是扫描维度从权值换成了 DFN,树状数组中维护的维度也随之从 DFN 换成了权值。
时间复杂度仍为 ,空间复杂度为 。下面代码采用这种写法。
【参考代码】
#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 200010using namespace std; int C[MAXN];int lowbit(int x){return x&-x;}int Query(int x) { int Ret=0; for (;x;x-=lowbit(x)) Ret+=C[x]; return Ret;}void Modify(int x,int Val) { for (;x<MAXN;x+=lowbit(x)) C[x]+=Val; return ;}struct Q{ int Value,DFN,Mark,From,Times;}q[MAXN<<2];bool cmp(Q a,Q b){return a.DFN<b.DFN;}int n,MaxD[MAXN],MinD[MAXN],Size[MAXN][2],Val[MAXN],ls[MAXN],rs[MAXN],dfn[MAXN],Line[MAXN],Mapping[MAXN],Back[MAXN],cnt=0,cnd=0,cnq=0; void Discrete() { sort(Line+1,Line+n+1); for (int i=1;i<=n;i++) { if (i==1||Line[i]!=Line[i-1]) Mapping[++cnt]=Line[i]; } return;} int Find(int x) {return lower_bound(Mapping+1,Mapping+cnt+1,x)-Mapping;} void DFS(int Now) { if (Now==0) return; dfn[Now]=++cnd;Back[cnd]=Now; DFS(ls[Now]);DFS(rs[Now]); MaxD[Now]=max(MaxD[ls[Now]],max(MaxD[rs[Now]],dfn[Now]));MinD[Now]=dfn[Now]; if (ls[Now]) { q[++cnq]={Val[Now]-1,MaxD[ls[Now]],Now,0,1}; q[++cnq]={Val[Now]-1,MinD[ls[Now]]-1,Now,0,-1}; } if (rs[Now]) { q[++cnq]={Val[Now],MaxD[rs[Now]],Now,1,1}; q[++cnq]={Val[Now],MinD[rs[Now]]-1,Now,1,-1}; }} void Deal() { sort(q+1,q+cnq+1,cmp); int p=0; for (int i=1;i<=cnq;i++) { while (p<q[i].DFN) { p++; Modify(Val[Back[p]],1); } if (q[i].From==0) { Size[q[i].Mark][0]+=Query(q[i].Value)*q[i].Times; } else { Size[q[i].Mark][1]+=(Query(cnt)-Query(q[i].Value))*q[i].Times; } } return ;} int main() { freopen("trade.in","r",stdin); freopen("trade.out","w",stdout); memset(Size,0,sizeof(Size)); scanf("%d",&n); for (int i=1;i<=n;i++) { scanf("%d",&Line[i]); Val[i]=Line[i]; } Discrete();for (int i=1;i<=n;i++) Val[i]=Find(Val[i]); for (int i=1;i<=n;i++) { scanf("%d%d",&ls[i],&rs[i]); } DFS(1); Deal();long long Ans=0; for (int i=1;i<=n;i++) { Ans+=1ll*Size[i][0]*Size[i][1]; } printf("%lld\n",Ans); return 0;}