Chương 7·Bài 75

Li Chao Tree

Có một tập đường thẳng y=ax+by = ax + b. Thỉnh thoảng thêm một đường mới, thỉnh thoảng hỏi: tại xx này, đường nào thấp nhất, giá trị bao nhiêu?

Bao lồi đòi hệ số góc phải đơn điệu. Cây Li Chao thì không đòi gì: mỗi nút của cây phân đoạn trên miền xx giữ một đường, thêm và hỏi đều O(log⁡C)O(\log C). Cùng khung còn chèn được đoạn thẳng (đường chỉ có hiệu lực trên một khoảng) và gộp hai cây, hợp cho DP trên cây.

Chèn và hỏi

Mỗi nút phụ trách đoạn [l,r][l, r] của miền xx và giữ một đường. Chèn đường ff vào nút đang giữ gg:

  1. So ff và gg tại điểm giữa mm; đường nhỏ hơn ở mm ở lại nút, đường kia đi tiếp.
  2. Hai đường cắt nhau tối đa một lần, nên đường thua ở mm chỉ có thể thắng ở một nửa: nửa trái nếu nó thắng ở ll, ngược lại nửa phải. Đệ quy vào đúng nửa đó.

Nút rỗng thì đặt đường vào và dừng, nên mỗi lần chèn tạo tối đa một nút mới: cây động trên miền [−109,109][-10^9, 10^9] chỉ cần số nút bằng số đường. Hỏi tại xx: đi từ gốc xuống lá chứa xx, lấy nhỏ nhất của các đường gặp trên đường đi. Đúng vì đường tốt nhất tại xx luôn nằm trên đường từ gốc tới lá đó.

Điểm giữa của đoạn có số âm lấy bằng (l + r) >> 1 để làm tròn xuống. Giá trị ax+bax + b với ∣a∣,∣x∣≤109|a|, |x| \le 10^9, ∣b∣≤1018|b| \le 10^{18} vừa long long.

Hình trên là các đường, dải xanh là đoạn của nút đang xét, vạch đứt là trung điểm. Hình dưới cho thấy đường mà mỗi nút đang giữ.

Chèn đường vào cây Li Chao.

Hỏi nhanh

Nút phụ trách [0,8][0, 8] đang giữ g(x)=xg(x) = x. Chèn f(x)=10−xf(x) = 10 - x. Đường nào ở lại nút, đường nào đi tiếp và đi về nửa nào?

Ví dụGiá Trị Thấp Nhất

Đề. Có sẵn nn đường, rồi qq thao tác (tổng tới 4⋅1054 \cdot 10^5): thêm đường ax+bax + b, hoặc hỏi giá trị nhỏ nhất tại xx, với ∣a∣,∣x∣≤109|a|, |x| \le 10^9, ∣b∣≤1018|b| \le 10^{18}.

Lời giải. Cây Li Chao trên miền x∈[−109,109]x \in [-10^9, 10^9], nút chỉ được tạo khi cần, nên số nút không vượt số đường. Đường có sẵn và đường thêm sau đều chèn cùng một cách; hỏi là đi một đường từ gốc xuống.

Bước 1: chèn một đường. Mỗi nút giữ một đường; lc, rc bằng 0 là chưa có con. insert nhận x theo tham chiếu nên tạo nút mới là gán thẳng vào ô con của cha. left là đường mới tốt hơn ở đầu trái ll, mid là tốt hơn ở trung điểm mm. Nếu mid thì đổi chỗ: nút luôn giữ đường tốt hơn tại mm, đường thua đi tiếp.

C++
void insert(int &x, ll l, ll r, ll a, ll b) {
    if (!x) {
        x = ++tot, la[x] = a, lb[x] = b;
        return;
    }
    ll m = (l + r) >> 1;
    bool left = a * l + b < f(x, l), mid = a * m + b < f(x, m);
    if (mid) swap(la[x], a), swap(lb[x], b);
    if (l == r) return;
    if (left != mid) insert(lc[x], l, m, a, b);
    else insert(rc[x], m + 1, r, a, b);
}

