【算法简介】
长链剖分的思想很简单,和树剖基本一致,就是把维护的子树节点最多改成了子树深度最大
这种数据结构一般用于优化动态规划(一般涉及了长度的dp)
【习题】
1.P3899 [湖南集训]谈笑风生
【题意】
求树上的三个点构成的点对(a,b,c),给定a求满足a和b是c的祖先,a和b的距离不超过给定的k
【分析】
分类讨论
1.当b比a高时,c点的可能取值有siz[a]-1个,b的取值有min(dep[b],dep[a],k)种,相乘即可
2.当b比a低时,c点的可能取值有siz[b]-1个,b的取值为min(dep[叶子]-dep[a],k),也是相乘
第一部分的贡献很好计算
计算第二部分贡献,发现每个节点的贡献可以赋给长儿子很多,所以用到了长链剖分来优化
维护一个后缀和,计算每个点的后缀和贡献
从链的最深点开始的点,第二部分贡献加上差分的和即可
注意实现过程中,因为要从父亲节点继承信息,所以开的数组要用指针版,方便赋值
【代码】
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
int head[maxn],n,m,tot;
ll siz[maxn],ans[maxn],tmp[maxn],*s[maxn],*temp=tmp;
struct edge
{
int to,nxt;
}e[maxn<<1];
void add(int x,int y)
{
e[++tot].to=y; e[tot].nxt=head[x]; head[x]=tot;
}
struct query
{
int no,z;
};
vector <query> q[maxn];
int dep[maxn],son[maxn],md[maxn];
void dfs1(int x,int fa)
{
dep[x]=md[x]=dep[fa]+1; siz[x]=1;
for(int i=head[x];i;i=e[i].nxt)
{
int to=e[i].to;
if(to==fa) continue;
dfs1(to,x);
siz[x]+=siz[to];
if(md[to]>md[son[x]]) son[x]=to;
}
if(son[x]) md[x]=md[son[x]];
}
void dfs(int x,int fa)
{
s[x][0]=siz[x]-1;
if(son[x]) s[son[x]]=s[x]+1,dfs(son[x],x),s[x][0]+=s[son[x]][0];
for(int i=head[x];i;i=e[i].nxt)
{
int to=e[i].to;
if(to==fa || to==son[x]) continue;
s[to]=temp; temp+=md[to]-dep[to]+1;
dfs(to,x);
for(int j=0;j<=md[to]-dep[to];j++) s[x][j+1]+=s[to][j];
s[x][0]+=s[to][0];
}
for(int i=0;i<q[x].size();i++)
{
int no=q[x][i].no,y=q[x][i].z;
ans[no]+=1LL*(siz[x]-1)*min(dep[x]-1,y);
ans[no]+=s[x][0]-siz[x]+1;
if(y<md[x]-dep[x]) ans[no]-=s[x][y+1];
}
}
int main()
{
freopen("longchain.in","r",stdin);
freopen("longchain.out","w",stdout);
scanf("%d%d",&n,&m);
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
q[x].push_back((query){i,y});
}
dfs1(1,0);
s[1]=temp; temp+=md[1];
dfs(1,0);
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
2.CF1009F Dominant Indices
【题意】
设 d(u,x) 为 u 子树中到 u 距离为 x 的节点数。对于每个点,求一个最小的 k,使得 d(u,k)最大。
【分析】
设计dp:
f[i][j]表示i子树中到i 的距离为j 的点的个数
在dp转移的时候,先做长儿子,每次处理完之后,直接把长儿子的信息向后错一位放到当前的f数组中,然后再把其他子树与当前暴力计算
我们在做dp的时候只对链的顶点申请内存,这样节省了很多空间,只需要开能放下一条链的长度即可O(N)
对于u的长儿子v,给v 的内存分配到f[u]+1上 ,即f[v=son[u]]=f[u]+1
然后去计算其他儿子的答案,对于u的非长儿子,为他申请内存f[v]=now , now+=dep[u],大小为v为顶端的长链长度
在dfs(v)之后,暴力合并信息即可
void dfs2(int u,int fa)
{
f[u][0]=1; // 先算上自己
if (son[u])
{
f[son[u]]=f[u]+1; // 共享内存,这样一步之后,f[son[u]][dep]会被自动保存到f[u][dep-1]
dfs2(son[u],u);
}
for (int e=head[u];e;e=nex[e])
{
int v=tail[e];
if (v==son[u] || v==fa) continue;
f[v]=now;now+=dep[v]; // 为 v 节点申请内存,大小等于以 v 为顶端的长链的长度
dfs2(v,u);
for (int i=1;i<=dep[v];i++)
{
f[u][i]+=f[v][i-1]; //暴力合并
}
}
}
【代码】
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
int head[maxn],n,m,tot;
ll siz[maxn],ans[maxn],tmp[maxn],*s[maxn],*temp=tmp;
struct edge
{
int to,nxt;
}e[maxn<<1];
void add(int x,int y)
{
e[++tot].to=y; e[tot].nxt=head[x]; head[x]=tot;
}
struct query
{
int no,z;
};
vector <query> q[maxn];
int dep[maxn],son[maxn],md[maxn];
void dfs1(int x,int fa)
{
dep[x]=md[x]=dep[fa]+1; siz[x]=1;
for(int i=head[x];i;i=e[i].nxt)
{
int to=e[i].to;
if(to==fa) continue;
dfs1(to,x);
siz[x]+=siz[to];
if(md[to]>md[son[x]]) son[x]=to;
}
if(son[x]) md[x]=md[son[x]];
}
void dfs(int x,int fa)
{
s[x][0]=siz[x]-1;
if(son[x]) s[son[x]]=s[x]+1,dfs(son[x],x),s[x][0]+=s[son[x]][0];
for(int i=head[x];i;i=e[i].nxt)
{
int to=e[i].to;
if(to==fa || to==son[x]) continue;
s[to]=temp; temp+=md[to]-dep[to]+1;
dfs(to,x);
for(int j=0;j<=md[to]-dep[to];j++) s[x][j+1]+=s[to][j];
s[x][0]+=s[to][0];
}
for(int i=0;i<q[x].size();i++)
{
int no=q[x][i].no,y=q[x][i].z;
ans[no]+=1LL*(siz[x]-1)*min(dep[x]-1,y);
ans[no]+=s[x][0]-siz[x]+1;
if(y<md[x]-dep[x]) ans[no]-=s[x][y+1];
}
}
int main()
{
freopen("longchain.in","r",stdin);
freopen("longchain.out","w",stdout);
scanf("%d%d",&n,&m);
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
q[x].push_back((query){i,y});
}
dfs1(1,0);
s[1]=temp; temp+=md[1];
dfs(1,0);
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}