【问题描述】

密室是一棵有 $n$ 个节点的完全二叉树,每个节点有一个灯泡。点亮所有灯泡即可逃出密室。 每个灯泡有个权值 $A_i$,每条边也有个权值 $B_i$。 点亮第 $1$ 个灯泡不需要花费,之后每点亮一 个新的灯泡V的花费,等于上一个被点亮的灯泡 $U$ 到这个点 $V$ 的距离 $D_{u,v}$,乘以这个点的权值 $A_v$。 在点灯的过程中,要保证任意时刻所有被点亮的灯泡必须连通,在点亮一个灯泡后必须先点亮其子树所有灯泡才能点亮其他灯泡。 请告诉他们,逃出密室的最少花费是多少。

【题目链接】

BZOJ 4446 小凸玩密室 SCOI2015

【解题思路】

自己做的时候并没有什么思路QwQ。 之后看了神奇的题解后想了好久似乎明白些了。

动态规划。

令 $f_{v,i}$ 表示从$v$开始点亮,$v$的子树还未点亮,$v$的子树全部点亮之后要走到深度为$i$的祖先的另外一个孩子(记作$t$)的最小代价。

转移:

当$v$为叶子时,直接走过去。

$$ f_{v, i} = A_t * dist(v, t)$$

当$v$只有左孩子时,先走到左孩子,再从左孩子走过去。

$$ f_{v, i} = A_{lc} * B_{lc} + f_{lc, i}$$

当$v$有两个孩子时,两种走法,先走左孩子,然后从左孩子走到右孩子,再从右孩子走过去,先走右孩子亦然,取较优的。

$$ f_{v, i} = min: \\ \\ A_{lc} * B_{lc} + f_{lc,\ depth(v)} + f_{rc, i}, \\ A_{rc} * B_{rc} + f_{rc,\ depth(v)} + f_{lc, i}. $$

至此,$f_{v, i}$的问题已全部转化为子问题,可以由孩子递推而来。

然而还没完,这只是另一个DP的基础。

另一个DP:

设 $g_{v, i}$ 表示走完v的子树再走到v的深度为i的祖先(记作$t$)的最小代价,转移和$f$基本同理:

叶子:

$$ g_{v,i} = A_{t} * dist(v, t) $$ 只有左孩子:

$$ g_{v, i} = A_{lc} * B_{lc} + g_{lc, i} $$

左右孩子都有:

$$ g_{v, i} = min: \\ \\ A_{lc} * B_{lc} + f_{lc, depth(v)} + g_{rc, i}, \\ A_{rc} * B_{rc} + f_{rc, depth(v)} + g_{lc, i}. $$

特殊的,当状态中$i = 0$时表示停在任意位置,并没有深度为0的点,因此我们计算距离时设为零。这也是不违背常理的。

于是DP完惹,考虑如何算答案,如果从根开始,答案即为 $g_{1, 0}$,除此之外,枚举所有非根的节点$v$作为起点,最优方案为先走v子树,再走v的兄弟子树,再走v的父亲的兄弟子树……,直到走到根,因为树高是$O(logn)$的,所以就这样模拟走着算就好。

取最小值,over。

【Tricks】

一些关于完全二叉树的计算, 对于点 $v$ :

  • 左孩子为 $v <\!\!< 1$.
  • 右孩子为 $v <\!\!< 1\ |\ 1$
  • 父亲为 $v >\!\!> 1$
  • 向上$k$层的祖先为 $v >\!\!> k$
  • 兄弟为 $v\ xor\ 1$
  • $ 1 \leq v \leq n$,否则$v$不存在

【AC代码】

#include <cstdio>
#include <algorithm>
 
#define MAXN 200000 + 1
#define MAXLOGN 18 + 1
 
#define int64 long long
 
int n, depth[MAXN];
int64 a[MAXN], b[MAXN], d[MAXN];
int64 f[MAXN][MAXLOGN], g[MAXN][MAXLOGN];
 
inline void dp(){
    for(int v = n; v; v--){
        for(int i = depth[v] - 1; i >= 0; i--){
            int lc = v << 1, rc = lc | 1;
            int target = v >> (depth[v] - i - 1) ^ 1;
            if(lc > n){
                f[v][i] = a[target] * (d[v] + d[target] - (d[target >> 1] << 1));
            }
            else if(rc > n){
                f[v][i] = f[lc][i] + a[lc] * b[lc];
            }
            else{
                f[v][i] = std::min(
                    a[lc] * b[lc] + f[lc][ depth[v] ] + f[rc][i],
                    a[rc] * b[rc] + f[rc][ depth[v] ] + f[lc][i]
                );
            }
        }
    }
    for(int v = n; v; v--){
        for(int i = depth[v]; i >= 0; i--){
            int lc = v << 1, rc = lc | 1;
            int target = v >> (depth[v] - i);
            if(lc > n){
                g[v][i] = a[target] * (d[v] - d[target]);
            }
            else if(rc > n){
                g[v][i] = g[lc][i] + a[lc] * b[lc];
            }
            else{
                g[v][i] = std::min(
                    a[lc] * b[lc] + f[lc][ depth[v] ] + g[rc][i],
                    a[rc] * b[rc] + f[rc][ depth[v] ] + g[lc][i]
                );
            }
        }
    }
}
 
inline int64 calc(int v){
    int64 ans = g[v][ depth[v] - 1];
    for(; v != 1; v >>= 1){
        int brother = v ^ 1, father = v >> 1;
        if(brother > n){
            ans += a[father >> 1] * b[father];
        }
        else{
            ans += a[brother] * b[brother] + g[brother][ depth[father] - 1 ];
        }
    }
    return ans;
}
 
inline int64 solve(){
    dp();
    int64 ans = g[1][0];
    for(int i = 2; i <= n; i++) ans = std::min(ans, calc(i));
    return ans;
}
 
int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%lld", a + i);
    for(int i = 2; i <= n; i++) scanf("%lld", b + i);
 
    depth[1] = 1, d[1] = 0;
    for(int i = 2; i <= n; i++){
        depth[i] = depth[i >> 1] + 1;
        d[i] = d[i >> 1] + b[i];
    }
 
    printf("%lld\n", solve());
 
    return 0;
}

就这样啦