题解:P11492 [BalticOI 2023] Minequake

· · 题解

P11492 [BalticOI 2023] Minequake题解

本题解公式较多,建议前往洛谷传送门观看。

本题解最终状态转移相较其他题解更加简单。

题目大意

题目描述的十分简洁易懂了,自己看题。本题解注重思路梳理和状态转移方程的推导。P11492 [BalticOI 2023] Minequake

思路

初步思路

看到这种树上求最小值的题目首先考虑树形 DP。又由于我比较菜,所以我们不妨先考虑已知起点的情况:

先假设以 1 号节点为根节点和起点,显然我们要思考以何种顺序遍历子树。我们当然可以先假设优先遍历子节点较少的子树是更优(其实稍加思考过后发现无论如何遍历子树对最终答案都没有影响,这一点在后文也会证明)。此时,我们就可以想到用一个数组 siz[x] 表示 x 子树的大小(包括 x 本身)

接下来,既然是 DP,那就让我们来定义一个状态。

DP 状态定义

定义:dp[x] 表示 x 子树中所有子节点(包括 x 号节点)第一次被访问到的时间和。

这时候问题来了:时间和是由起始节点的位置决定的,在目前的思路中是由 1 号节点的位置决定,那么就无法完成子树内部独立的状态转移,违背了 DP 的初衷。那怎么办呢?

凉拌炒鸡蛋,别做了吧。

经过许久的思考,我们可以修改定义:

dp[x] 表示x 号节点出发x 子树中所有子节点第一次被访问到的时间减去 x 号节点第一次被访问的时间的和。说人话,就是假设 x 号节点第一次被访问的时间为 0,就可以直接按照之前的想法求子树和,之后状态转移的时候再处理。

状态转移方程的推导

假设第 x 号节点存在子节点 y_1,y_2\dots y_ksiz[x] 表示 x 的子树大小(包括 x 本身),从编号小的结点遍历到编号大的节点。

我们以存在 2 个子节点的子树为例,稍加推理则有:

\textstyle dp[x]=dp[y_1]+siz[y_1]\times 1 +dp[y_2]+siz[y_2]\times (1+2\times siz[y_1])

这个式子看着难懂,其实一点也不简单。

对于子节点 y_1,假设 dp[y_1] 已知(利用深搜思想)。那么回顾定义,在这一步的分析中,y_1 号节点第一次被访问的时间是 1 (假设访问 x 号节点的时间为 0),可是在计算 dp[y_1] 的时候我们认为访问 y_1 节点的时间为 0。那么 y_1 子树上所有节点对此时答案的贡献也都要增加 1 (总共增加 siz[y_1]\times 1),最终 y_1 号子树对答案的贡献是 dp[y_1]+siz[y_1] \times 1。同理,因为我们需要访问完 y_1 号子树后所有节点后回到 x 节点(用时为 2\times siz[y_1]),之后再访问 y_2 号节点,所以访问到 y_2 号节点的时间应该是 1+2\times siz[y_1],最终 y_2 号节点对答案的贡献是 dp[y_2]+siz[y_2]\times (1+2\times siz[y_1])。于是我们便有了上面这个特殊的状态转移方程。

紧接着,利用特殊到一般的思想,我们获得了一个正确的状态转移方程。建议尝试自己推理一下:当第 x 号节点存在子节点 y_1,y_2\dots y_k 时,有:

\textstyle dp[x]=\sum_{i=1}^{k}(dp[y_i]+siz[y_i]\times (1+2\times \sum_{j=1}^{j<i}siz[y_j]) )

此时我们已经完成了假设以 1 号节点为根节点和起点时的任务(时间复杂度 O(n\log{n})),并且证明了无论遍历顺序如何都不会影响答案,在这种假设下,一个深搜即可解决问题,最终答案为 dp[1]。但是为了这道题后续需要的操作,我们将这个式子进行化简。建议尝试自己推理一下

\begin{aligned}\textstyle dp[x]&=\sum_{i=1}^{k}(dp[y_i]+siz[y_i]\times (1+2\times \sum_{j=1}^{j<i}siz[y_j]) )\\\textstyle &=\sum_{i=1}^{k}dp[y_i]+\sum_{i=1}^{k}siz[y_i]+2\times \sum_{i=1}^{k}\sum_{j=1}^{j<i}(siz[y_j]\times siz[y_i])\\\textstyle &=\sum_{i=1}^{k}dp[y_i]+\sum_{i=1}^{k}siz[y_i]+(\sum_{i=1}^{k}siz[y_i])^2-\sum_{i=1}^{k}siz[y_i]^2\\\textstyle &=\sum_{i=1}^{k}dp[y_i]+(siz[x]-1)+(siz[x]-1)^2-\sum_{i=1}^{k}siz[y_i]^2\\\textstyle &=\sum_{i=1}^{k}dp[y_i]+siz[x]^2-siz[x]-\sum_{i=1}^{k}siz[y_i]^2\end{aligned}

其中有一个东西我在这里解释一下:关于 2\times \sum_{i=1}^{k}\sum_{j=1}^{j<i}(siz[y_j]\times siz[y_i]) 是如何变为 (\sum_{i=1}^{k}siz[y_i])^2-\sum_{i=1}^{k}siz[y_i]^2 的。