Đi nửa nào: hai đường cắt nhau tối đa một lần. left != mid thì hai đường đổi thứ tự giữa ll và mm, nên đường thua chỉ có thể thắng ở nửa trái. left == mid thì trên cả nửa trái đường giữ ở nút vẫn tốt hơn, đường thua chỉ còn cơ hội ở nửa phải.

Bước 2: hỏi. Đi từ gốc xuống theo pp, lấy nhỏ nhất của mọi đường gặp. Đường tốt nhất tại pp hoặc nằm ở một nút trên đường đi, hoặc đã bị đẩy xuống đúng nửa chứa pp.

C++
while (x) {
    res = min(res, f(x, p));
    ll m = (l + r) >> 1;
    if (p <= m) x = lc[x], r = m;
    else x = rc[x], l = m + 1;
}

Tràn số: ∣a⋅p∣≤1018|a \cdot p| \le 10^{18} cộng ∣b∣≤1018|b| \le 10^{18} vẫn dưới 9,2⋅10189{,}2 \cdot 10^{18}. (l + r) >> 1 làm tròn xuống cả khi l+rl + r âm, khác với / 2.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int M = 4e5 + 5;
const ll L = -1e9, R = 1e9, INF = LLONG_MAX;
int tot, root, lc[M], rc[M];
ll la[M], lb[M];

ll f(int x, ll p) {
    return la[x] * p + lb[x];
}

void insert(int &x, ll l, ll r, ll a, ll b) {
    if (!x) {
        x = ++tot, la[x] = a, lb[x] = b;
        return;
    }
    ll m = (l + r) >> 1;
    bool left = a * l + b < f(x, l), mid = a * m + b < f(x, m);
    if (mid) swap(la[x], a), swap(lb[x], b);
    if (l == r) return;
    if (left != mid) insert(lc[x], l, m, a, b);
    else insert(rc[x], m + 1, r, a, b);
}

ll query(ll p) {
    ll res = INF, l = L, r = R;
    int x = root;
    while (x) {
        res = min(res, f(x, p));
        ll m = (l + r) >> 1;
        if (p <= m) x = lc[x], r = m;
        else x = rc[x], l = m + 1;
    }
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; i++) {
        ll a, b;
        cin >> a >> b;
        insert(root, L, R, a, b);
    }
    while (q--) {
        int t;
        cin >> t;
        if (t == 0) {
            ll a, b;
            cin >> a >> b;
            insert(root, L, R, a, b);
        } else {
            ll x;
            cin >> x;
            cout << query(x) << '\n';
        }
    }
}
2 5
2 5
-1 8
1 0
0 1 -3
1 4
1 -2
1 3

Mỗi thao tác O(log⁡2⋅109)≈31O(\log 2 \cdot 10^9) \approx 31 bước. Miền xx nhỏ thì có thể dựng cây tĩnh trên mảng, nhưng cây động không thua:

Li Chao: 500 nghìn đường và 500 nghìn câu hỏi, x tới một triệu, trung vị 5 lần chạy, i7-10850H, g++ -O2.nút động, đánh số theo thứ tự tạo32 msmảng tĩnh theo chỉ số 2x, 2x + 139 ms · gấp 1,2 lần
Li Chao với 500 nghìn đường và 500 nghìn câu hỏi, x tới một triệu, hai cách cài, đo trên i7-10850H, g++ -O2.

Hai cách ngang tốc độ (chênh dưới 1,2 lần), mà cây động chỉ cần một nút cho mỗi đường thay vì mảng 2C2C ô. Quy tắc: miền lớn hay chưa rõ thì cứ dùng cây động.

Đoạn thẳng

