Link
Difficulty
算法难度5,思维难度7,代码难度6
Description
给定一棵 个点的完全二叉树,树边带权,点带 的权值。
定义两个点的距离 为 到 的最短路径上经过的边权之和。
你一开始可以选择一个起始点,并点亮这个点,这一步不需要花费。
你在点亮一个点之后,必须紧接着将它的子树内所有点都点亮,才可以点亮其他点。
同时你需要满足点亮的点时刻在一个连通块内。
除了第一步,你点亮其他点所需的代价为 ,其中 是你上一次点亮的点, 是你这次点亮的点。
Solution
我们考虑如何dp这个东西。
首先要考虑如果确定一个起点,答案会是什么样子的。
答案一定是:先每次选择一个儿子,填完这棵子树,再出来填另一棵;填完这个点为根的子树后,向父亲走,再去填另一棵子树,这样往复直到填完整棵树。
那么我们来考虑如何用设计dp状态,使得能用这个dp来凑出答案来。
假如我们设 表示填完 的子树所需的最小代价呢,显然这东西没法转移,因为你不知道你从哪儿来。
假如来个 表示填完 的子树且最后停在 的最小代价呢,显然状态数太过庞大。
我们换个思路,既然没法记录从哪儿来,不如记录我们下一步去哪儿?
我们用 表示填完 的子树,且最后去点亮 的最小代价,发现合法状态数非常少,是 级别的。
因为每个点填完子树之后,下一步只能点亮一个祖先或者祖先的另一个儿子。
具体来定义一下这个dp状态吧: 代表填完 的子树, 代表下一步填 级祖先, 代表下一步填 级祖先的另一个儿子,也就是 级祖先的兄弟。
我们发现这个dp状态可以涵盖所有需要的信息,转移虽然有点儿复杂,但却十分自然。
来具体说一下状态转移吧:
- 如果这个点是叶子,也就是它没有儿子,那么只需要直接计算跳跃的代价就好了。
- 如果这个点有左儿子,那么只需要先走下去并计算这一步的代价,然后从左儿子跳到 级祖先即可。
- 如果这个点有左右儿子,那么就对于先走左边还是先走右边分类讨论,然后取min即可。
这样我们就可以处理出来所有dp值了。
考虑有了这些dp值,我们如何计算最终答案。
考虑枚举从哪个点开始,然后模拟填的过程:先每次选择一个儿子,填完这棵子树,再出来填另一棵;填完这个点为根的子树后,向父亲走,再去填另一棵子树,这样往复直到填完整棵树。
然而这之中会有点儿小问题,就是走到根之后很难继续走下去了,我们发现没有合适的状态供我们使用了。
我们可以添加一个虚拟节点 号点,它的点权为 ,它连一条向 的边,权值为 。
这样一切问题都可以完美地解决了,因为我们的策略总是最后走到 号点然后停下,然而 号点权值为 ,对答案毫无影响。
这样我们就可以用 的时间复杂度来解决这个题了。
我思考了一下,觉得dp的一大关键就是如何用状态拼出来结果,自己体会一下吧。
写这个题的时候需要注意边界问题。
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
#define LL long long
using namespace std;
inline int read(){
int x=0,f=1;char ch=' ';
while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0' && ch<='9')x=x*10+(ch^48),ch=getchar();
return f==1?x:-x;
}
const int N=2e5+5;
int n;
LL a[N],val[N];
LL st[N][20],dis[N][20],dp[N][20][2];
inline void solve(){
for(int i=1;i<=n;++i)st[i][0]=i,dis[i][0]=0;
for(int i=1;i<=19;++i)st[0][i]=-1;
for(int i=1;i<=n;++i)
for(int j=1;j<=19;++j){
if((1<<(j-1))<=i)st[i][j]=i>>j;
else st[i][j]=-1;
if(~st[i][j])dis[i][j]=dis[st[i][1]][j-1]+val[i];
}
for(int i=n;i>=1;--i){
for(int j=0;~st[i][j] && j<=18;++j){
if((i<<1)>n){
if(~st[i][j])dp[i][j][0]=dis[i][j]*a[st[i][j]];
if(~st[i][j+1])dp[i][j][1]=(dis[i][j+1]+val[st[i][j]^1])*a[st[i][j]^1];
}
else if((i<<1|1)>n){
if(~st[i][j])dp[i][j][0]=dp[i<<1][j+1][0]+val[i<<1]*a[i<<1];
if(~st[i][j+1])dp[i][j][1]=dp[i<<1][j+1][1]+val[i<<1]*a[i<<1];
}
else{
if(~st[i][j])dp[i][j][0]=min(val[i<<1]*a[i<<1]+dp[i<<1][0][1]+dp[i<<1|1][j+1][0],val[i<<1|1]*a[i<<1|1]+dp[i<<1|1][0][1]+dp[i<<1][j+1][0]);
if(~st[i][j+1])dp[i][j][1]=min(val[i<<1]*a[i<<1]+dp[i<<1][0][1]+dp[i<<1|1][j+1][1],val[i<<1|1]*a[i<<1|1]+dp[i<<1|1][0][1]+dp[i<<1][j+1][1]);
}
}
}
LL ans=0x3f3f3f3f3f3f3f3f;
for(int i=1;i<=n;++i){
LL tmp=dp[i][1][0];
for(int j=st[i][1],last=i;j;last=j,j=st[j][1]){
if(last&1)
tmp+=val[last-1]*a[last-1]+dp[last-1][2][0];
else if((last+1)<=n)
tmp+=val[last+1]*a[last+1]+dp[last+1][2][0];
else
tmp+=val[j]*a[st[j][1]];
}
ans=min(ans,tmp);
}
printf("%lld\n",ans);
}
int main(){
memset(dp,0x3f,sizeof dp);
n=read();
for(int i=1;i<=n;++i)a[i]=read();
for(int i=2;i<=n;++i)val[i]=read();
solve();
return 0;
}