八倍经验!题解:AT_abc348_e [ABC348E] Minimize Sum of Distances

· · 题解

锣鼓传送门 AT 传送门

双倍经验

三倍经验

四倍经验

五倍经验

六倍经验

七倍经验

八倍经验

先考虑暴力做法。

如何求出结点 i 的答案呢?显然可以直接从结点 i 开始 DFS 一遍就行。

这样的时间复杂度为 O(N^2)。怎么优化呢?

请看下面这张图片:

定义 size_u 表示 u 的子树内所有结点的 C 的和,ALL 为所有结点的 C 的和。

如图,假设钦定 0 为根节点,并已经计算出了以 0 为根节点的答案,现在该如何计算以 2 为根节点的答案呢?

我们发现,整棵树可以分成两部分,一部分是 2 的子树,我们叫它 A,即蓝框框起来的地方。剩下的一部分我们叫它 B,也就是用黄框框起来的地方。

我们注意到,A 部分的每个结点到结点 2 的距离,正好比到结点 0 的距离多 1(显然自己到自己的距离是 0)。

由于每个结点的贡献和距离成正比。距离多 1,也就意味着答案也要增加。

增加多少呢?当然就是 A 部分的大小。即 ALL-size_2

换句话说,ans_2=ans_0+\text{A的大小}

上面的式子不完整,还有一个 B 呢!同理可得,由于 B2 的距离比到 0 的距离要短,答案也要少一个 B。即 ans_2=ans_0+\text{A的大小}-\text{B的大小}

这样我们通过 $ans_0$ 推出了 $ans_2$。聪明的你突发奇想,可不可以继续向下推呢?当然可以。我们也可以推出 $2$ 的儿子的答案,儿子的儿子的答案……。 等等,这个过程不就是又一个 DFS 吗? 总结一下我们的算法流程: 1. 先钦定一个结点为根,暴力预处理一遍它得答案。并求出其子树大小。 2. 然后从根节点开始 DFS,按照上面的方法递推出每个该结点的答案。 3. 取最小值即可。时间复杂度 $O(N)$。 这种方法叫“换根 DP”,也叫“二次换根与扫描法”。你也可以在各种各样的算法书上看到这类题,也算是经典咏流传了。~~不然为什么有八倍经验。~~ [AC ](https://www.luogu.com.cn/record/154736491)代码: ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=1e5+7; const long long inf=1e20+7; int N; long long f[maxn],dep[maxn],sz[maxn],ALL,coin[maxn]; int head[maxn<<1],nxt[maxn<<1],to[maxn<<1],cnt_edge; void AddEdge(int u,int v) {nxt[++cnt_edge]=head[u];to[cnt_edge]=v;head[u]=cnt_edge;} void dfs(int u,int fa,long long dep){ f[1]+=coin[u]*dep;sz[u]=coin[u]; for(int i=head[u];i;i=nxt[i]){ int v=to[i];if(v==fa)continue; dfs(v,u,dep+1);sz[u]+=sz[v]; } } void solve(int u,int fa,long long ans){ f[u]=ans; for(int i=head[u];i;i=nxt[i]){ int v=to[i];if(v==fa)continue; long long dans=ans; dans-=sz[v];dans+=ALL-sz[v];//递推计算,你没看错,就这么简单。 solve(v,u,dans); } } int main(){ scanf("%d",&N); for(int i=1;i<N;i++) {int a,b;scanf("%d %d",&a,&b);AddEdge(a,b);AddEdge(b,a);} for(int i=1;i<=N;i++)scanf("%lld",&coin[i]),ALL+=coin[i]; dfs(1,0,0);solve(1,0,f[1]); f[0]=inf;for(int i=1;i<=N;i++)f[0]=min(f[0],f[i]); printf("%lld",f[0]); return 0; } ```