Đường chỉ có hiệu lực trên [l,r)[l, r): đi xuống cây phân đoạn như cập nhật đoạn, tại mỗi nút nằm trọn trong [l,r)[l, r) thì chèn kiểu Li Chao bình thường trong cây con của nút đó. Có O(log⁡C)O(\log C) nút phủ, mỗi nút chèn tốn O(log⁡C)O(\log C), tổng O(log⁡2C)O(\log^2 C) mỗi đoạn; hỏi vẫn O(log⁡C)O(\log C).

Làm ngoại tuyến thì nén các điểm hỏi thành mảng xs đã sắp, dựng cây Li Chao tĩnh trên chỉ số 0..k−10..k-1, so sánh đường tại xs[m]. Đoạn [l,r)[l, r) đổi thành khoảng chỉ số các điểm hỏi nằm trong nó. Không có điểm hỏi nào trong đoạn thì bỏ qua đường đó.

Ví dụHàm Trên Nửa Khoảng

Đề. n+q≤4⋅105n + q \le 4 \cdot 10^5 thao tác: thêm hàm ax+bax + b hiệu lực trên [l,r)[l, r), hoặc hỏi giá trị nhỏ nhất tại xx (in INFINITY nếu không có hàm nào).

Lời giải. Làm ngoại tuyến. Đọc hết thao tác, gom mọi xx được hỏi, sắp và bỏ trùng thành xs. Cây Li Chao dựng trên chỉ số 0..k−10..k-1, so sánh các đường tại xs[m] thay vì tại mm. Hàm hiệu lực trên [l,r)[l, r) đổi thành khoảng chỉ số các điểm hỏi nằm trong đó; khoảng này phủ bởi O(log⁡k)O(\log k) nút của cây phân đoạn, và mỗi nút phủ nhận một lần chèn Li Chao trong cây con của nó.

Bước 1: chèn Li Chao trên chỉ số. Giống insert của ví dụ trước, viết bằng vòng lặp, thay ll, mm bằng xs[l], xs[m]. has[x] = 0 là nút chưa có đường.

C++
bool left = a * xs[l] + b < la[x] * xs[l] + lb[x];
bool mid = a * xs[m] + b < la[x] * xs[m] + lb[x];
if (mid) swap(la[x], a), swap(lb[x], b);
if (l == r) return;
if (left != mid) x = 2 * x, r = m;
else x = 2 * x + 1, l = m + 1;

Bước 2: chèn theo đoạn. Đi như cập nhật đoạn của cây phân đoạn; ở mỗi nút nằm trọn trong khoảng thì gọi push cho cây con của nút đó. Đường đặt ở nút này chỉ được hỏi bởi các điểm nằm trong đoạn của nút, nên nó không lọt ra ngoài [l,r)[l, r).

C++
void addSeg(int x, int l, int r, int ql, int qr, ll a, ll b) {
    if (qr < l || r < ql) return;
    if (ql <= l && r <= qr) {
        push(x, l, r, a, b);
        return;
    }
    int m = (l + r) / 2;
    addSeg(2 * x, l, m, ql, qr, a, b);
    addSeg(2 * x + 1, m + 1, r, ql, qr, a, b);
}

Bước 3: đổi [l,r)[l, r) thành chỉ số. ql là điểm hỏi đầu tiên ≥l\ge l, qr là điểm hỏi cuối cùng <r< r. ql > qr nghĩa là không ai hỏi trong khoảng đó, bỏ qua đường. Thao tác làm lại đúng thứ tự đọc, nên câu hỏi chỉ thấy các hàm thêm trước nó.

C++
int ql = lower_bound(xs.begin(), xs.end(), o[1]) - xs.begin();
int qr = lower_bound(xs.begin(), xs.end(), o[2]) - xs.begin() - 1;
if (ql <= qr) addSeg(1, 0, k - 1, ql, qr, o[3], o[4]);

