题目描述
一棵有n个节点的树,每个节点有两个权值 \(a\)i 和 \(b\)i 从\(i\)点只能走到\(i\)子树内的点,从\(i\)到\(j\)的花费为 \(a\)i 和 \(b\)j 的乘积
求从根到叶子节点的最短路径长度(其中1为根)
\(n\) \(\leq\) 100000, |\(a\)i|, |\(b\)i| \(\leq\) 100000
分析
首先来一个正常人都不会这样想的暴力:
我们暴力枚举每一个点\(i\),然后暴力枚举\(i\)子树内的点,再暴力算出从\(i\)到子树内每个点的花费,然后暴力将其连边,接着暴力地将每个叶子节点向一个辅助点连一条权值为0的边,最后暴力地以1为起点,那个辅助点为终点,跑暴力的SPFA算法(因为有负边), 复杂度也是十分暴力的\(O(n\)\(3\)\()\)
当然如果他不卡SPFA(相信也没人会卡)的话,复杂度可以达到十分优秀的\(O(kn\)\(2\)\()\)
当然如果你发现了这其实是一个DAG,复杂度其实可以达到\(O(n\)\(2\)\()\)
好的胡扯到此结束,我们现在来考虑正解
首先正常人一眼过去肯定会发现这题可以DP,我们设\(f\)\(i\)为根节点1到i节点的最小花费
那么我们有
如果我们暴力按照上面的转移方程来做的话,你会发现他就是上面提到的DAG最短路(DAG最短路的实质其实就是DP)
我们再观察一下这个柿子
不难发现,其中\(a\)\(i\)只和i有关,\(b\)\(j\)与\(f\)\(j\)只和j有关
那么\(f_i\)其实就是求多条直线\(y = b_j * x + f_j\)在\(x = a_i\)处的最大值
那这就是很经典的李超线段树问题了
但由于是树形DP,故此题还需要合并李超线段树(其实就是普通的线段树合并啦
代码
#include<bits/stdc++.h>
#define int LL
using namespace std;
typedef long long LL;
const int N = 200010;
const int inf = 1e17;
struct line{
double k, b;
}p[N << 2];
int tree[N << 5], ls[N << 5], rs[N << 5], n, tot,
head[N], nxt[N], to[N], a[N], b[N], root[N], cnt;
long long dp[N];
int calc(int id, int d) { return p[id].k * d + p[id].b; }
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < ‘0‘ || c > ‘9‘) { if(c == ‘-‘) f = -1; c = getchar(); }
while(c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - ‘0‘, c = getchar();
return x * f;
}
void ins(int l, int r, int &x, int u)
{
if(!x) { x = ++tot; tree[x] = u; return ; }
int v = tree[x], mid = (l + r) / 2,
resu = calc(u, mid), resv = calc(v, mid);
if(l == r) { if(resu < resv) tree[x] = u; return ; }
if(!v) { tree[x] = u; return ; }
if(p[u].k > p[v].k){
if(resu < resv){
tree[x] = u;
ins(mid + 1, r, rs[x], v);
}
else ins(l, mid, ls[x], u);
}
else if(p[u].k < p[v].k){
if(resu < resv){
tree[x] = u;
ins(l, mid, ls[x], v);
}
else ins(mid + 1, r, rs[x], u);
}
else if(resu < resv) tree[x] = u;
}
LL que(int l, int r, int x, int d)
{
if(l > d || r < d || !x) return inf;
if(l == r) return calc(tree[x], d);
int mid = (l + r) / 2;
return min(1ll * calc(tree[x], d), min(que(l, mid, ls[x], d), que(mid + 1, r, rs[x], d)));
}
int merge(int x, int y, int l, int r)
{
if(!x || !y) return x + y;
int tx = tree[x], ty = tree[y], mid = (l + r) / 2;
if(l == r) return calc(tx, mid) < calc(ty, mid) ? x : y;
ls[x] = merge(ls[x], ls[y], l, mid);
rs[x] = merge(rs[x], rs[y], mid + 1, r);
ins(l, r, x, tree[y]);
return x;
}
void addedge(int u, int v)
{
to[++cnt] = v;
nxt[cnt] = head[u], head[u] = cnt;
}
void dfs(int u, int f)
{
bool flag = false;
for(int i = head[u]; i; i = nxt[i]){
int v = to[i];
if(v == f) continue;
flag = true;
dfs(v, u);
root[u] = merge(root[u], root[v], -1e5, 1e5);
}
dp[u] = que(-1e5, 1e5, root[u], a[u]);
if(!flag) dp[u] = 0;
p[u].k = b[u], p[u].b = dp[u];
ins(-1e5, 1e5, root[u], u);
}
signed main()
{
n = read();
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 1; i <= n; i++) b[i] = read();
int u, v;
for(int i = 1; i < n; i++){
u = read(), v = read();
addedge(u, v);
addedge(v, u);
}
dfs(1, 0);
for(int i = 1; i <= n; i++) printf("%lld ", dp[i]);
return 0;
}
需要注意的是,如果线性DP出现\(f_i = min(b_j * a_i + c_j)\)的转移方程,我们应该首先考虑斜率优化(即\(a_i\)是否单调)