题目
思路一
注意到点灯的条件是很苛刻的。如果你仔细思考,就会明白:本质上,可以写成某一种 d p \tt{dp} dp 。
首先只考虑从根节点 1 1 1 开始点灯的情况。发现搞定 x x x 的子树之后,一定是去 x x x 的某个祖先的兄弟。
为什么呢?因为 x x x 的某一个祖先,先走到了 x x x 所隶属的儿子子树。“回溯”的时候,就不得不进入其中,继续点灯。
或者再换一种说法。我可以将树按照某一种形态排列,使得每个非叶子节点被点亮后,下一个被点亮的,是其左儿子。也就是说,我让 灯被点亮的顺序恰好为先序遍历的顺序。
这样一来,当子树 x x x 内所有的灯都被点亮的时候,下一个到达的将是 x x x 的某个祖先的兄弟。
然后就可以 d p \tt{dp} dp 了(因为只有 log n \log n logn 个祖先)。可以用 f ( x , j ) f(x,j) f(x,j) 来表示状态。
那么不是从 1 1 1 开始呢?例如我们是从图中的黑色点开始?
我们需要算一个红色的玩意儿。发现是从
x
x
x开始走到某个祖先。那么再
d
p
\tt{dp}
dp 一次就好了。
有了这些,我们就可以用一个变量存储,一路上的“红色路径”(走到祖父节点处)即可。
代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline int readint(){
int a = 0, f = 1; char c = getchar();
for(; c<'0' or c>'9'; c=getchar())
if(c == '-') f = -1;
for(; '0'<=c and c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
void writeint(long long x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar(x%10+'0');
}
const int MaxN = 200005;
long long v[MaxN], a[MaxN]; int n;
void init(){
n = readint();
for(int i=1; i<=n; ++i)
a[i] = readint();
for(int i=2; i<=n; ++i)
v[i] = readint();
}
long long dp[MaxN][20], depth[MaxN];
// 搞定x的子树(走到x的代价不计入),回到x的i级祖先的兄弟
void getDP(int x){
if((x<<1) <= n) getDP(x<<1);
if((x<<1|1) <= n) getDP(x<<1|1);
/* 先递归处理左右儿子 */
for(int j=0; j<20; ++j){
if(not (x>>j)) break;
int fa = x>>j, lca = fa>>1;
/* 此时即是在求回到fa的兄弟的dp值 */
if((x<<1) > n) // 叶子节点
dp[x][j] = (depth[x]+depth[fa^1]-(depth[lca]<<1))*a[fa^1];
else if((x<<1|1) > n) // 只有左儿子
dp[x][j] = v[x<<1]*a[x<<1]+dp[x<<1][j+1];
else /* 膝下有两子 */
dp[x][j] = min(v[x<<1]*a[x<<1]+dp[x<<1][0]+dp[x<<1|1][j+1],
v[x<<1|1]*a[x<<1|1]+dp[x<<1|1][0]+dp[x<<1][j+1]);
/* '膝下有两子'走法唯二(此处以先走左儿子作例):
1. 搞定左儿子,回到其0级祖先的兄弟处(即右儿子)
2. 其右儿子被搞定,然后走回其j+1级祖先
毕竟,x的j级祖先,就是son(x)的j+1级祖先 */
}
}
long long f[MaxN][20], answer;
// 回到x的i+1级祖先
void getF(int x){
if((x<<1) <= n) getF(x<<1);
if((x<<1|1) <= n) getF(x<<1|1);
for(int j=0; j<20; ++j){
if(not (x>>j)) break;
int fa = x>>j>>1;
if((x<<1) > n)
f[x][j] = (depth[x]-depth[fa])*a[fa];
else if((x<<1|1) > n)
f[x][j] = v[x<<1]*a[x<<1]+f[x<<1][j+1];
else
f[x][j] = min(v[x<<1]*a[x<<1]+dp[x<<1][0]+f[x<<1|1][j+1],
v[x<<1|1]*a[x<<1|1]+dp[x<<1|1][0]+f[x<<1][j+1]);
// 与dp的递推没有差了许多,同样的道理
}
}
void dfs(int x,long long sum){
// printf("PPL or2 (%d %lld)\n",x,sum);
answer = min(answer,f[x][0]+sum);
if((x<<1) > n)
return ;
if((x<<1|1) > n){ // 只有左儿子
dfs(x<<1,sum+v[x]*a[x>>1]);
}else{
dfs(x<<1,sum+f[x<<1|1][1]+v[x<<1|1]*a[x<<1|1]);
dfs(x<<1|1,sum+f[x<<1][1]+v[x<<1]*a[x<<1]);
}
}
void solve(){
for(int i=2; i<=n; ++i) // 预处理深度
depth[i] = depth[i>>1]+v[i];
getDP(1); answer = dp[1][0];
getF(1); dfs(1,0);
writeint(answer);
putchar('\n');
}
signed main(){
// freopen("escape.in","r",stdin);
// freopen("escape.out","w",stdout);
init();
solve();
return 0;
}
思路二
状态定义:搞定哪一个子树,结束于哪一个点上。
复杂度,令人惊讶。对于第 k k k 层,其叶子总数是 O ( n ) \mathcal O(n) O(n) 的。一共只有 log n \log n logn 层,所以状态数量是 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的。
其转移便极其简单了。