P11189 「KDOI-10」水杯降温 题解
Update on 2024.10.15:
Update on 2024.12.10:刚刚了解到使用 align* 而非 align 可以去除公式后的编号,为优化读者阅读体验故作出修改。辛苦管理员了!
Update on 2025.7.13:虽然笔者已经退役但是我咋刚刚注意到文章所有左右引号都用反了啊,在此改正。
很多人赛时考虑过“先做子树加操作,再去判定链减的合法性”的思路,但在中途改变了想法。故该题解旨在说明该思路的可行性。
做法可能相对复杂,但笔者相信其亦具有一定的参考性。
基本定义
下将“链减”操作称为 A 操作,“子树加”操作称为 B 操作。
下记
记
确定大体思路
可以考虑先进行一种操作,再去判断:进行完这一种操作后的局面是否能使用另一种操作,来使局面变得合法。
讨论两种思路:
- 思路一:若一个局面在只使用 A 操作下可以变得合法,则需要满足:
- 条件一:
\forall i,a_i\ge 0 ; - 条件二:
\forall i\notin \mathbb{LEAF},a_i=\sum_{j\in e_i} a_j .
- 条件一:
- 思路二:若一个局面在只使用 B 操作下可以变得合法,则需要满足:
- 条件一:
\forall i,a_i\le 0 ; - 条件二:
\forall i\ne 1,a_i\le a_{fa_i} .
- 条件一:
注意到题目给出的 B 性质恰为:
考察判定条件
下文的所有内容将围绕思路一展开。
考察条件二的等式,令
特别地,将
考察一次 B 操作对
若对
- 对于
u 子树内任意一非叶节点i ,b_i\leftarrow b_i-d_i+1 ; - 对于
u 的父节点p ,b_p\leftarrow b_p-1 .
每一次操作后
因此对于原局面的一个非叶结点
优化判定形式
当时我接下来就没啥思路了,于是我选择将每个点执行了几次 B 操作设出来:
设对
下令
那么在执行完所有 B 操作后,
这个式子的结构相当令人恼火。
首先,其具有非常丑陋的后效性——无论是考虑从上到下确定
其次,该式中
这启发我们定义:
注意:由
将
这个式子的结构非常优雅且具有高贵的无后效性!现在我们可以考虑自底向上确定
设计判定信息
下文遵循自底向上考虑的逻辑顺序展开。
首先是考虑所有叶子:假如
对于一个子节点全是叶子的节点
左边是每个叶子节点
这启示我们对于每个
假如我们已经确定了
可以考虑暴力从
-
对于右边:
- 稳定增大
1 .
- 稳定增大
-
对于左边:
- 一开始可能会比任何一个
-b_v 都小,每次增大0 ; - 随着
t 的增大,可能会恰好超过其中一个-b_v ,每次增大1 ; - 又随着
t 的增大,可能会恰好超过其中两个-b_v ,每次增大2 ; - 以此类推。
- 一开始可能会比任何一个
那么
这启示我们对于每个
这同时启示我们使用二分即可轻松求得
回过头来思考如何求解
- 首先,在“左边每次增大
0 ”终止的那个时刻(上图点\text{B} ),b_u+t-\sum_{v\in e_u} \max(-b_v,t) 达到巅峰。左端点一定在此位置或其之前。 - 其次,假如在点
\small\mathrm{B} 之前,那么每往前一步,这个值就减1 。然而这个值要始终大于等于0 ,那么可行的左端点就是容易求得的。
于是我们解决了子节点都是叶子的情况。
对于子节点不全是叶子的情况,处理思路与之类似。具体来说:
若
解释一下:
-
上面那个式子和之前的那个类似,即每个子节点
f_v 取值的下界之和,应当小于等于b_u+k . -
下面那个式子是说,上界不能小于
b_u+k 。这样才能确保b_u+k 是取得到的(为什么在子节点全是叶子的情况中没有这一条?因为叶子节点i 的g_i 可以被认为是+\infty ,该式自然满足)。
可以使用与上一种情况类似的思路求解
至此我们解决了整个问题。
程序流程
-
首先求解
b_i 。若存在非叶结点b_i<0 则返回无解并退出。 -
接下来递归求解
f_i :- 对叶子节点,定下界
\ell_i=\max(0,-b_i) ,上界r_i=+\infty . - 对非叶节点,容易计算
\ell_i 并通过二分确定r_i 。若不存在合法的r_i 则返回无解并退出。 - 二分的上下界存在细节:前文那个大括号下面的式子不能忽略。
- 对叶子节点,定下界
-
返回有解,退出程序。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int Mx=100005,p=998244353;
int read(){
char ch=getchar();
int Alice=0,Aya=1;
while(ch<'0'||ch>'9'){
if(ch=='-') Aya=-Aya;
ch=getchar();
}
while(ch>='0'&&ch<='9')
Alice=(Alice<<3)+(Alice<<1)+(ch^48),ch=getchar();
return (Aya==1?Alice:-Alice);
}
int n;
int fa[Mx];
int a[Mx],b[Mx];
int deg[Mx];
int l[Mx],r[Mx];
vector<int>e[Mx];
void Solve(){
n=read();
for(int i=1;i<=n;i++) deg[i]=0,l[i]=0,r[i]=1e13,e[i].clear();
for(int i=2;i<=n;i++) fa[i]=read(),deg[fa[i]]++,e[fa[i]].push_back(i);
for(int i=1;i<=n;i++) a[i]=b[i]=read();
for(int i=2;i<=n;i++) b[fa[i]]-=a[i];//计算 b[i]
for(int i=1;i<=n;i++){
if(deg[i]&&b[i]<0){//判无解
puts("Shuiniao");
return;
}
}
for(int i=1;i<=n;i++) if(deg[i]==0&&b[i]<0) l[i]=-b[i];
for(int i=n;i>=1;i--){
if(deg[i]==0) continue;
int L=1e13,R=1e13,ok=-1;
for(int v:e[i]) L=min(L,l[v]);
for(int v:e[i]) R=min(R,r[v]);
int sf=0,sg=0;
for(int v:e[i]) sf+=l[v];
for(int v:e[i]) sg+=r[v];
if(sg<b[i]){
puts("Shuiniao");
return;
}
L=min(L,sg-b[i]),R=min(R,sg-b[i]);//注意二分的上下界
l[i]=min(L,max(0ll,sf-b[i]));//计算下界
while(L<=R){
int mid=((L+R)>>1);
int w=0;
for(int v:e[i]) w+=max(mid,l[v]);
w-=mid;
if(w<=b[i]) L=mid+1,ok=mid;
else R=mid-1;
}
if(ok==-1){//判无解
puts("Shuiniao");
return;
}
r[i]=ok;//二分上界
}
puts("Huoyu");
}
signed main(){
read();
int T=read();
for(int i=1;i<=T;i++) Solve();
return 0;
}