【题解】UOJ#284 快乐游戏鸡
题目大意
给出一棵有 \(n\) 个节点的树,编号为 \(i\) 的点权为 \(w_i\),在树上通过一条边需要花费时间 \(1\),如果能通过一个点权为 \(w_i\) 的点当且仅当此时的死亡次数大于等于 \(w_i\),否则会立即回到起点并且死亡次数加一。给出 \(q\) 组询问,每组询问给出起点 \(s\) 和终点 \(t\),且 \(s\) 一定是 \(t\) 的父亲,只能往 \(s\) 的子树内走。问最后从 \(s\) 走到 \(t\) 至少需要多长时间(不需要考虑 \(s\),\(t\) 上的点权)。
分析
有个很直觉的思路,直接头铁从 \(s\) 向 \(t\) 莽,离线下来统计答案即可,这样是链的特殊性质分。
但是稍微看下样例就知道这个想法是错的,并且样例启发我们是从 \(s\) 往一些深度较浅的点走,快速积累死亡次数后再去冲 \(t\)。因此我们考虑将 \(s\) 的子树内的点按照深度从浅到深排序,对于同一个深度只需要保留 \(w_i\) 最大的那个点;并且积累的死亡次数刚刚好够通过 \(s->t\) 的路径一定是优的。接下来我们再考虑怎么处理这两个问题(维护 \(s\) 的子树,统计答案)。
假设存在两个点 \(i\),\(j\),并且 \(deep_i<deep_j\),\(w_i>w_j\),那我们一定可以通过 \(i\) 积累到最多 \(w_i\) 的死亡次数,而这个一定比 \(j\) 优,所以我们根本不需要考虑 \(j\)。因此若我们考虑维护一个单调栈,栈内的元素一定是按照 \(w\) 和深度递增的。从深到浅插入点,假设当前插入的点是 \(i\),那么栈内元素的深度一定都大于 \(i\),如果 \(w_i\) 还大于 \(w_{top}\),那么 \(top\) 一定是一个没用的值,直接弹掉。
但是这样要么只能在线 \(O(nq)\) 做,要么就得离线对每个点都开一个栈,空间也不支持。而我们发现 \(fa_i\) 和 \(i\) 存储的栈内实际上所有 \(i\) 子树内的元素都是重复的,大大浪费了空间。所以我们考虑将查询离线下来,维护好 \(i\) 的栈后统计所有 \(s=i\) 的查询的答案,然后 \(i\) 的栈就直接丢给 \(fa_i\) 合并就好。
考虑怎么合并。用启发式合并的思想,显然是把 \(i\) 合并到 \(fa_i\) 时间复杂度更优,并且 \(fa_i\) 可以直接继承它长儿子的栈,时间复杂度就正确了。合并时只会影响 \(fa_i\) 栈内深度小于 \(i\) 中最深深度的元素。把这些元素拿出来,和 \(i\) 按照深度从小到大,类似于归并排序一样把元素一个一个重新插进 \(fa_i\) 的栈。但是这样除非一些释放空间的操作,要不然空间复杂度还是不对。借鉴 hzy 博客 里面说的队列启发式合并,我们考虑将长队列按照 \([dfn_i,dfn_i+siz_i-1]\) 的空间分配。这样合并的时候就可以重复利用空间。
最后考虑怎么统计答案。首先考虑到统计答案的时候我们要在单调栈所对应的树上的点之间跑来跑去,直接暴力走显然时间复杂度不对。考虑相邻两个 \(i\) 和 \(i+1\) 之间的贡献就是 \(deep_{i+1}(w_{i+1}-w_i)\)。这样就可以维护一个从栈底到当前元素的后缀和了。对于点 \(s\),刚刚好能从 \(s\) 到 \(t\) 的死亡次数一定会是这条路径上的点权最大值 \(W\),这个可以用树链剖分+线段树或者树上倍增处理掉。然后在它的单调栈中二分找到最大的满足 \(w_l<=W\) 的 \(l\)。最后的答案就是单调栈之间转移的贡献 \(sum_{top}-sum_l+deep_{top}\times w_{top}\) 再加上让死亡次数从 \(w_l\) 到 \(W\) 的贡献 \((W-w_l)\times deep_{l+1}\),最后由于栈里维护的深度是假装起点为根节点的,每多死一次就会多跑 \(deep_s\),所以最后还要减去 \(deep_s\times w\),再加上死亡次数达到 \(W\) 后从 \(s\) 到 \(t\) 的时间 \(deep_t-deep_s\)。注意特判一下单调栈中根本没有 \(\le W\) 的情况。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n;
int a[N];
int head[N],tot;
struct node{
int nex,to;
}edge[N];
void add(int x,int y){
edge[++tot].nex=head[x];
edge[tot].to=y;
head[x]=tot;
}
int fa[N][25];
int deep[N],maxdep[N];
int son[N];
int maxi[N][25];
void pre(int x,int fx){
deep[x]=deep[fx]+1;
fa[x][0]=fx;
maxi[x][0]=a[fx];
for(int i=1;i<=20;++i){
fa[x][i]=fa[fa[x][i-1]][i-1];
maxi[x][i]=max(maxi[x][i-1],maxi[fa[x][i-1]][i-1]);
}
for(int i=head[x];i;i=edge[i].nex){
int v=edge[i].to;
if(v==fx) continue;
pre(v,x);
if(maxdep[v]>maxdep[son[x]]) son[x]=v;
}
maxdep[x]=maxdep[son[x]]+1;
}
int query(int x,int y){
int nmaxi=0;
for(int i=20;i>=0;--i){
if(deep[fa[y][i]]>deep[x]){
nmaxi=max(nmaxi,maxi[y][i]);
y=fa[y][i];
}
}
return nmaxi;
}
vector<pair<int,int> >que[N];
struct Node{
int deep,w,sum;
}stk[N],ttk[N];
int dfn[N],dn;
int fro[N],tai[N];
void insert(int x,Node k){//insert k into x
while(fro[x]<=tai[x]&&stk[fro[x]].w<=k.w) fro[x]++;
if(fro[x]>tai[x]||stk[fro[x]].deep>k.deep){
stk[fro[x]-1].deep=k.deep;
stk[fro[x]-1].w=k.w;
stk[fro[x]-1].sum=0;
if(fro[x]<=tai[x]){
stk[fro[x]-1].sum=stk[fro[x]].sum+(stk[fro[x]].w-k.w)*stk[fro[x]].deep;
}
fro[x]--;
}
}
int top;
void unionn(int x,int y){//unionn y into x
top=0;
while(fro[x]<=tai[x]&&stk[fro[x]].deep<stk[tai[y]].deep){
ttk[++top]=stk[fro[x]];
fro[x]++;
}
while(top&&fro[y]<=tai[y]){
if(ttk[top].deep>stk[tai[y]].deep){
insert(x,ttk[top]);
top--;
}
else{
insert(x,stk[tai[y]]);
tai[y]--;
}
}
while(fro[y]<=tai[y]){
insert(x,stk[tai[y]]);
tai[y]--;
}
while(top){
insert(x,ttk[top]);
top--;
}
}
int ansi[N];
void solve(int x,int id){
int w=query(x,que[x][id].first);
int l=fro[x]-1,r=tai[x]+1;
while(l+1<r){
int mid=(l+r)>>1;
if(stk[mid].w<=w) l=mid;
else r=mid;
}
int sum=0;
if(stk[fro[x]].w>w) sum=w*stk[fro[x]].deep-deep[x]*w;
else sum=stk[fro[x]].sum-stk[l].sum+stk[fro[x]].deep*stk[fro[x]].w+(w-stk[l].w)*stk[l+1].deep-deep[x]*w;
sum+=deep[que[x][id].first]-deep[x];
ansi[que[x][id].second]=sum;
}
void dfs(int x,int fx){
dfn[x]=++dn;
if(son[x]){
dfs(son[x],x);
fro[x]=fro[son[x]];
tai[x]=tai[son[x]];
}
else{
fro[x]=dn;
tai[x]=dn-1;
}
for(int i=head[x];i;i=edge[i].nex){
int v=edge[i].to;
if(v==fx||v==son[x]) continue;
dfs(v,x);
unionn(x,v);
}
for(int i=0;i<que[x].size();++i) solve(x,i);
insert(x,Node{deep[x],a[x]});
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=2;i<=n;++i){
int x;
cin>>x;
add(x,i);
}
pre(1,0);
int m;
cin>>m;
for(int i=1;i<=m;++i){
int s,t;
cin>>s>>t;
pair<int,int>tmp={t,i};
que[s].push_back(tmp);
}
dfs(1,0);
for(int i=1;i<=m;++i) cout<<ansi[i]<<"\n";
return 0;
}
总结
本题需要分析题目性质得到对于每个点用单调栈维护信息的解法,并需要灵活运用队列启发式合并等启发式算法降低时间复杂度,个人感觉本题还是不错的。