query đi từ gốc tới lá p; mỗi nút trên đường đi chứa p trong đoạn của mình, nên mọi đường ở đó đều có hiệu lực tại xs[p].

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 2e5 + 5;
const ll INF = LLONG_MAX;
int k, has[4 * N];
ll la[4 * N], lb[4 * N];
vector<ll> xs;

void push(int x, int l, int r, ll a, ll b) {
    while (true) {
        if (!has[x]) {
            has[x] = 1, la[x] = a, lb[x] = b;
            return;
        }
        int m = (l + r) / 2;
        bool left = a * xs[l] + b < la[x] * xs[l] + lb[x];
        bool mid = a * xs[m] + b < la[x] * xs[m] + lb[x];
        if (mid) swap(la[x], a), swap(lb[x], b);
        if (l == r) return;
        if (left != mid) x = 2 * x, r = m;
        else x = 2 * x + 1, l = m + 1;
    }
}

void addSeg(int x, int l, int r, int ql, int qr, ll a, ll b) {
    if (qr < l || r < ql) return;
    if (ql <= l && r <= qr) {
        push(x, l, r, a, b);
        return;
    }
    int m = (l + r) / 2;
    addSeg(2 * x, l, m, ql, qr, a, b);
    addSeg(2 * x + 1, m + 1, r, ql, qr, a, b);
}

ll query(int p) {
    ll res = INF;
    int x = 1, l = 0, r = k - 1;
    while (true) {
        if (has[x]) res = min(res, la[x] * xs[p] + lb[x]);
        if (l == r) return res;
        int m = (l + r) / 2;
        if (p <= m) x = 2 * x, r = m;
        else x = 2 * x + 1, l = m + 1;
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, q;
    cin >> n >> q;
    vector<array<ll, 5>> ops;
    for (int i = 0; i < n; i++) {
        ll l, r, a, b;
        cin >> l >> r >> a >> b;
        ops.push_back({0, l, r, a, b});
    }
    for (int i = 0; i < q; i++) {
        ll t;
        cin >> t;
        if (t == 0) {
            ll l, r, a, b;
            cin >> l >> r >> a >> b;
            ops.push_back({0, l, r, a, b});
        } else {
            ll x;
            cin >> x;
            ops.push_back({1, x, 0, 0, 0});
            xs.push_back(x);
        }
    }
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(), xs.end()), xs.end());
    k = xs.size();
    for (array<ll, 5> o : ops) {
        if (o[0] == 0) {
            int ql = lower_bound(xs.begin(), xs.end(), o[1]) - xs.begin();
            int qr = lower_bound(xs.begin(), xs.end(), o[2]) - xs.begin() - 1;
            if (ql <= qr) addSeg(1, 0, k - 1, ql, qr, o[3], o[4]);
        } else {
            ll v = query(lower_bound(xs.begin(), xs.end(), o[1]) - xs.begin());
            if (v == INF) cout << "INFINITY\n";
            else cout << v << '\n';
        }
    }
}
1 5
0 5 2 3
1 0
1 5
0 4 8 -1 10
1 4
1 6

Độ phức tạp: mỗi hàm O(log⁡2k)O(\log^2 k), mỗi câu hỏi O(log⁡k)O(\log k).

DP hàm bậc nhất

DP dạng dpi=min⁡j<i(dpj+g(j)⋅xi+h(j))+c(i)dp_i = \min_{j < i} \big(dp_j + g(j) \cdot x_i + h(j)\big) + c(i): với mỗi jj đã tính xong, thêm đường hệ số góc g(j)g(j), tung độ gốc dpj+h(j)dp_j + h(j); tính dpidp_i là hỏi tại xix_i. Bình phương hiệu là dạng hay gặp: (hi−hj)2=hi2−2hihj+hj2(h_i - h_j)^2 = h_i^2 - 2 h_i h_j + h_j^2, phần chỉ phụ thuộc ii đưa ra ngoài, phần chỉ phụ thuộc jj vào tung độ gốc, tích chéo −2hj⋅hi-2 h_j \cdot h_i là hệ số góc nhân điểm hỏi.

