2022 SDU Trial Problem A:
简单题意:
给定一棵有根树,将此树划分成 θ 条从某个点到它的祖先的链,每条链的长度和权重存在限制,问 θ 的最小值。
我是怎么想的:
这是一个比较熟悉的问题,尽管题目有长度和权重限制。
我的第一个想法是,将所有点按照深度降序排列,依次遍历。如果遍历到一个未被标记的节点,那就从此处开始一步一步往上走,把沿途的点给标记上,如果遇到一个已经标记过的点或者链的长度或者重量超过了限制,那就停止往上走。然后遍历下一个节点。这是一个很朴素的剖分方法,相信很多人第一感都是这样的。由于每个点只能被标记一次,所以这个暴力方法的复杂度实际上是 O ( n ) O(n) O(n)的。
这样对吗?答案是否定的。
考虑下面这张图:
如果L=2,W=1,那么,按照我们的算法,我们会得到三条链:
4-2
3
1
明明节点3是可以往上走的,但是因为2已经被标记过了,所以3错失了往上走的机会。
修正后的算法:
修正的办法很简单,我们可以一直往上跳到不能再跳为止。但是,这种做法的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2) 的,显然无法AC。于是我们想到了一个加速树上跳跃的经典算法:
Binary Lifting On Tree (树上倍增)。
只要知道每个点的父亲标号、每个点跳到父亲的花费,就能预处理出它的K级父亲的标号、跳到K级父亲的花费。
假设计算每个点能跳到的最高位置的工作已经解决了,那我们如何统计答案呢?
这就是树形dp的问题了(不会的同学回去想一下,这个东西一两句说不清楚)。
为什么我一直WA:
这是我错误的树上跳跃实现:
int m=x,d=deep[m];
for(int j=19;j>=0;j--){
if(d&(1<<j)){
if(now+val[m][j]<=up){
now=now+val[m][j];
m=fa[m][j];
}
}
}
return m;
这份代码一眼看去就是经典的树上倍增,d是深度,
从高位到低位检查d的二进制位,如果可以往上跳那就往上跳。但是,在存在限制条件时,检查2进制位并不是一个周到的算法。
下面改进的算法才work:
int m=x,d=deep[m];
for(int j=19;j>=0;j--){
if(d>=(1<<j)){
if(now+val[m][j]<=up){
now=now+val[m][j];
m=fa[m][j];
d-=1<<j;
}
}
}
return m;
我相信一个脑袋正常的人都会按照正解这样来写,因为这样的意思才是“能跳就跳”。
为什么我会写成最上面那种逻辑呢?因为经典的树上倍增就是这样实现的,写习惯了。