P1008 · OFFICIAL SOLUTION

P1008 商路照影 官方题解

Gioush OJ · P1008 商路照影

商路照影

【题意简述】

给定一棵有根二叉树,第 ii 个节点的权值为 wiw_i。对于每个节点 xx,统计满足 uuxx 的左儿子子树内,vvxx 的右儿子子树内,并且 wu<wx<wvw_u<w_x<w_v 的有序点对 (u,v)(u,v) 数量。

显然,每一个合法点对的中转点 xx 都是唯一的。若记左儿子子树内权值小于 wxw_x 的节点数量为 LxL_x,右儿子子树内权值大于 wxw_x 的节点数量为 RxR_x,那么节点 xx 对答案的贡献就是 LxRxL_xR_x

【数据点 1∼81\sim 81∼8】

这部分 n2000n\leq 2000

对于每个节点 xx,分别遍历其左儿子子树与右儿子子树,统计权值小于和大于 wxw_x 的节点数量即可,时间复杂度为 O(n2)\mathcal{O}(n^2)

【数据点 9∼129\sim 129∼12】

特殊性质保证二叉树按照编号自然连接,因此整棵树的高度为 O(logn)\mathcal{O}(\log n)

考虑对于每个节点维护其子树内所有权值组成的有序序列。得到两个儿子的序列以后,可以通过二分查询出 Lx,RxL_x,R_x,再使用归并排序的方法合并两个儿子的序列,并插入 wxw_x

每个权值在树的每一层至多参与一次归并,总时间复杂度为 O(nlogn)\mathcal{O}(n\log n)

【正解一:按权值扫描】

首先对原树进行一次 DFS,求出每个节点的 DFN 序与子树大小。这样任意一个节点 uu 的子树都对应 DFN 序上的连续区间。

于是,求左儿子子树中权值小于 wxw_x 的节点数量,可以转化为一个二维偏序问题:节点的 DFN 序需要落在一段区间中,权值需要小于给定值。

将所有节点按照权值从小到大排序,并使用树状数组维护已经加入节点的 DFN 序。处理节点 xx 时,树状数组中只保留权值小于 wxw_x 的节点,那么在左儿子子树对应的 DFN 区间上查询区间和,得到的就是 LxL_x

由于题目要求严格小于,所以相同权值的节点必须放在同一组中。对于一组权值相同的节点,应该先完成这一组的所有查询,再将这一组节点的 DFN 序加入树状数组。

同理,将所有节点按照权值从大到小处理,树状数组中只保留权值大于 wxw_x 的节点,在右儿子子树对应的区间上查询即可得到 RxR_x。相同权值仍然需要先查询、后加入。

最后计算 xLxRx\sum_x L_xR_x 即可。

这是一种较自然的二维偏序维护方式:把权值这一维作为扫描顺序,把 DFN 这一维放进树状数组中维护。

排序的时间复杂度为 O(nlogn)\mathcal{O}(n\log n),树状数组操作的总时间复杂度为 O(nlogn)\mathcal{O}(n\log n),空间复杂度为 O(n)\mathcal{O}(n)

【正解二:按 DFN 扫描】

也可以将二维偏序的两个维度交换。

我们仍然先 DFS 得到每个节点的 DFN。设节点 xx 的左儿子为 lxl_x,右儿子为 rxr_x。如果 lxl_x 非空,那么 lxl_x 子树在 DFN 序上对应一段区间

[MinD(lx),MaxD(lx)].[\operatorname{MinD}(l_x),\operatorname{MaxD}(l_x)].

要求左儿子子树内权值小于 wxw_x 的节点数量,即

{u:Dfn(u)[MinD(lx),MaxD(lx)], wu<wx}.\left|\{u:\operatorname{Dfn}(u)\in[\operatorname{MinD}(l_x),\operatorname{MaxD}(l_x)],\ w_u<w_x\}\right|.

将权值离散化后,严格小于 wxw_x 等价于离散化权值不超过 Rank(wx)1\operatorname{Rank}(w_x)-1。于是可以把这次询问拆成两个前缀:

Ask(MaxD(lx),Rank(wx)1)Ask(MinD(lx)1,Rank(wx)1).\operatorname{Ask}(\operatorname{MaxD}(l_x),\operatorname{Rank}(w_x)-1) - \operatorname{Ask}(\operatorname{MinD}(l_x)-1,\operatorname{Rank}(w_x)-1).

其中 Ask(p,v)\operatorname{Ask}(p,v) 表示在 DFN 前缀 [1,p][1,p] 中,权值排名不超过 vv 的节点数量。

右儿子子树内权值大于 wxw_x 的节点数量同理。若总权值排名数为 mm,则

{u:Dfn(u)p, wu>wx}=Ask(p,m)Ask(p,Rank(wx)).\left|\{u:\operatorname{Dfn}(u)\leq p,\ w_u>w_x\}\right| =\operatorname{Ask}(p,m)-\operatorname{Ask}(p,\operatorname{Rank}(w_x)).

因此每个右儿子区间同样可以拆成两个 DFN 前缀。

具体实现时,将所有形如 Ask(p,v)\operatorname{Ask}(p,v) 的询问按照 pp 从小到大排序。然后按 DFN 从小到大扫描节点,每扫到一个节点,就把它的权值排名加入树状数组。此时树状数组维护的是“当前 DFN 前缀中,各权值排名出现了多少次”。处理询问 Ask(p,v)\operatorname{Ask}(p,v) 时,树状数组中恰好已经加入了 DFN 不超过 pp 的节点,查询权值前缀和即可。

代码中的 Q 结构体正是在记录这些前缀询问:

  • DFN 表示前缀右端点 pp
  • Value 表示权值阈值 vv
  • Times 表示区间差分中的 +1+11-1
  • From=0 表示统计左儿子子树内的小于部分;
  • From=1 表示统计右儿子子树内的大于部分。

这一做法与正解一维护的是同一个二维偏序,只是扫描维度从权值换成了 DFN,树状数组中维护的维度也随之从 DFN 换成了权值。

时间复杂度仍为 O(nlogn)\mathcal{O}(n\log n),空间复杂度为 O(n)\mathcal{O}(n)。下面代码采用这种写法。

【参考代码】

#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;}