Hỏi nhanh

dpi=min⁡j<i(dpj+(hi−hj)2)dp_i = \min_{j < i} (dp_j + (h_i - h_j)^2). Đường thẳng thêm vào sau khi tính dpjdp_j là gì?

Khi điểm hỏi nguyên trong miền nhỏ (như h≤106h \le 10^6), dùng cây tĩnh trên mảng 4C4C phần tử, không cần nút động.

Ví dụBệ Cao Dần

Đề. n≤2⋅105n \le 2 \cdot 10^5 bệ, độ cao tăng hi≤106h_i \le 10^6. Nhảy từ ii tới j>ij > i tốn (hj−hi)2+C(h_j - h_i)^2 + C, C≤1012C \le 10^{12}. Tìm chi phí nhỏ nhất từ bệ 1 tới bệ nn.

Lời giải. dpidp_i là chi phí nhỏ nhất để tới bệ ii. Khai triển (hi−hj)2(h_i - h_j)^2 được

dpi=hi2+C+min⁡j<i(−2hj⋅hi+dpj+hj2).dp_i = h_i^2 + C + \min_{j < i} \big(-2 h_j \cdot h_i + dp_j + h_j^2\big).

Phần trong ngoặc là giá trị tại x=hix = h_i của đường hệ số góc −2hj-2h_j, tung độ dpj+hj2dp_j + h_j^2. Tính xong dpjdp_j thì thêm đường của jj; tính dpidp_i là hỏi nhỏ nhất tại hih_i. Độ cao nguyên tới 10610^6 nên dùng cây Li Chao tĩnh trên mảng 4⋅1064 \cdot 10^6 ô, nút xx có con 2x2x, 2x+12x + 1; insert, query viết bằng vòng lặp như các ví dụ trước.

Bệ đầu có dp0=0dp_0 = 0, thêm đường của nó trước vòng lặp. Mỗi bệ sau: hỏi tại hih_i, cộng hi2+Ch_i^2 + C, rồi thêm đường của chính nó.

C++
insert(-2 * h[0], h[0] * h[0]);
for (int i = 1; i < n; i++) {
    dp[i] = query(h[i]) + h[i] * h[i] + c;
    insert(-2 * h[i], dp[i] + h[i] * h[i]);
}

Độ lớn: hệ số góc tới 2⋅1062 \cdot 10^6 nhân xx tới 10610^6 là 2⋅10122 \cdot 10^{12}; dpdp không quá n(C+1012)n (C + 10^{12}), khoảng 4⋅10174 \cdot 10^{17}, vẫn vừa long long.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int X = 1e6;
const ll INF = LLONG_MAX;
ll la[4 * X + 5], lb[4 * X + 5];
bool has[4 * X + 5];

void insert(ll a, ll b) {
    int x = 1, l = 1, r = X;
    while (true) {
        if (!has[x]) {
            has[x] = 1, la[x] = a, lb[x] = b;
            return;
        }
        int m = (l + r) / 2;
        bool left = a * l + b < la[x] * l + lb[x], mid = a * m + b < la[x] * m + lb[x];
        if (mid) swap(la[x], a), swap(lb[x], b);
        if (l == r) return;
        if (left != mid) x = 2 * x, r = m;
        else x = 2 * x + 1, l = m + 1;
    }
}

