Chương 4·Bài 42

Convex Hull Trick

Bạn có một bảng giá cước: mỗi nhà mạng jj thu phí cố định mjm_j cộng kjk_j cho mỗi đơn vị dùng. Với lượng dùng xx, nhà mạng rẻ nhất là ai? Mỗi nhà mạng là một đường thẳng y=kjx+mjy = k_j x + m_j, và câu hỏi là đường nào thấp nhất tại xx.

Nhiều quy hoạch động có chuyển tiếp đúng dạng ấy: dpi=min⁡j<i(dpj+bj⋅xi)dp_i = \min_{j < i} (dp_j + b_j \cdot x_i), hoặc đưa về được dạng đó sau khi khai triển một bình phương. Tập các đường chỉ cần giữ phần bao dưới: những đường từng là nhỏ nhất ở một khoảng xx nào đó.

Khi hệ số góc thêm vào đơn điệu và điểm hỏi cũng đơn điệu, bao dưới giữ bằng một hàng đợi hai đầu và mỗi bước tốn O(1)O(1) trung bình, đưa O(n2)O(n^2) về O(n)O(n). Đây là kỹ thuật bao lồi đường thẳng (convex hull trick, CHT).

Cực tiểu các đường thẳng

Viết chuyển tiếp thành dpi=min⁡j(kj⋅xi+mj)+cidp_i = \min_j (k_j \cdot x_i + m_j) + c_i, trong đó kjk_j và mjm_j chỉ phụ thuộc jj, xix_i và cic_i chỉ phụ thuộc ii. Mỗi jj đã tính xong cho một đường thẳng y=kjx+mjy = k_j x + m_j; hỏi dpidp_i là hỏi đường nào thấp nhất tại xix_i. Tập các đường thấp nhất theo xx tạo thành một bao dưới lồi: đi từ trái sang phải, hệ số góc của đường đang thấp nhất giảm dần. Một đường nằm hoàn toàn trên bao không bao giờ cần nữa.

Đường xanh dương đang trong hàng đợi, đường cong xanh lá là bao dưới. Kéo x để hỏi đường thấp nhất.

Ví dụDãy Chuyển Tiếp Đơn Điệu

Đề. Hai dãy aa tăng ngặt (a1=1a_1 = 1) và bb giảm ngặt (bn=0b_n = 0), n≤105n \le 10^5. dp1=0dp_1 = 0, dpi=min⁡j<i(dpj+bj⋅ai)dp_i = \min_{j < i} (dp_j + b_j \cdot a_i). In dpndp_n.

Lời giải. Mỗi jj cho một đường thẳng y=bj⋅x+dpjy = b_j \cdot x + dp_j; tính dpidp_i là hỏi giá trị nhỏ nhất của các đường tại x=aix = a_i. Hệ số góc giảm ngặt khi jj tăng, điểm hỏi tăng ngặt: đúng trường hợp hàng đợi hai đầu ở mục sau. Ở đây viết sẵn khung đó.

Bước 1: đường nào vô dụng. Bao lồi lưu trong hai mảng K (hệ số góc), M (tung độ gốc), dùng như hàng đợi hai đầu từ hd tới tl - 1. Đường giữa yy vô dụng khi giao điểm của xx với zz nằm không xa hơn giao điểm của xx với yy; so bằng nhân chéo để khỏi chia, và tích tới 1018⋅10910^{18} \cdot 10^9 nên dùng __int128.

C++
bool bad(ll x, ll y, ll z) {
    return (__int128)(M[z] - M[x]) * (K[x] - K[y]) <= (__int128)(M[y] - M[x]) * (K[x] - K[z]);
}

Bước 2: thêm đường vào cuối. Ghi đường mới vào ô tl; chừng nào đường áp chót vô dụng thì chép đường mới đè lên nó và lùi tl; cuối cùng tăng tl.

