题解:P5399 [Ynoi2018] 駄作
拼尽全力无法战胜最优解,不过应该取得了最大点最快的最优解(2.11s),写篇题解记录一下卡常技巧。
思路
树分块
目的:将树分成
做法:
后序遍历整个树,到一个点的时候遍历它的儿子,将当前儿子前的一段连通块加入当且仅当:
- 当前连通块大小
\ge B ; - 当前连通块存在一个下界点且下一个点子树内也存在一个下界点(为了保证下界点唯一);
- 当前儿子是最后一个儿子且之前已经分过至少一个连通块了且当前连通块存在一个下界点或者当前点是根。
如果分过至少一个连通块,则将当前点标记为下界点。
具体实现的时候需要先倒序遍历所有儿子并使用一个栈记录还没分连通块的点(因为需要知道下一个点的节点情况所以需要先遍历,因为使用栈所以倒序遍历)。需要一个变量记录当前连通块的下界点,一个布尔变量记录是否分过至少一个连通块。
代码:
int nxt[100010];
int siz[100010];
int down[100010];
int tot=0,st[100010];
int ttot=0,rt[100010],id[100010];
int fa2[100010],dep2[100010];
void build(int u)
{
int lst=0;
for(int i=e1[u].size()-1; i>=0; --i)
{
int v=e1[u][i];
build(v);
nxt[v]=lst,lst=v;
}
int nowrt=0; bool vis=0;
for(int v:e1[u])
{
siz[u]+=siz[v];
if(down[v]!=0) nowrt=down[v];
if(siz[u]>=B || (nowrt!=0 && down[nxt[v]]!=0) || (nxt[v]==0 && ((vis && nowrt!=0) || fa1[u]==0)))
{
rt[++ttot]=nowrt,fa2[ttot]=u;
while(siz[u]!=0) id[st[tot]]=ttot,--tot,--siz[u];
nowrt=0,vis=1;
}
}
down[u]=(vis?u:nowrt);
++siz[u],st[++tot]=u;
}
rt[0]=1,fa2[0]=ttot+1;
for(int i=ttot; i>=1; --i) fa2[i]=id[fa2[i]],dep2[i]=dep2[fa2[i]]+1;
连通块数量和大小量级证明:
- 大小:一个点子树内剩下的点个数一定
<B ,因此当siz_u+siz_v \ge B 时,siz_u,siz_v <B ,所以siz_u+siz_v \le 2B ; - 数量:首先,如果一个点子树内有界点,则这个点子树大小
\ge B 。考虑将点的贡献分成两类:一类是子树内存在两个及以上有界点的儿子,贡献有界点的儿子个数;另一类是任意时刻siz \ge B 时,贡献1 。对于第二类贡献,最多使数量增多\dfrac{n}{B} 。对于第一类贡献,可以将它拆成有界点的儿子个数-1 加上子树内存在两个及以上有界点的儿子的点的个数。前者考虑进行先序遍历,最初有一个节点,大小为B (大小的意思是,我们可以证明这种条件下这些点的个数大于“大小”),之后每创造一个儿子会使用一个贡献创造一个大小为B 的节点,这样递归下去构造就可以得到贡献个数乘上B \le n ,因此贡献个数\le \dfrac{n}{B} 。后者考虑这样构造出来的点的个数也\le \dfrac{n}{B} ,所以最多再增加\dfrac{n}{B} 。这三类贡献加起来便可以得到上界\dfrac{3n}{B} 。而且这个界是可以达到的:构造一个长度为\dfrac{n}{B} 的链,每个点下面再挂一个大小为\dfrac{n}{B} 的子树,此时三类贡献都可以达到最大值。
上述思路是我看题解没看明白后自己发明的,可能不是最优的。
这道题
考虑对块内和两个块之间的答案分别统计。
块内:对于询问的点所在的块,可以计算每条边的贡献:子树内满足
块外:将块外的答案分成两部分:这个块内的所有点汇合到某个界点的距离乘上界点上面或者下面满足条件的点的个数,以及两个块之间的边(体现为一个其他块的上界点与下界点的距离)乘上它上下的满足条件的点的个数。因此我们只需要对每个块预处理出
具体实现的时候可以先逐块处理求出块内的所有贡献以及
卡常技巧
这里介绍一个优化效果非常好的卡常技巧。
考虑将树重新编号,使得每一个树分块的连通块内部编号连续,且整体仍然满足 dfs 序。这是容易达到的。在递归过程中,先遍历与它属于同一个块的所有儿子,再遍历与它不属于同一个块的所有儿子。两次遍历的过程中都需要将同一个块的一段先提出来,最后遍历有界点的儿子。
这样的话几乎所有地方就都访问连续了。而且还有一个更加美好的优化:考虑使用 dfs 序求 lca(详见 Alex_wei 的博客),那么对于
在块外的贡献需要求出
代码:
int cnt=0,idd[100010];
void dfs(int u)
{
idd[u]=++cnt;
for(int i=0; i<e1[u].size(); ++i)
{
if(id[e1[u][i]]!=id[u]) continue;
int now=i,wz=-1;
while(now<e1[u].size() && id[e1[u][now]]==id[e1[u][i]])
{
if(down[e1[u][now]]!=0) wz=now;
++now;
}
--now;
for(int j=i; j<=now; ++j) if(j!=wz) dfs(e1[u][j]);
if(wz!=-1) dfs(e1[u][wz]);
i=now;
}
for(int i=0; i<e1[u].size(); ++i)
{
if(id[e1[u][i]]==id[u]) continue;
int now=i,wz=-1;
while(now<e1[u].size() && id[e1[u][now]]==id[e1[u][i]])
{
if(down[e1[u][now]]!=0) wz=now;
++now;
}
--now;
for(int j=i; j<=now; ++j) if(j!=wz) dfs(e1[u][j]);
if(wz!=-1) dfs(e1[u][wz]);
i=now;
}
}
for(int i=0; i<=ttot; ++i)
{
if(rt[i]==0) memset(diis[i],0x3f,sizeof(diis[i]));
else
{
diis[i][rt[i]]=0;
int lca=1e9;
for(int j=rt[i]+1; j<=n; ++j) lca=min(lca,fa1[j]),diis[i][j]=dis[rt[i]]+dis[j]-2*dis[lca];
lca=1e9;
for(int j=rt[i]-1; j>=1; --j) lca=min(lca,fa1[j+1]),diis[i][j]=dis[rt[i]]+dis[j]-2*dis[lca];
}
}
memset(diis[ttot+1],0x3f,sizeof(diis[ttot+1]));
for(int i=1; i<=n; ++i)
{
for(int j=0; j<=ttot+1; ++j) diss[i][j]=diis[j][i];
}
提交记录