1. 程式人生 > 其它 >題解[CF932F] Escape Through Leaf

題解[CF932F] Escape Through Leaf

題目描述

一棵有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節點的最小花費
那麼我們有

\[f_i = min_{j,j是i的後代} \qquad {f_j + a_i * b_j} \]

如果我們暴力按照上面的轉移方程來做的話,你會發現他就是上面提到的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\)是否單調)