ll query(ll p) {
    ll res = INF;
    int x = 1, l = 1, r = X;
    while (true) {
        if (has[x]) res = min(res, la[x] * p + lb[x]);
        if (l == r) return res;
        int m = (l + r) / 2;
        if (p <= m) x = 2 * x, r = m;
        else x = 2 * x + 1, l = m + 1;
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n;
    ll c;
    cin >> n >> c;
    vector<ll> h(n), dp(n);
    for (int i = 0; i < n; i++) cin >> h[i];
    dp[0] = 0;
    insert(-2 * h[0], h[0] * h[0]);
    for (int i = 1; i < n; i++) {
        dp[i] = query(h[i]) + h[i] * h[i] + c;
        insert(-2 * h[i], dp[i] + h[i] * h[i]);
    }
    cout << dp[n - 1] << '\n';
}
6 7
2 5 6 11 12 20

Độ phức tạp: O(nlog⁡106)O(n \log 10^6).

Gộp cây

DP trên cây dpu=min⁡v∈subtree(u), v≠u(au⋅bv+dpv)dp_u = \min_{v \in \text{subtree}(u),\, v \ne u} (a_u \cdot b_v + dp_v): mỗi đỉnh vv đóng góp đường bvx+dpvb_v x + dp_v, đỉnh uu hỏi tại aua_u trên tập đường của cây con. Mỗi đỉnh giữ một cây Li Chao động; cây của uu là gộp cây của các con: merge(x, y) chèn đường ở gốc yy vào xx rồi gộp đệ quy các cặp con trái, con phải. Mỗi đường bị đẩy xuống sâu hơn sau mỗi lần chèn, nên tổng công là O(nlog⁡C)O(n \log C) lần đẩy cộng O(nlog⁡C)O(n \log C) lần gộp.

Xử lý đỉnh theo thứ tự BFS ngược để con xong trước cha, khỏi đệ quy theo cây.

Ví dụNhảy Xuống Lá

Đề. Cây n≤105n \le 10^5 đỉnh, gốc 1, ∣ai∣,∣bi∣≤105|a_i|, |b_i| \le 10^5. Từ uu không phải lá nhảy tới hậu duệ nghiêm ngặt vv tốn aubva_u b_v, nhảy tiếp tới khi tới lá. In chi phí nhỏ nhất từ mỗi đỉnh (lá là 0).

Lời giải. dpu=min⁡v(aubv+dpv)dp_u = \min_v (a_u b_v + dp_v) trên mọi hậu duệ nghiêm ngặt vv, lá có dp=0dp = 0. Với uu cố định, aubv+dpva_u b_v + dp_v là giá trị tại x=aux = a_u của đường bvx+dpvb_v x + dp_v. Vậy mỗi đỉnh giữ một cây Li Chao chứa đường của mọi đỉnh trong cây con của nó. Cây của uu là gộp cây của các con; hỏi tại aua_u cho dpudp_u; rồi mới thêm đường của chính uu, để uu không tự nhảy tới chính nó. insert, query là Li Chao động trên [−105,105][-10^5, 10^5] như ví dụ đầu bài.

Bước 1: gộp hai cây Li Chao. Chèn đường ở gốc y vào cây x, rồi gộp đệ quy hai cặp con. Nút y bị bỏ sau khi đường của nó được chèn vào x; các nút con của y được dùng lại. Mỗi đường chỉ đi xuống sâu hơn sau mỗi lần bị chèn, độ sâu tối đa O(log⁡C)O(\log C), nên tổng công của mọi lần gộp là O(nlog⁡C)O(n \log C).

C++
int merge(int x, int y, ll l, ll r) {
    if (!x || !y) return x | y;
    insert(x, l, r, la[y], lb[y]);
    ll m = (l + r) >> 1;
    lc[x] = merge(lc[x], lc[y], l, m);
    rc[x] = merge(rc[x], rc[y], m + 1, r);
    return x;
}

Bước 2: duyệt từ lá lên. BFS từ gốc lấy par và order; duyệt order từ cuối thì con xong trước cha. Đỉnh không có con là lá, dp=0dp = 0.

C++
for (int i = n - 1; i >= 0; i--) {
    int u = order[i];
    bool leaf = 1;
    for (int v : adj[u])
        if (v != par[u]) leaf = 0, root[u] = merge(root[u], root[v], L, R);
    dp[u] = leaf ? 0 : query(root[u], a[u]);
    insert(root[u], L, R, b[u], dp[u]);
}

Độ lớn: ∣aubv∣≤1010|a_u b_v| \le 10^{10}, đường dài tới 10510^5 bước nên ∣dp∣≤1015|dp| \le 10^{15}, vừa long long. Mỗi lần insert tạo tối đa một nút, nên mảng M = 4e6 nút là dư.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 1e5 + 5, M = 4e6 + 5;
const ll L = -1e5, R = 1e5, INF = LLONG_MAX;
int n, tot, root[N], par[N], lc[M], rc[M];
ll a[N], b[N], dp[N], la[M], lb[M];
vector<int> adj[N];

void insert(int &x, ll l, ll r, ll p, ll q) {
    if (!x) {
        x = ++tot, la[x] = p, lb[x] = q;
        return;
    }
    ll m = (l + r) >> 1;
    bool left = p * l + q < la[x] * l + lb[x], mid = p * m + q < la[x] * m + lb[x];
    if (mid) swap(la[x], p), swap(lb[x], q);
    if (l == r) return;
    if (left != mid) insert(lc[x], l, m, p, q);
    else insert(rc[x], m + 1, r, p, q);
}

int merge(int x, int y, ll l, ll r) {
    if (!x || !y) return x | y;
    insert(x, l, r, la[y], lb[y]);
    ll m = (l + r) >> 1;
    lc[x] = merge(lc[x], lc[y], l, m);
    rc[x] = merge(rc[x], rc[y], m + 1, r);
    return x;
}

ll query(int x, ll p) {
    ll res = INF, l = L, r = R;
    while (x) {
        res = min(res, la[x] * p + lb[x]);
        ll m = (l + r) >> 1;
        if (p <= m) x = lc[x], r = m;
        else x = rc[x], l = m + 1;
    }
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    vector<int> order = {1};
    for (int i = 0; i < (int)order.size(); i++) {
        int u = order[i];
        for (int v : adj[u])
            if (v != par[u]) par[v] = u, order.push_back(v);
    }
    for (int i = n - 1; i >= 0; i--) {
        int u = order[i];
        bool leaf = 1;
        for (int v : adj[u])
            if (v != par[u]) leaf = 0, root[u] = merge(root[u], root[v], L, R);
        dp[u] = leaf ? 0 : query(root[u], a[u]);
        insert(root[u], L, R, b[u], dp[u]);
    }
    for (int i = 1; i <= n; i++) cout << dp[i] << ' ';
    cout << '\n';
}
6
4 -2 3 5 1 7
2 6 -1 4 3 -5
1 2
1 3
2 4
2 5
3 6

Độ phức tạp: O(nlog⁡C)O(n \log C) với C=2⋅105C = 2 \cdot 10^5.

Khi đi thi

  • DP mà mỗi trạng thái cũ cho một hàm bậc nhất của điểm hỏi, hệ số góc hay điểm hỏi không đơn điệu: cây Li Chao, O(log⁡C)O(\log C) mỗi thao tác. Đơn điệu cả hai thì bao lồi với con trỏ gọn hơn.
  • Miền xx lớn: cây động, mỗi lần chèn tạo một nút. Biết trước điểm hỏi: cây tĩnh, nén điểm hỏi khi cần.
  • Đường có miền hiệu lực: chèn vào các nút phủ, O(log⁡2C)O(\log^2 C). DP trên cây: gộp cây Li Chao của các con.

Tóm tắt

  • Mỗi nút giữ đường tốt nhất tại điểm giữa; đường thua đi về nửa duy nhất nó có thể thắng.
  • Hỏi lấy nhỏ nhất trên đường từ gốc tới lá chứa điểm hỏi.
  • Đoạn thẳng: chèn Li Chao trong các nút phủ khoảng hiệu lực.
  • Gộp: chèn đường gốc của cây này vào cây kia, gộp đệ quy hai con.

Bài tập

Đọc thêm