「ZJOI2017」线段树 树剖写法
「ZJOI2017」线段树
给一个无脑暴力的写法。想法十分简单,就是有亿点点难写。
首先考虑将问题特殊化。
假设是让给出若干个点,求其到某一个点的距离之和。
那么答案是:
注意到只要快速算出 \(dep_{lca}\) 之和即可。
考虑进行如下操作:
对于每个 \(v\),使 \(v\) 到根节点路径上所有边的边权 \(+1\)。
显然最后对 \(u\) 求出 \(u\) 到根节点路径上所有边的边权和就是 \(dep_{lca}\) 之和。
那么就是一个链加链和问题。可以用树剖解决。
那么再考虑将问题泛化。
如果询问的 \(l\) 端点永远是 \(1\)。
那么我们只要离线询问,然后维护 \(S_{[1,i]}\) 即可。维护了 \(S_{[1,i]}\) 之后问题就变成上面那个问题了。
现在问题是如何维护 \(S_{[1,i]}\)。
需要一个简单的结论。
结论 \(1\) ,对于一个点 \(u\),设其管辖的区间为 \([l_u,r_u]\)。那么对于一个点 \(v\) 满足 \([l_v,r_v]\) 包含了 \([l_u,r_u]\),\(v\) 一定是 \(u\) 的祖先。
考虑维护一个栈,维护了 \(S_{[1,i]}\)。
即有 \([L_{S_1},R_{S_1}]\cup\dots\cup[L_{S_k},R_{S_k}]=[1,i]\)。
且栈中任意两元素所管辖的区间交为空集。
所以我们找到管辖 \([i,i]\) 区间的点 \(u\)。然后一直向上爬它的父亲,直到 \(r_{fa_u}>i\) 为止。那么这个点就是满足其管辖区间右端点为 \(i\),且区间长度最大的点,然后我们一直弹出栈顶直到满足栈顶与 \(u\) 管辖的区间没有交集为止。
其正确性显然。
那么这个问题就解决了。
然后再将问题泛化。
现在我们要求的问题与原问题相同。
容易想到利用上个问题的减法加上差分来解决这个问题。
但是注意到,我们直接利用差分将栈中所有包含非询问区间的点的贡献减去后,并不是正确的答案。而是少了一部分。(当然也有可能减去后剩下的点的并集刚好是询问区间,那么就不用算接下来的了,也就是点不会被拆的情况)
即栈中可能有一个节点 \(p\) 管辖的区间满足 \(l_p<l_q\le r_p\)。那么我们肯定是要减去这个节点的贡献的,但是 \([l_q,r_p]\) 这个区间的贡献就被我们忽略了。
考虑找个方法加上这个区间的贡献。
注意到这样的节点 \(p\) 只会存在一个,也就是只有一个点 \(p\) 会被拆成若干个点。
我们可以考虑这若干个点的位置。
显然这若干个点只可能在 \(p\) 的子树内。
下面用一个区间来表示恰好管辖这个区间的点。
考虑几个显而易见的结论。
结论 \(2\) 。
我们思考当什么时候节点 \(u\) 管 辖的区间是满足其管辖的区间是询问的区间的子集。
即其子树下所有管辖区间都是询问区间的子集的条件下,节点 \(u\) 管辖的区间是询问区间的子集。
(看起来好像很没有用,但是实际上非常有用)
结论 \(3\)。
再考虑什么时候节点 \(u\) 是能对询问区间做出贡献的。
即其父亲不能对询问区间做出贡献。
那么结合一下。
一个节点 \(u\) 能做出贡献当且仅当其父亲的另一个儿子所在子树内存在节点管辖的区间不是询问区间的子集。
那么我们找到所有点满足其管辖的区间 \([a,a]\in[l_p,l_q-1]\)。我们将所有满足这样条件的点到 \(p\) 的路径上的点全部覆盖一遍(也可以理解成染色)。
那么 \(p\) 的子树内剩下的未被覆盖的点一定是若干个子树。那么有贡献的点就是若干个子树的根。
由结论 \(1\) 可知这是正确的。
我们要快速得到这若干个子树的根。
然后再考虑一个结论。
结论 \(4\)
然后我们注意到由于这是一棵线段树,那么对于 \([i+k,i+k]\) 的所有祖先,其一定是 \([i,i]\) 的祖先或者 位于\([i,i]\) 某个祖先的右子树内。
虽然看起来结论很多,但其实都是显而易见的东西。
结合一下结论 \(2,3,4\) 我们可以发现,我们要求的点不是 \([a_p,a_p]\)(\(a_p\) 表示任意一个处于 \([l_p,l_q-1]\) 内的点) 的祖先,但是得是 \([a_q,a_q]\)(\(a_q\) 表示任意一个处于 \([ l_q,r_p ]\) 内的点) 的祖先。
然后考虑一下结论四,我们可以发现,上述那个方法可以被优化了。
我们只需要将找到最右端的 \(a_p\) 即 \(l_q-1\)。然后将 \([l_q-1,l_q-1]\) 到 \([l_p,l_q-1]\) 路径上的点全部覆盖。
然后忽略被覆盖点的左子树,因为其左子树中肯定有点管辖的区间不是询问区间的子集。(根据结论 \(1,4\) 可得。)
那么没有被覆盖的产生的子树的根就是 \([l_q-1,l_q-1]\) 到 \([l_p,l_q-1]\) **这条链上的所有不在这条链上的右儿子 **。(是右儿子不是右子树)
简单来说要同时满足两个条件:
- 不在这条链上。
- 是这条链上某个点的右儿子。
那么这若干个点的位置就被我们求得了。
现在要求的是如何求出这若干个点到询问的点 \(u_q\) 的距离。
依旧是考虑利用深度计算,我们先通过倍增跳到一个点 \(t\),满足 \(t\) 在这条链(或者这条链链首的祖先上,即 \([l_p,l_q-1]\) 的祖先)上,且 \(t\) 是 \(u_q\) 的祖先中距离 \(u_q\) 最小的一个。
那么在 \(t\) 之下(深度比它大)的有贡献的点和它的 \(lca\) 就是 \(t\)。\(t\) 之上有贡献的点和他的 \(lca\) 就是有贡献点的父亲(他肯定在链上)。
分段之后我们可以预处理出 \(cnt_i,v_i\) 。
分别表示,以 \(fa_i\) 为链尾,根为链首的链上所有点的不在链上的右儿子(其实就是有贡献的点)的个数之和与深度之和。
然后根据 \(t,cnt_i,v_i\)。我们就可以通过分类讨论的方法在 \(O(\log n)\) (瓶颈在倍增跳点)内算出这类点的贡献。
注意分类的细节。
在这里特别提出一种情况,就是 \(t\) 的右儿子是有贡献的点(就是它右儿子不在链上的意思),而且 \(u_q\) 在 \(t\) 的右子树内。那么这个时候它们的 \(lca\) 应该是 \(t\) 的右儿子而不是 \(t\) 。
忽视这个细节就会 \(100->20\)。(还我 \(80\) 分)
瓶颈在树剖,复杂度为 \(O(n\log n\log n)\)
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = 4e5+5;
int mi[MAXN],n,m,le[MAXN],ri[MAXN],ls[MAXN],rs[MAXN],node;
int lg[MAXN],f[21][MAXN],tot,dep[MAXN],pos[MAXN],st[MAXN],top;
int siz[MAXN],dfn[MAXN],ttd,tt[MAXN],son[MAXN];
ll v[MAXN],cnt[MAXN],Ans[MAXN],val;
struct Ques
{
int u,l,id;
};
vector <Ques> Qu[MAXN];
struct Add
{
int u,id;
};
vector <Add> Ad[MAXN];
struct Tree
{
ll t[MAXN<<2],tg[MAXN<<2];
Tree(){memset(t,0,sizeof t);memset(tg,0,sizeof tg);}
void clear(){memset(t,0,sizeof t);memset(tg,0,sizeof tg);}
void pu(int k) {t[k]=t[(k<<1)]+t[(k<<1|1)];}
void pd(int k,int l,int r)
{
if(!tg[k]) return ;
int mid=l+r>>1;
tg[(k<<1)]+=tg[k];tg[(k<<1|1)]+=tg[k];
t[(k<<1)]+=tg[k]*(mid-l+1);t[(k<<1|1)]+=tg[k]*(r-mid);
tg[k]=0;
}
void upd(int le,int ri,int k,int l,int r,ll x)
{
if(le<=l&&r<=ri)
{
t[k]+=x*(r-l+1);tg[k]+=x;
return ;
}
int mid=l+r>>1;pd(k,l,r);
if(le<=mid) upd(le,ri,(k<<1),l,mid,x);
if(ri>mid) upd(le,ri,(k<<1|1),mid+1,r,x);
pu(k);
}
ll que(int le,int ri,int k,int l,int r)
{
if(le<=l&&r<=ri) return t[k];
int mid=l+r>>1;ll ans=0;pd(k,l,r);
if(le<=mid) ans+=que(le,ri,(k<<1),l,mid);
if(ri>mid) ans+=que(le,ri,(k<<1|1),mid+1,r);
return ans;
}
}T;//线段树板板
bool in(int x,int y)
{
return le[x]<=le[y]&&ri[y]<=ri[x];
}
void predfs(int &k,int fa,int l,int r,int d,bool rep)
{
if(!k) k=++node;
cnt[k]=cnt[fa]+(rep?0:1);v[k]=v[fa]+(rep?0:d);
le[k]=l;ri[k]=r;dep[k]=d;
f[0][k]=fa;
for(int i=1;i<=lg[d];++i)
f[i][k]=f[i-1][f[i-1][k]];
if(l==r)
{
pos[l]=k;
return ;
}
++tot;int mid=mi[tot];
predfs(ls[k],k,l,mid,d+1,0);
predfs(rs[k],k,mid+1,r,d+1,1);
}//预处理,没什么好讲的。
void dfs1(int p,int fa)
{
siz[p]=1;
if(ls[p])
{
dfs1(ls[p],p),siz[p]+=siz[ls[p]];
if(siz[ls[p]]>siz[son[p]]) son[p]=ls[p];
}
if(rs[p])
{
dfs1(rs[p],p),siz[p]+=siz[rs[p]];
if(siz[rs[p]]>siz[son[p]]) son[p]=rs[p];
}
}
void dfs2(int p,int fa,int t)
{
tt[p]=t;dfn[p]=++ttd;
if(son[p]) dfs2(son[p],p,t);
if(ls[p]&&son[p]!=ls[p]) dfs2(ls[p],p,ls[p]);
if(rs[p]&&son[p]!=rs[p]) dfs2(rs[p],p,rs[p]);
}
void U(int x,int v){while(x) T.upd(dfn[tt[x]],dfn[x],1,1,node,v),x=f[0][tt[x]];}
ll Q(int x){ll r=-T.que(1,1,1,1,node);while(x) r+=T.que(dfn[tt[x]],dfn[x],1,1,node),x=f[0][tt[x]];return r;}
//由于树剖将边权化为点权了,所以 1 的点权应该是没有任何意义的。
ll calc(int u,int x,int y)//x为链尾,y为链首
{
ll d=dep[u];
bool step=0;
for(int i=lg[dep[u]]-1;i>=0;--i) if(f[i][u]&&!in(f[i][u],x)) u=f[i][u];
if(!in(u,x)) u=f[0][u],step=1;
//向上跳的过程,注意有可能本来 u 就在链上,然后记录一下 u 是不是跳上来的
ll r=0;
r+=v[x]-v[u]+(cnt[x]-cnt[u])*d-2ll*(cnt[x]-cnt[u])*dep[u];
r+=v[u]+cnt[u]*d-2ll*(v[u]-cnt[u]);
if(le[rs[u]]>le[x]&&step&&in(y,u)) r-=2;
//如果 u 不是本来就在链上然后跳到了 t (这里的 u 存的值实际上已经是 t了)
//那么说明 u 只能是 t 的右子树中的一部分 (因为 u 的左儿子)
//此时就要特判一下,因为上述公式是把 lca 当 t 来算的,实际上是 t 的右儿子。
if(in(u,y))
{
r-=v[y]-v[u]+(cnt[y]-cnt[u])*d-2ll*(cnt[y]-cnt[u])*dep[u];
r-=v[u]+cnt[u]*d-2ll*(v[u]-cnt[u]);
}
else r-=v[y]+cnt[y]*d-2ll*(v[y]-cnt[y]);
//减去链首以上的贡献部分
return r;
}
void perform(Ques q)
{
int l=1,r=top,p=0;
while(l<=r)
{
int mid=l+r>>1;
if(le[st[mid]]<=q.l-1) p=mid,l=mid+1;
else r=mid-1;
}
//二分出栈内不合法贡献的点所构成区间的右端点
Ans[q.id]+=1ll*top*dep[q.u]+val-2ll*Q(q.u);
Ad[ri[st[p]]].push_back(Add{q.u,q.id});//存起来,到时候减去
if(ri[st[p]]!=q.l-1) Ans[q.id]+=calc(q.u,pos[q.l-1],st[p]);
//如果点会被拆就计算。
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i) lg[i]=lg[i-1]+(1<<lg[i-1]==i);
for(int i=1;i<n;++i) scanf("%d",&mi[i]);
scanf("%d",&m);
for(int i=1;i<=m;++i)
{
int u,l,r;scanf("%d %d %d",&u,&l,&r);
Qu[r].push_back(Ques{u,l,i});
}
int rt=0;
predfs(rt,0,1,n,0,1);
dfs1(rt,0);dfs2(rt,0,rt);
for(int i=1;i<=n;++i)
{
int u=pos[i];
while(ri[f[0][u]]==i) u=f[0][u];
while(top&&in(u,st[top]))
{
U(st[top],-1);
val-=dep[st[top]];
--top;
}
st[++top]=u;U(u,1);val+=dep[u];
for(Ques q:Qu[i]) perform(q);
}
//接下来做第二次,目的是减去栈中前面不合法的贡献
T.clear();top=0;val=0;
for(int i=1;i<=n;++i)
{
int u=pos[i];
while(ri[f[0][u]]==i) u=f[0][u];
while(top&&in(u,st[top]))
{
U(st[top],-1);
val-=dep[st[top]];
--top;
}
st[++top]=u;U(u,1);val+=dep[u];
for(Add q:Ad[i]) Ans[q.id]-=1ll*top*dep[q.u]+val-2ll*Q(q.u);
}
for(int i=1;i<=m;++i) printf("%lld\n",Ans[i]);
return 0;
}