C++
void add(ll k, ll m) {
    K[tl] = k, M[tl] = m;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

Bước 3: hỏi ở đầu. Điểm hỏi tăng dần nên đường tốt nhất chỉ dịch sang phải: bỏ đường đầu khi đường thứ hai đã không tệ hơn tại xx.

C++
ll get(ll x) {
    while (tl - hd >= 2 && K[hd + 1] * x + M[hd + 1] <= K[hd] * x + M[hd]) hd++;
    return K[hd] * x + M[hd];
}

Bước 4: quy hoạch động. Hỏi tại aia_i được dpidp_i, rồi thêm đường của chính ii cho các bước sau.

C++
add(b[1], dp[1]);
for (ll i = 2; i <= n; i++) {
    dp[i] = get(a[i]);
    add(b[i], dp[i]);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, a[N], b[N], dp[N], K[N], M[N], hd, tl;

bool bad(ll x, ll y, ll z) {
    return (__int128)(M[z] - M[x]) * (K[x] - K[y]) <= (__int128)(M[y] - M[x]) * (K[x] - K[z]);
}

void add(ll k, ll m) {
    K[tl] = k, M[tl] = m;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

ll get(ll x) {
    while (tl - hd >= 2 && K[hd + 1] * x + M[hd + 1] <= K[hd] * x + M[hd]) hd++;
    return K[hd] * x + M[hd];
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    for (ll i = 1; i <= n; i++) cin >> b[i];
    dp[1] = 0;
    add(b[1], dp[1]);
    for (ll i = 2; i <= n; i++) {
        dp[i] = get(a[i]);
        add(b[i], dp[i]);
    }
    cout << dp[n] << '\n';
}
4
1 2 4 7
9 6 3 0

Độ phức tạp: mỗi đường vào và ra hàng đợi một lần: O(n)O(n).

So với thử mọi jj trên cùng bài:

Cùng quy hoạch động, 40 nghìn bước, trung vị 5 lần chạy, i7-10850H, g++ -O2.hàng đợi hai đầu0,6 msthử mọi j, O(n²)553 ms · gấp 922 lần
Quy hoạch động chuyển tiếp đường thẳng, 40 nghìn bước, đo trên i7-10850H, g++ -O2.

Hàng đợi hai đầu nhanh hơn cỡ nghìn lần ở n=40000n = 40000.

Hàng đợi hai đầu

Khi đường được thêm theo hệ số góc giảm dần (cho bài min), đường mới luôn thấp nhất ở rất xa bên phải, nên được đặt vào cuối bao. Đường áp chót BB giữa AA (trước nó) và CC (đường mới) trở nên vô dụng khi giao điểm của AA và CC nằm không xa hơn giao điểm của AA và BB về bên trái: lúc đó trên mọi xx, hoặc AA hoặc CC thấp hơn BB. So hai giao điểm bằng nhân chéo, không chia:

mC−mAkA−kC≤mB−mAkA−kB  ⟺  (mC−mA)(kA−kB)≤(mB−mA)(kA−kC).\frac{m_C - m_A}{k_A - k_C} \le \frac{m_B - m_A}{k_A - k_B} \iff (m_C - m_A)(k_A - k_B) \le (m_B - m_A)(k_A - k_C).

Khi điểm hỏi tăng dần, đường tốt nhất chỉ dịch sang phải trên bao: bỏ đường đầu khi đường thứ hai đã không tệ hơn tại xx hiện tại. Mỗi đường vào và ra hàng đợi một lần, tổng O(n)O(n). Nếu điểm hỏi không đơn điệu, giữ nguyên bao và tìm kiếm nhị phân đường tốt nhất, O(log⁡n)O(\log n) mỗi câu; đo trên 2 triệu bước, cách này chỉ chậm hơn hàng đợi cỡ 1,2 lần. Nếu cả hệ số góc cũng không đơn điệu, dùng cây Li Chao (bài 75).

Hỏi nhanh

Bao đang có đường AA, BB (hệ số góc giảm dần), thêm đường CC có hệ số góc nhỏ hơn nữa. Khi nào bỏ BB?

Ví dụHệ Số Sau Mỗi Trạm

Đề. n≤2⋅105n \le 2 \cdot 10^5 trạm có tải s1≤⋯≤sns_1 \le \dots \le s_n, hệ số x≥f1≥⋯≥fnx \ge f_1 \ge \dots \ge f_n. Xử lý trạm ii với hệ số cc tốn c⋅sic \cdot s_i rồi hệ số thành fif_i. Phải xử lý trạm nn; tìm chi phí nhỏ nhất.

Lời giải. dpidp_i là chi phí nhỏ nhất khi trạm ii là trạm xử lý gần nhất: dpi=min⁡(x⋅si, min⁡j<i(dpj+fj⋅si))dp_i = \min(x \cdot s_i,\ \min_{j < i} (dp_j + f_j \cdot s_i)). Đường ban đầu (x,0)(x, 0) ứng với chưa xử lý trạm nào, hệ số vẫn là xx; đường của jj có hệ số góc fjf_j, tung độ gốc dpjdp_j. Hệ số góc không tăng, điểm hỏi sis_i không giảm: dùng khung hàng đợi của ví dụ trước. Đáp án là dpdp của trạm cuối, vì trạm nn bắt buộc phải xử lý.

Bước 1: hai đường cùng hệ số góc. Hệ số góc chỉ không tăng nên có thể bằng nhau. Khi đó chỉ giữ đường có tung độ gốc nhỏ hơn; không xử lý trường hợp này thì bad sẽ nhân với hiệu hệ số góc bằng 0 và cho kết quả sai.

C++
void add(ll k, ll m) {
    if (tl > hd && K[tl - 1] == k) {
        if (M[tl - 1] <= m) return;
        tl--;
    }
    K[tl] = k, M[tl] = m;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

Bước 2: quy hoạch động. bad và get như ví dụ trước.

C++
add(x, 0);
ll dp = 0;
for (ll i = 1; i <= n; i++) {
    dp = get(s[i]);
    add(f[i], dp);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5;
ll n, x, s[N], f[N], K[N], M[N], hd, tl;

bool bad(ll a, ll b, ll c) {
    return (__int128)(M[c] - M[a]) * (K[a] - K[b]) <= (__int128)(M[b] - M[a]) * (K[a] - K[c]);
}

void add(ll k, ll m) {
    if (tl > hd && K[tl - 1] == k) {
        if (M[tl - 1] <= m) return;
        tl--;
    }
    K[tl] = k, M[tl] = m;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

ll get(ll q) {
    while (tl - hd >= 2 && K[hd + 1] * q + M[hd + 1] <= K[hd] * q + M[hd]) hd++;
    return K[hd] * q + M[hd];
}

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

    cin >> n >> x;
    for (ll i = 1; i <= n; i++) cin >> s[i];
    for (ll i = 1; i <= n; i++) cin >> f[i];
    add(x, 0);
    ll dp = 0;
    for (ll i = 1; i <= n; i++) {
        dp = get(s[i]);
        add(f[i], dp);
    }
    cout << dp << '\n';
}
3 5
1 3 6
4 2 1

Độ phức tạp: O(n)O(n).

Khai triển bình phương

Chi phí kiểu (hi−hj)2(h_i - h_j)^2 hay (Si−Sj)2(S_i - S_j)^2 không phải đường thẳng theo ii, nhưng khai triển ra thì có: (hi−hj)2=hi2−2hihj+hj2(h_i - h_j)^2 = h_i^2 - 2 h_i h_j + h_j^2. Phần hi2h_i^2 chỉ phụ thuộc ii, đưa ra ngoài min; phần còn lại là đường có hệ số góc −2hj-2h_j, tung độ gốc dpj+hj2dp_j + h_j^2, hỏi tại x=hix = h_i. Dãy hh tăng thì hệ số góc giảm và điểm hỏi tăng, đúng trường hợp hàng đợi hai đầu. Với chia dãy thành đoạn và chi phí bình phương tổng đoạn, làm tương tự với tổng tiền tố SS. Giá trị trung gian có thể tới 101710^{17} nhân 10610^6, nên phép so sánh giao điểm cần __int128.

Hỏi nhanh

Chuyển tiếp dpi=min⁡j(dpj+(hi−hj)2)+cdp_i = \min_j \left(dp_j + (h_i - h_j)^2\right) + c. Sau khi khai triển, đường của jj có hệ số góc và tung độ gốc là gì?

Ví dụBậc Đá Tăng Dần

Đề. n≤2⋅105n \le 2 \cdot 10^5 bậc với độ cao tăng ngặt tới 10610^6. Nhảy từ jj tới i>ji > j tốn (hi−hj)2+c(h_i - h_j)^2 + c. Tìm chi phí nhỏ nhất từ bậc 1 tới bậc nn.

Lời giải. Khai triển bình phương: dpi=min⁡j<i(dpj+hj2−2hjhi)+hi2+cdp_i = \min_{j < i} (dp_j + h_j^2 - 2h_j h_i) + h_i^2 + c. Phần trong ngoặc là đường thẳng của jj tại x=hix = h_i: hệ số góc −2hj-2h_j (giảm dần), tung độ gốc dpj+hj2dp_j + h_j^2. Độ cao tăng ngặt nên không có hai đường cùng hệ số góc, add không cần bước kiểm đó.

Bước 1: khung hàng đợi bad, add, get như ví dụ đầu.

Bước 2: quy hoạch động. Bậc 1 có dp1=0dp_1 = 0. Mỗi bậc hỏi tại hih_i (tăng dần), cộng hi2+ch_i^2 + c, rồi thêm đường của chính nó.

C++
add(-2 * h[1], h[1] * h[1]);
for (ll i = 2; i <= n; i++) {
    dp[i] = get(h[i]) + h[i] * h[i] + c;
    add(-2 * h[i], dp[i] + h[i] * h[i]);
}

Đáp án tới khoảng 2⋅10172 \cdot 10^{17}.

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

const ll N = 2e5 + 5;
ll n, c, h[N], dp[N], K[N], M[N], hd, tl;

bool bad(ll a, ll b, ll d) {
    return (__int128)(M[d] - M[a]) * (K[a] - K[b]) <= (__int128)(M[b] - M[a]) * (K[a] - K[d]);
}

void add(ll k, ll m) {
    K[tl] = k, M[tl] = m;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

ll get(ll x) {
    while (tl - hd >= 2 && K[hd + 1] * x + M[hd + 1] <= K[hd] * x + M[hd]) hd++;
    return K[hd] * x + M[hd];
}

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

    cin >> n >> c;
    for (ll i = 1; i <= n; i++) cin >> h[i];
    add(-2 * h[1], h[1] * h[1]);
    for (ll i = 2; i <= n; i++) {
        dp[i] = get(h[i]) + h[i] * h[i] + c;
        add(-2 * h[i], dp[i] + h[i] * h[i]);
    }
    cout << dp[n] << '\n';
}
4 3
1 2 5 8

Độ phức tạp: O(n)O(n).

Ví dụChia Bản Thảo

Đề. Nhiều bộ tới hết input. Dãy n≤5⋅105n \le 5 \cdot 10^5 số không âm và hằng số mm; chia dãy thành đoạn, đoạn có tổng ss tốn s2+ms^2 + m. Tìm tổng nhỏ nhất.

Lời giải. Với tổng tiền tố SS: dpi=Si2+m+min⁡j<i(dpj+Sj2−2SjSi)dp_i = S_i^2 + m + \min_{j < i} (dp_j + S_j^2 - 2S_j S_i), dp0=0dp_0 = 0. Đường của jj có hệ số góc −2Sj-2S_j, tung độ gốc dpj+Sj2dp_j + S_j^2; j=0j = 0 cho đường (0,0)(0, 0). Các số có thể bằng 0 nên SS chỉ không giảm: hai đường liên tiếp có thể cùng hệ số góc, nên dùng bản add có kiểm cùng hệ số góc như ví dụ hệ số trạm.

Bước 1: đọc thẳng vào tổng tiền tố, đặt lại hàng đợi mỗi bộ.

C++
for (ll i = 1; i <= n; i++) {
    cin >> S[i];
    S[i] += S[i - 1];
}
hd = tl = 0;
add(0, 0);

Bước 2: quy hoạch động. dp chỉ cần giá trị hiện tại, vì đường của ii đã mang theo nó.

C++
ll dp = 0;
for (ll i = 1; i <= n; i++) {
    dp = get(S[i]) + S[i] * S[i] + m;
    add(-2 * S[i], dp + S[i] * S[i]);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 5e5 + 5;
ll n, m, S[N], K[N], M[N], hd, tl;

bool bad(ll a, ll b, ll c) {
    return (__int128)(M[c] - M[a]) * (K[a] - K[b]) <= (__int128)(M[b] - M[a]) * (K[a] - K[c]);
}

void add(ll k, ll v) {
    if (tl > hd && K[tl - 1] == k) {
        if (M[tl - 1] <= v) return;
        tl--;
    }
    K[tl] = k, M[tl] = v;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

ll get(ll x) {
    while (tl - hd >= 2 && K[hd + 1] * x + M[hd + 1] <= K[hd] * x + M[hd]) hd++;
    return K[hd] * x + M[hd];
}

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

    while (cin >> n >> m) {
        for (ll i = 1; i <= n; i++) {
            cin >> S[i];
            S[i] += S[i - 1];
        }
        hd = tl = 0;
        add(0, 0);
        ll dp = 0;
        for (ll i = 1; i <= n; i++) {
            dp = get(S[i]) + S[i] * S[i] + m;
            add(-2 * S[i], dp + S[i] * S[i]);
        }
        cout << dp << '\n';
    }
}
4 2
1 2 1 3

Độ phức tạp: mỗi bộ O(n)O(n).

Bỏ phần tử bị trội

Khi dữ liệu chưa có thứ tự, hãy tìm thứ tự làm chuyển tiếp thành đoạn liên tiếp và làm hệ số góc, điểm hỏi đơn điệu. Với các hình chữ nhật mua theo nhóm, chi phí nhóm là max⁡w⋅max⁡h\max w \cdot \max h. Một hình nằm gọn trong hình khác (cả ww lẫn hh không lớn hơn) đi cùng hình kia mà không tốn thêm, nên bỏ nó. Sau khi bỏ, sắp theo ww tăng thì hh giảm ngặt, và nhóm tối ưu luôn là một đoạn liên tiếp: chi phí đoạn [j+1,i][j + 1, i] là wi⋅hj+1w_i \cdot h_{j+1}.

Deque chỉ loại đường ở đầu khi thứ tự truy vấn đúng với chiều bao.Sắp slope và x trước khi dùng dequem giảm dầnx tăng dần
Sau khi bỏ hình bị chứa và sắp: hệ số góc giảm dần, điểm hỏi tăng dần, đúng điều kiện của hàng đợi hai đầu.

Ví dụGom Lô Đất

Đề. n≤5⋅104n \le 5 \cdot 10^4 lô đất wi×hiw_i \times h_i. Mua một nhóm tốn chiều rộng lớn nhất nhân chiều cao lớn nhất của nhóm. Chia thành các nhóm để tổng chi phí nhỏ nhất.

Lời giải. Một lô vừa hẹp hơn vừa thấp hơn một lô khác thì đi chung nhóm với lô đó miễn phí, nên bỏ được. Sau khi bỏ, sắp theo ww tăng thì hh giảm ngặt, và một nhóm tốt nhất luôn là một đoạn liên tiếp: nhóm j+1..ij + 1..i tốn wi⋅hj+1w_i \cdot h_{j+1} (rộng nhất là lô cuối, cao nhất là lô đầu). Vậy dpi=min⁡j<i(dpj+hj+1⋅wi)dp_i = \min_{j < i} (dp_j + h_{j+1} \cdot w_i): đường của jj có hệ số góc hj+1h_{j+1} giảm dần, điểm hỏi wiw_i tăng dần.

Bước 1: bỏ lô bị chứa. Sắp theo (w,h)(w, h); dùng ngăn xếp: lô mới cao không kém lô trên đỉnh thì lô trên đỉnh nằm gọn trong lô mới (hẹp hơn, thấp hơn), bỏ nó.

C++
sort(p + 1, p + n + 1);
for (ll i = 1; i <= n; i++) {
    while (m && h[m] <= p[i].second) m--;
    m++;
    w[m] = p[i].first;
    h[m] = p[i].second;
}

Bước 2: quy hoạch động. Đường của jj là (hj+1,dpj)(h_{j+1}, dp_j), thêm ngay sau khi có dpjdp_j (với j=0j = 0 là dp0=0dp_0 = 0). Lô cuối không cần thêm đường vì không còn lô nào sau nó.

C++
ll dp = 0;
add(h[1], 0);
for (ll i = 1; i <= m; i++) {
    dp = get(w[i]);
    if (i < m) add(h[i + 1], dp);
}

Chi phí tới 5⋅104⋅10125 \cdot 10^4 \cdot 10^{12}, dùng long long; so sánh giao điểm trong __int128.

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

const ll N = 5e4 + 5;
ll n, m, w[N], h[N], K[N], M[N], hd, tl;
pair<ll, ll> p[N];

bool bad(ll a, ll b, ll c) {
    return (__int128)(M[c] - M[a]) * (K[a] - K[b]) <= (__int128)(M[b] - M[a]) * (K[a] - K[c]);
}

void add(ll k, ll v) {
    K[tl] = k, M[tl] = v;
    while (tl - hd >= 2 && bad(tl - 2, tl - 1, tl)) {
        K[tl - 1] = K[tl], M[tl - 1] = M[tl];
        tl--;
    }
    tl++;
}

ll get(ll x) {
    while (tl - hd >= 2 && K[hd + 1] * x + M[hd + 1] <= K[hd] * x + M[hd]) hd++;
    return K[hd] * x + M[hd];
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> p[i].first >> p[i].second;
    sort(p + 1, p + n + 1);
    for (ll i = 1; i <= n; i++) {
        while (m && h[m] <= p[i].second) m--;
        m++;
        w[m] = p[i].first;
        h[m] = p[i].second;
    }
    ll dp = 0;
    add(h[1], 0);
    for (ll i = 1; i <= m; i++) {
        dp = get(w[i]);
        if (i < m) add(h[i + 1], dp);
    }
    cout << dp << '\n';
}
3
2 5
4 3
6 2

Độ phức tạp: O(nlog⁡n)O(n \log n) cho phần sắp, phần còn lại O(n)O(n).

Khi đi thi

  • Chuyển tiếp có tích f(i)⋅g(j)f(i) \cdot g(j) hoặc bình phương hiệu: tách thành phần theo ii, phần theo jj và tích chéo; tích chéo cho hệ số góc và điểm hỏi.
  • Kiểm tính đơn điệu: hệ số góc thêm vào có giảm (bài min) và điểm hỏi có tăng không. Không đơn điệu điểm hỏi thì tìm kiếm nhị phân trên bao; không đơn điệu hệ số góc thì cây Li Chao (bài 75).
  • So giao điểm bằng nhân chéo với __int128, xử lý hai đường cùng hệ số góc riêng; bài max thì đổi dấu hoặc đảo chiều các bất đẳng thức.

Tóm tắt

  • dpi=min⁡j(kjxi+mj)+cidp_i = \min_j (k_j x_i + m_j) + c_i: mỗi jj là một đường, cần bao dưới tại xix_i.
  • Hệ số góc giảm, điểm hỏi tăng: hàng đợi hai đầu, thêm ở cuối bỏ đường vô dụng, hỏi ở đầu bỏ đường đã thua; O(n)O(n).
  • Đường BB giữa AA và CC vô dụng khi (mC−mA)(kA−kB)≤(mB−mA)(kA−kC)(m_C - m_A)(k_A - k_B) \le (m_B - m_A)(k_A - k_C).
  • Bình phương hiệu khai triển thành đường; sắp và bỏ phần tử bị trội để có thứ tự đơn điệu.

Bài tập

Đọc thêm