我们举个例子:假设 k=3,那么则有:

\textstyle \sum_{i=1}^{k=3}\sum_{j=1}^{j<i}(siz[y_j]\times siz[y_i])=siz[y_1]\times siz[y_2]+siz[y_1]\times siz[y_3]+siz[y_2]\times siz[y_3]

我们都学过 (a+b+c)^2=(a^2+b^2+c^2)+2ab+2bc+2ac,所以有:

\begin{aligned}\textstyle \sum_{i=1}^{k=3}\sum_{j=1}^{j<i}(siz[y_j]\times siz[y_i])&=siz[y_1]\times siz[y_2]+siz[y_1]\times siz[y_3]+siz[y_2]\times siz[y_3]\\&=\frac{(\sum_{i=1}^{k=3}siz[y_i])^2-\sum_{i=1}^{k=3}siz[y_i]^2}{2}\end{aligned} 最终状态转移: $$ \textstyle dp[x]=\sum_{i=1}^{k}dp[y_i]+siz[x]^2-siz[x]-\sum_{i=1}^{k}siz[y_i]^2 $$ ### 正解思路 在之前的思路里,我们假设了 $1$ 号节点为出发节点。可题意中起始节点不确定。这时候考虑**换根 DP**。 令 $ans[x]$ 表示以 $x$ 为起始节点和根节点的答案。 接着考虑计算 $x$ 号节点的所有子节点的答案。显然,对于 $x$ 号节点的所有子节点 $z$,我们只要将 $dp[x]$ 中 $z$ 节点的贡献扣除,再在 $dp[z]$ 中加入 $x$ 节点的贡献,就可以计算出以 $z$ 为起始节点的答案。这里主要涉及状态的转移推理。**建议尝试自己推理一下**,我会将重点步骤列出,最终可以根据结论编写代码。 在换根前,有: $$ \textstyle ans[x]=dp[x] $$ 因为 $x$ 为根节点,所以有: $$ \begin{aligned} \textstyle dp[x]&=\sum_{i=1}^{k}dp[y_i]+siz[x]^2-siz[x]-\sum_{i=1}^{k}siz[y_i]^2 \\ &=\sum_{i=1}^{k}dp[y_i]+n^2-n-\sum_{i=1}^{k}siz[y_i]^2 \end{aligned} $$ 所以: $$ \textstyle ans[x]=\sum_{i=1}^{k}dp[y_i]+n^2-n-\sum_{i=1}^{k}siz[y_i]^2 $$ 换根后,令 $newdp[x]$ 表示扣除了 $z$ 的贡献后的答案(即 $x$ 号节点的子树中不包含 $z$),与原状态转移方程对应计算,则有: $$ \textstyle siz[x]=n-siz[z] $$ 以及: $$ \textstyle newdp[x]=ans[x]-dp[z]-(n^2-n)+siz[x]^2-siz[x]+siz[z]^2 $$ 此时在 $dp[z]$ 中加入 $x$ 节点的贡献,与原状态转移方程对应计算,最终的状态转移方程如下: $$ \begin{aligned}\textstyle ans[z]&=dp[z]+newdp[x]-siz[z]^2+n^2+siz[z]-n-(n-siz[z])^2\\&=ans[x]-n+2\times siz[z]\end{aligned} $$ 将 $newdp[x]$ 的式子代入表示,最终得到: $$ \textstyle ans[z]=ans[x]-n+2\times siz[z] $$ 推了这么久,最终式子竟然如此简单!(打代码的时候狂喜)。 ## Coding思路总结 首先,假设 $1$ 号节点为根节点,利用**树形 DP** 的思想将以 $1$ 号节点为起始节点的答案计算出来。 状态转移: $$ \textstyle dp[x]=\sum_{i=1}^{k}dp[y_i]+siz[x]^2-siz[x]-\sum_{i=1}^{k}siz[y_i]^2 $$ 接着使用**换根 DP**,将不同节点为起始节点和根节点的答案一并算出。 状态转移: $$ \textstyle ans[z]=ans[x]-n+2\times siz[z] $$ ## AC Code ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,u,v,sz[N],dp[N],ans[N],mn; vector<int>vc[N]; int dfs1(int x,int fa) { for(auto i:vc[x])//枚举子节点 { if(i==fa)continue; dp[i]=dfs1(i,x);//先计算子节点的答案 sz[x]+=sz[i];//顺便计算siz数组 dp[x]+=dp[i]-(sz[i]*sz[i]);//依照状态转移方程更新dp } dp[x]+=sz[x]*sz[x]-sz[x];//依照状态转移方程更新dp return dp[x]; } void dfs2(int x,int fa) { for(auto i:vc[x])//枚举子节点 { if(i==fa)continue; ans[i]=ans[x]-n+2*sz[i];//依照状态转移方程更新ans mn=min(mn,ans[i]); dfs2(i,x); } } signed main() { cin>>n; for(int i=1;i<n;i++) { cin>>u>>v; vc[u].push_back(v); vc[v].push_back(u); } for(int i=1;i<=n;i++)sz[i]=1;//将自己计入siz数组 mn=ans[1]=dfs1(1,0);//先尝试以1为根节点树形dp dfs2(1,0);//接着换根计算答案 cout<<mn; return 0; } ``` 写了快 2 个小时的题解。