长链剖分
将较长的链剖出来,通过共用数组的方式来实现优化。
注意!!!
【指针版长链剖分】循环遍历儿子们的答案时,
for(int j=0;j<len[ver[i]];j++)...
而不是(因为申请了长度为 \(len\) 的数组!!)
for(int j=0;j<=len[ver[i]];j++)...
【模板】树上 k 级祖先
支持 \(O(1)\) 查询 \(k\) 级祖先,预处理是 \(O(n\log n)\) 的。
$\texttt{code}$
// Author:A weak man named EricQian
#include<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define Maxn 500005
typedef long long ll;
typedef unsigned int ui;
inline int rd()
{
int x=0;
char ch,t=0;
while(!isdigit(ch = getchar())) t|=ch=='-';
while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
return x=t?-x:x;
}
int n,q,s,tot,root;
ll ans,Last;
int fa[Maxn][22],g[Maxn];
int maxdep[Maxn],dep[Maxn],lson[Maxn],tp[Maxn];
int hea[Maxn],nex[Maxn],ver[Maxn];
vector<int> Up[Maxn],Down[Maxn];
inline ui get(ui x)
{
x ^= x << 13,x ^= x >> 17,x ^= x << 5;
return s = x;
}
inline void add(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
void dfs1(int x)
{
for(int i=hea[x];i;i=nex[i])
{
maxdep[ver[i]]=dep[ver[i]]=dep[x]+1,fa[ver[i]][0]=x;
for(int j=1;j<=20;j++) fa[ver[i]][j]=fa[fa[ver[i]][j-1]][j-1];
dfs1(ver[i]);
if(maxdep[ver[i]]>maxdep[lson[x]]) lson[x]=ver[i],maxdep[x]=maxdep[ver[i]];
}
}
void dfs2(int x,int T)
{
tp[x]=T;
if(x==T)
{
for(int i=maxdep[x]-dep[x],tmp=x;i>=0;i--)
Up[x].push_back(tmp),tmp=fa[tmp][0];
for(int i=maxdep[x]-dep[x],tmp=x;i>=0;i--)
Down[x].push_back(tmp),tmp=lson[tmp];
}
if(lson[x]) dfs2(lson[x],T);
for(int i=hea[x];i;i=nex[i]) if(ver[i]!=lson[x]) dfs2(ver[i],ver[i]);
}
ll query(int x,int k)
{
if(!k) return x;
x=fa[x][g[k]],k-=(1<<g[k]);
k-=dep[x]-dep[tp[x]];
// 跳到链顶端,若有,则继续向上跳;若无,说明在这条链中
x=tp[x];
return (k>=0)?Up[x][k]:Down[x][-k];
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n=rd(),q=rd(),s=rd();
for(int i=1,fa;i<=n;i++) fa=rd(),add(fa,i),root=(!fa)?i:root;
g[0]=-1; for(int i=1;i<=n;i++) g[i]=g[i>>1]+1;
dep[root]=1,dfs1(root),dfs2(root,root);
for(int i=1,x,k;i<=q;i++)
{
x=(get(s)^Last)%n+1;
k=(get(s)^Last)%dep[x];
Last=query(x,k);
ans^=1ll*i*Last;
}
printf("%lld\n",ans);
//fclose(stdin);
//fclose(stdout);
return 0;
}
然而大多数情况下,长链剖分都是用指针实现的,下面就是一个比较简单的例子。
CF1009F Dominant Indices
我们发现在同一条长链中,我们不必每次将整个数组转移,只用将长儿子的深度 \(+1\) 边可以得到父亲在这个儿子上的答案,因此采用 数组公用空间 来实现 \(+1\) 的操作。
具体来说就是用指针实现数组的移位。
比如我们进行了如下的定义与赋值:
int *f[Maxn],tmp[Maxn],*o=tmp;
o+=1,f[1]=o;
那么,我们可以直接访问 f[1][0]
,但是 f[1][0]
实际上存储在了 tmp[1]
的位置。
$\texttt{code}$
// Author:A weak man named EricQian
#include<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define Maxn 1000005
typedef long long ll;
inline int rd()
{
int x=0;
char ch,t=0;
while(!isdigit(ch = getchar())) t|=ch=='-';
while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
return x=t?-x:x;
}
int n,tot;
int ans[Maxn],lson[Maxn],len[Maxn]; // 从下往上记录,表示这个点能往下走的距离。
int hea[Maxn],nex[Maxn<<1],ver[Maxn<<1];
int *f[Maxn],tmp[Maxn],*o=tmp;
inline void add(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
void dfs1(int x,int fa)
{
for(int i=hea[x];i;i=nex[i])
{
if(ver[i]==fa) continue;
dfs1(ver[i],x);
if(len[ver[i]]>len[lson[x]]) lson[x]=ver[i];
}
len[x]=len[lson[x]]+1;
}
void dfs2(int x,int fa)
{
f[x][0]=1;
if(lson[x]) f[lson[x]]=f[x]+1,dfs2(lson[x],x),ans[x]=ans[lson[x]]+1;
if(f[x][ans[x]]<=1) ans[x]=0;
for(int i=hea[x];i;i=nex[i])
{
if(ver[i]==fa || ver[i]==lson[x]) continue;
f[ver[i]]=o,o+=len[ver[i]];
dfs2(ver[i],x);
for(int j=1;j<=len[ver[i]];j++)
{
f[x][j]+=f[ver[i]][j-1];
if(f[x][j]>f[x][ans[x]] || (f[x][j]==f[x][ans[x]] && j<ans[x]))
ans[x]=j;
}
}
}
int main()
{
//ios::sync_with_stdio(false); cin.tie(0);
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n=rd();
for(int i=1,x,y;i<n;i++) x=rd(),y=rd(),add(x,y),add(y,x);
dfs1(1,0);
f[1]=o,o+=len[1];
dfs2(1,0);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
//fclose(stdin);
//fclose(stdout);
return 0;
}
再来一个较为复杂的例子。
P5904 [POI2014]HOT-Hotels 加强版
这一题的转移方程就比较难以想到,我们设:
-
\(f(i,j)\) 表示在 \(i\) 为根的子树中,距离 \(i\) 为 \(j\) 的子树有多少个
-
\(g(i,j)\) 表示在 \(i\) 为根的子树中,满足 \(dist(i,\operatorname{lca}(x,y))+j=dist(x,\operatorname{lca}(x,y))=dist(y,\operatorname{lca}(x,y))\) 的点对有多少个。
有这样的转移:
\[ans \leftarrow g_{i, 0}
\]
\[ans \leftarrow \sum_{x,y \in \operatorname{son}(i), x \ne y} f_{x,j-1} \times g_{y,j+1}
\]
\[g_{i,j} \leftarrow \sum_{x,y \in \operatorname{son}(i), x \ne y} f_{x,j-1} \times f_{y,j-1}
\]
\[g_{i,j} \leftarrow \sum_{x \in \operatorname{son}(i)} g_{x, j+1}
\]
\[f_{i,j} \leftarrow \sum_{x \in \operatorname{son}(i)} f_{x, j-1}
\]
因此我们可以用 \(f(i,j)\) 的前缀和优化,\(g(i,j)\) 的后缀和优化来实现 \(O(n)\) 之内的转移。
又发现这个转移方程都是以深度为下标的,所以可以利用上指针优化的长链剖分实现均摊 \(O(n)\) 。
$\texttt{code}$
#define infll 0x7f7f7f7f7f7f7f7f
#define inf 0x3f3f3f3f
#define Maxn 500005
typedef long long ll;
inline int rd()
{
int x=0;
char ch,t=0;
while(!isdigit(ch = getchar())) t|=ch=='-';
while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
return x=t?-x:x;
}
int n,tot;
int len[Maxn],lson[Maxn],hea[Maxn],nex[Maxn<<1],ver[Maxn<<1];
ll ans,*f[Maxn],*g[Maxn],tmp[Maxn<<1],*o=tmp;
inline void add(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
void dfs1(int x,int fa)
{
for(int i=hea[x];i;i=nex[i])
{
if(ver[i]==fa) continue;
dfs1(ver[i],x);
if(len[ver[i]]>len[lson[x]]) lson[x]=ver[i];
}
len[x]=len[lson[x]]+1;
}
void dfs2(int x,int fa)
{
if(lson[x]) f[lson[x]]=f[x]+1,g[lson[x]]=g[x]-1,dfs2(lson[x],x);
f[x][0]=1,ans+=g[x][0];
for(int i=hea[x];i;i=nex[i])
{
if(ver[i]==fa || ver[i]==lson[x]) continue;
f[ver[i]]=o,o+=len[ver[i]]<<1;
g[ver[i]]=o,o+=len[ver[i]]<<1;
dfs2(ver[i],x);
for(int j=0;j<=len[ver[i]];j++)
{
if(j) ans+=f[x][j-1]*g[ver[i]][j];
ans+=g[x][j+1]*f[ver[i]][j]; // 从前、后利用前缀和计算
}
for(int j=0;j<=len[ver[i]];j++)
{
g[x][j+1]+=f[x][j+1]*f[ver[i]][j]; // 利用前缀和累加答案
if(j) g[x][j-1]+=g[ver[i]][j]; // 更新从后往前的前缀和
f[x][j+1]+=f[ver[i]][j]; // 更新从前往后的前缀和
}
}
}
int main()
{
n=rd();
for(int i=1,x,y;i<n;i++) x=rd(),y=rd(),add(x,y),add(y,x);
dfs1(1,0);
f[1]=o,o+=len[1]<<1;
g[1]=o,o+=len[1]<<1;
dfs2(1,0);
printf("%lld\n",ans);
return 0;
}