换根dp \color{green}{\texttt{换根dp}} 换根dp
严格来说,其实并没有
换根dp
这一种dp
方法,它只是树上dp
问题的一个解题技巧。但是由于这个技巧太过于重要了,所以我们可以把它叫做一个单独类型的dp
方法。
所谓的 换根dp
,是针对无根树上 dp
问题的一个解题技巧。
众所周知,我们解无根树上 dp
问题时一般会先强制一个节点(一般来说,为了方便,会选定
1
1
1 号点)当整棵树的根,然后在考虑求解。
但是,如果要求出所有点做根时的 dp
值该怎么办呢?暴力
O
(
n
)
O(n)
O(n) 枚举?不是不行,但是在大多数时候会 TLE
(注意,我没有说一定不行!)。
dp
题的一大特色是:理论非常难讲懂,所以我们直接上一道例题。
P2986 [USACO10MAR]Great Cow Gathering G \color{green}{\texttt{P2986 [USACO10MAR]Great Cow Gathering G}} P2986 [USACO10MAR]Great Cow Gathering G
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
[solution] \color{blue}{\texttt{[solution]}} [solution]
最原始的想法是一个 O ( n 2 ) O(n^2) O(n2) 的暴力: O ( n ) O(n) O(n) 枚举节点, O ( n ) O(n) O(n) 求出所有牛到一个节点的不方便值。
可是 1 ≤ n ≤ 1 × 1 0 5 1 \leq n \leq 1 \times 10^5 1≤n≤1×105, O ( n 2 ) O(n^2) O(n2) 这样的算法实在是太慢了。有没有快点的方法?
有。
我们记 f u f_u fu 表示所有牛到 u u u 这个节点的不方便值,则:
f u = ∑ i = 1 n C i × s i , u f_u=\sum\limits_{i=1}^{n} C_i\times s_{i,u} fu=i=1∑nCi×si,u
其中, s i , u s_{i,u} si,u 表示 i i i 和 u u u 之间的距离。
我们再记 s z u sz_u szu 表示 u u u 的子树的大小(注意:这个大小指的是有多少头牛而非有多少个节点)。
显然,我们会先指定一个节点当根(这个根就是所有奶牛要到的节点),我们不妨就指定 1 1 1 吧, O ( n ) O(n) O(n) 递归求出 s z i ( 1 ≤ i ≤ n ) sz_i(1 \leq i \leq n) szi(1≤i≤n) 和 f 1 f_1 f1。
如何用 f 1 f_1 f1 和 s z sz sz 推导出其它的 f i ( 2 ≤ i ≤ n ) f_i(2 \leq i \leq n) fi(2≤i≤n) 呢?我们发现这个移动节点的过程就是无根树移动根的过程,我们可以画这么一幅图:
这就是一个换根的过程。
它能告诉我们一些什么呢?首先我们发现以 3 3 3 为根的子树内的所有的点需要做得距离少了 s 1 , 3 s_{1,3} s1,3,但那些不是 3 3 3 的儿子的点(包括 1 1 1),就需要多走 s 1 , 3 s_{1,3} s1,3 的距离。
所以,在这幅图中,我们有:
f 3 = f 1 − s z 3 × s 1 , 3 + ( n − s z 3 ) × s 1 , 3 f_{3}=f_1-sz_3 \times s_{1,3} + (n-sz_3) \times s_{1,3} f3=f1−sz3×s1,3+(n−sz3)×s1,3
类似地,我们可以推广到一般情况:
f v = f u − s z v × s u , v + ( n − s z v ) × s u , v f_v = f_u - sz_v \times s_{u,v}+(n-sz_v) \times s_{u,v} fv=fu−szv×su,v+(n−szv)×su,v
于是,我们再 dfs
一次,就可以在
O
(
n
)
O(n)
O(n) 的时间复杂度内求出所有的
f
i
(
1
≤
i
≤
n
)
f_i(1 \leq i \leq n)
fi(1≤i≤n) 了。
总时间复杂度: O ( n ) O(n) O(n)。
[code] \color{blue}{\texttt{[code]}} [code]
const int N=1e5+100;
struct edge{//链式前向星
int next,to,len;
}e[N<<1];int h[N],tot;
inline void add(int a,int b,int c){
e[++tot]=(edge){h[a],b,c};h[a]=tot;
e[++tot]=(edge){h[b],a,c};h[b]=tot;
}
int sze[N],n,C[N],cnt;//含义如题
long long F1,ans;//要开long long
typedef long long ll;//为了方便
void dfs1(int u,int fa,ll s){
F1+=1ll*C[u]*s;sze[u]=C[u];
for(int i=h[u];i;i=e[i].next){
register int to=e[i].to;
if (to==fa) continue;//!
dfs1(to,u,s+e[i].len);
sze[u]+=sze[to];//updata
}
}
void dfs2(int u,int fa,ll Fu){
for(int i=h[u];i;i=e[i].next){
register int to=e[i].to;
if (to==fa) continue;//!
ll Fv=Fu-1ll*sze[to]*e[i].len
+1ll*(cnt-sze[to])*e[i].len;
ans=min(ans,Fv);//更新答案
dfs2(to,u,Fv);//继续递归
}
}
int main(){
n=read();F1=0;//init
for(int i=1;i<=n;i++)
cnt+=(C[i]=read());
for(int i=2;i<=n;i++){
int u=read(),v=read(),w=read();
add(u,v,w);//按照题目要求加边
}
dfs1(1,-1,0);//求F1
ans=F1;//ans最大为F1
dfs2(1,-1,F1);//换根
printf("%lld",ans);
}
总结 \color{green}{\texttt{总结}} 总结
-
一般地,做这种题目时,步骤如下:
- 选定一个节点当根,求出它的
dp
值。 - 考虑从它转移到其它节点的变化量。
- 两遍
dfs
求解。
- 选定一个节点当根,求出它的
-
另一篇类似的我的博客:https://blog.csdn.net/zhuyingye_123456/article/details/105026661#comments