Chương 7·Bài 78

Aliens Trick

Nhiều DP có thêm ràng buộc “chọn đúng (hay không quá) KK thứ”, khiến trạng thái phải mang chiều số lượng và tốn O(NK)O(NK). Nếu đáp án f(k)f(k) theo số lượng kk là hàm lồi (hay lõm), Alien trick (nới lỏng Lagrange) bỏ chiều đó: cộng một khoản phạt λ\lambda cho mỗi lần chọn, giải DP không giới hạn số lượng, rồi chặt nhị phân λ\lambda để số lần chọn tối ưu đúng bằng KK. Tổng O(Nlog⁡C)O(N \log C).

Phạt mỗi lần chọn

Cần max⁡\max trên mọi phương án có không quá KK đoạn. Gọi f(k)f(k) là giá trị tốt nhất khi dùng đúng kk đoạn. Với khoản phạt λ≥0\lambda \ge 0 trừ vào mỗi đoạn, giải bài không giới hạn số đoạn:

g(λ)=max⁡k(f(k)−λk).g(\lambda) = \max_k \big(f(k) - \lambda k\big).

Đây là DP thường, không có chiều kk, chỉ ghi kèm số đoạn của phương án tối ưu. Phạt càng lớn thì số đoạn tối ưu càng nhỏ. Nếu ff lõm (lợi ích của đoạn thêm vào giảm dần), mọi kk đều là tối ưu ở một λ\lambda nào đó: các đường f(k)−λkf(k) - \lambda k theo λ\lambda tạo thành bao lồi mà không đường nào bị che khuất. Tìm λ\lambda để số đoạn tối ưu bằng KK thì f(K)=g(λ)+λKf(K) = g(\lambda) + \lambda K.

Bài mẫu: chọn k phần tử không kề nhau, tổng lớn nhất. Mỗi đường là f(k) − λk theo λ; kéo λ, đường cao nhất (cam) cho k tối ưu và các ô được chọn.

Khi nhiều kk cùng tối ưu. Với ff nguyên và λ\lambda nguyên, ở một số λ\lambda cả đoạn k∈[cmin⁡,cmax⁡]k \in [c_{\min}, c_{\max}] cùng tối ưu, và có thể không λ\lambda nào cho đúng KK là duy nhất. Cố định cách phá hòa: luôn chọn số đoạn lớn nhất khi bằng giá trị, rồi tìm λ\lambda lớn nhất còn có cmax⁡(λ)≥Kc_{\max}(\lambda) \ge K. Ở λ+1\lambda + 1 số đoạn tụt dưới KK, nên cmin⁡(λ)<K≤cmax⁡(λ)c_{\min}(\lambda) < K \le c_{\max}(\lambda): KK nằm trong đoạn tối ưu và g(λ)+λKg(\lambda) + \lambda K vẫn đúng bằng f(K)f(K). Ghép sai chiều (phá hòa chọn nhiều mà lại tìm λ\lambda nhỏ nhất có cmax⁡≤Kc_{\max} \le K) sẽ lệch đúng một bậc ở các ca hòa.

Ví dụNhững Đoạn Hài Lòng

Đề. Dãy N≤3⋅105N \le 3 \cdot 10^5 số, ∣Ai∣≤109|A_i| \le 10^9. Chọn không quá KK đoạn rời nhau, tổng phần tử được chọn lớn nhất.

Lời giải. Gọi f(j)f(j) là tổng lớn nhất khi dùng đúng jj đoạn; ff lõm theo jj. Thay ràng buộc số đoạn bằng mức phạt λ\lambda mỗi lần mở đoạn mới, quy hoạch động không còn chiều jj. Chặt nhị phân λ∈[0,3⋅1014]\lambda \in [0, 3 \cdot 10^{14}] để số đoạn tối ưu chạm KK.

Bước 1: quy hoạch động có phạt. Mỗi trạng thái là cặp (giá trị, số đoạn). in là đang ở trong một đoạn, out là đang ở ngoài. Vào đoạn từ ngoài thì trừ lam và tăng số đoạn; ở trong thì cộng tiếp aia_i. LLONG_MIN / 4 làm trạng thái “không thể” mà cộng thêm vẫn không tràn.

C++
pair<ll, ll> out = {0, 0}, in = {LLONG_MIN / 4, 0};
for (int i = 1; i <= n; i++) {
    pair<ll, ll> nin = max(make_pair(in.first + a[i], in.second), make_pair(out.first + a[i] - lam, out.second + 1));
    pair<ll, ll> nout = max(out, in);
    in = nin, out = nout;
}

So cặp bằng max của pair: giá trị bằng nhau thì lấy cặp có số đoạn lớn hơn. Quy ước phá hòa này phải giống nhau ở mọi chỗ, để số đoạn trả về là số lớn nhất trong các phương án tối ưu với mức phạt đó.

Bước 2: chặt mức phạt. Số đoạn chỉ giảm khi lam tăng. Tìm res lớn nhất mà số đoạn vẫn ít nhất KK; tại đó đường thẳng độ dốc res chạm ff ở KK, nên f(K)=g(res)+res⋅Kf(K) = g(res) + res \cdot K.

C++
while (l <= h) {
    ll mid = (l + h) / 2;
    if (solve(mid).second >= k) res = mid, l = mid + 1;
    else h = mid - 1;
}
cout << solve(res).first + res * k << '\n';

Nếu ngay lam = 0 số đoạn đã dưới KK thì res = 0, và công thức cho đúng cực đại không giới hạn, tức đáp án của “không quá KK”.

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

const int N = 3e5 + 5;
int n, k;
ll a[N];

pair<ll, ll> solve(ll lam) {
    pair<ll, ll> out = {0, 0}, in = {LLONG_MIN / 4, 0};
    for (int i = 1; i <= n; i++) {
        pair<ll, ll> nin = max(make_pair(in.first + a[i], in.second), make_pair(out.first + a[i] - lam, out.second + 1));
        pair<ll, ll> nout = max(out, in);
        in = nin, out = nout;
    }
    return max(out, in);
}

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

    cin >> n >> k;
    for (int i = 1; i <= n; i++) cin >> a[i];
    ll l = 0, h = 3e14, res = 0;
    while (l <= h) {
        ll mid = (l + h) / 2;
        if (solve(mid).second >= k) res = mid, l = mid + 1;
        else h = mid - 1;
    }
    cout << solve(res).first + res * k << '\n';
}
7 2
4 -6 5 3 -10 7 2

O(Nlog⁡3⋅1014)≈49NO(N \log 3 \cdot 10^{14}) \approx 49N.

Chọn không kề nhau

Bài cực tiểu thì làm đối xứng: thưởng λ\lambda cho mỗi lần chọn (trừ vào chi phí), thưởng càng lớn càng chọn nhiều; f(k)f(k) phải lồi. Tìm λ\lambda nhỏ nhất có số lần chọn (phá hòa bằng chọn nhiều nhất) ít nhất KK, đáp án g(λ)+λKg(\lambda) + \lambda K.

Bài chọn KK khoảng hở không kề nhau có f(k)f(k) lồi: tương đương một luồng chi phí nhỏ nhất, mỗi đơn vị luồng thêm không rẻ hơn đơn vị trước. Kiểm tính lồi theo cách này, hoặc thử bằng brute force: in f(k)f(k) cho kk nhỏ và xem hiệu kề có tăng dần không.

Hỏi nhanh

Các khoảng hở 3,3,3,10,13, 3, 3, 10, 1 (theo thứ tự), chọn 2 khoảng không kề nhau với tổng nhỏ nhất. Đáp án là bao nhiêu?

Ví dụCa Gác Tách Biệt

Đề. N≤5⋅105N \le 5 \cdot 10^5 thời điểm phân biệt, chọn K≤5000K \le 5000 cuộc gặp (mỗi cuộc gặp dùng hai thời điểm làm đầu mút) không chồng nhau, không chung đầu mút, tổng độ dài nhỏ nhất.

Lời giải. Sắp các thời điểm; g[i] là khoảng hở thứ ii giữa hai thời điểm liên tiếp. Mỗi cuộc gặp tối ưu là một khoảng hở, và hai cuộc gặp không chung đầu mút nghĩa là hai khoảng được chọn không kề nhau. Bài cực tiểu nên dùng thưởng λ\lambda cho mỗi lần chọn: thưởng càng lớn càng chọn nhiều. Chặt nhị phân λ∈[0,109]\lambda \in [0, 10^9].

Bước 1: quy hoạch động có thưởng. take là khoảng hở cuối được chọn, skip là không; chọn khoảng ii chỉ được từ skip, cộng g[i] - lam. Số lần chọn lưu dưới dạng số âm, để min trên cặp tự phá hòa theo hướng chọn nhiều hơn; hàm trả về đổi lại dấu dương.

C++
pair<ll, ll> take = {LLONG_MAX / 4, 0}, skip = {0, 0};
for (int i = 1; i < n; i++) {
    pair<ll, ll> nt = {skip.first + g[i] - lam, skip.second - 1};
    pair<ll, ll> ns = min(take, skip);
    take = nt, skip = ns;
}
pair<ll, ll> r = min(take, skip);
return {r.first, -r.second};

Bước 2: chặt thưởng. Tìm res nhỏ nhất mà số lần chọn ít nhất KK, rồi cộng lại res * k.

C++
if (solve(mid).second >= k) res = mid, h = mid - 1;
else l = mid + 1;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 5e5 + 5;
int k, n;
ll t[N], g[N];

pair<ll, ll> solve(ll lam) {
    pair<ll, ll> take = {LLONG_MAX / 4, 0}, skip = {0, 0};
    for (int i = 1; i < n; i++) {
        pair<ll, ll> nt = {skip.first + g[i] - lam, skip.second - 1};
        pair<ll, ll> ns = min(take, skip);
        take = nt, skip = ns;
    }
    pair<ll, ll> r = min(take, skip);
    return {r.first, -r.second};
}

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

    cin >> k >> n;
    for (int i = 0; i < n; i++) cin >> t[i];
    sort(t, t + n);
    for (int i = 1; i < n; i++) g[i] = t[i] - t[i - 1];
    ll l = 0, h = 1e9, res = h;
    while (l <= h) {
        ll mid = (l + h) / 2;
        if (solve(mid).second >= k) res = mid, h = mid - 1;
        else l = mid + 1;
    }
    cout << solve(res).first + res * k << '\n';
}
2 6
10 1 4 20 21 7

O(Nlog⁡N+Nlog⁡109)O(N \log N + N \log 10^9).

Đúng K và đối xứng

Đề đòi đúng KK vẫn làm y hệt. Hàm f(k)f(k) có thể chỉ lõm trên một nửa miền: tô RR bóng đỏ trong NN bóng để tổng thưởng các cặp kề khác màu lớn nhất có f(R)=f(N−R)f(R) = f(N - R) (đổi màu toàn bộ), và ff lõm trên [0,N/2][0, N/2]. Thay RR bằng min⁡(R,N−R)\min(R, N - R) rồi làm như thường.

Khoảng tìm λ\lambda: từ 0 tới lợi ích lớn nhất mà một lần chọn thêm có thể mang lại (ở đây 2max⁡Ai2 \max A_i), cộng dư một ít.

Ví dụDãy Đèn Hai Màu

Đề. N≤2⋅105N \le 2 \cdot 10^5 bóng, Ai≤109A_i \le 10^9 là thưởng nếu bóng ii và i+1i + 1 khác màu. Tô đúng RR bóng đỏ, tổng thưởng lớn nhất.

Lời giải. Đổi màu toàn bộ không đổi thưởng, nên chỉ cần xét r=min⁡(R,N−R)r = \min(R, N - R), phía mà ff lõm. Quy hoạch động theo màu bóng trước, phạt λ\lambda mỗi bóng đỏ, phá hòa bằng max trên cặp để chọn nhiều đỏ hơn, như ví dụ đầu.

Bước 1: hai trạng thái màu. dp[c] là bóng hiện tại có màu cc (1 là đỏ). Mỗi bóng đỏ bị trừ lam và tăng số đỏ; cặp kề khác màu cộng a[i - 1].

C++
for (int c = 0; c < 2; c++) {
    nd[c] = {LLONG_MIN, 0};
    for (int p = 0; p < 2; p++) {
        pair<ll, ll> v = {dp[p].first + (p != c ? a[i - 1] : 0) - (c ? lam : 0), dp[p].second + c};
        nd[c] = max(nd[c], v);
    }
}

Bước 2: chặt mức phạt như ví dụ đầu. Khoảng chặt tới 3⋅1093 \cdot 10^9: lợi ích lớn nhất của một bóng đỏ thêm là hai cặp kề, tối đa 2⋅1092 \cdot 10^9, cộng dư.

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

const int N = 2e5 + 5;
int n;
ll r, a[N];

pair<ll, ll> solve(ll lam) {
    pair<ll, ll> dp[2] = {{0, 0}, {-lam, 1}};
    for (int i = 2; i <= n; i++) {
        pair<ll, ll> nd[2];
        for (int c = 0; c < 2; c++) {
            nd[c] = {LLONG_MIN, 0};
            for (int p = 0; p < 2; p++) {
                pair<ll, ll> v = {dp[p].first + (p != c ? a[i - 1] : 0) - (c ? lam : 0), dp[p].second + c};
                nd[c] = max(nd[c], v);
            }
        }
        dp[0] = nd[0], dp[1] = nd[1];
    }
    return max(dp[0], dp[1]);
}

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

    cin >> n >> r;
    for (int i = 1; i < n; i++) cin >> a[i];
    r = min(r, n - r);
    ll l = 0, h = 3e9, res = 0;
    while (l <= h) {
        ll mid = (l + h) / 2;
        if (solve(mid).second >= r) res = mid, l = mid + 1;
        else h = mid - 1;
    }
    cout << solve(res).first + res * r << '\n';
}
5 2
3 8 2 6

O(Nlog⁡3⋅109)O(N \log 3 \cdot 10^9).

Hai giới hạn: lồng hai lớp phạt

Có hai loại tài nguyên, mỗi loại giới hạn số lượng: phạt riêng λa\lambda_a và λb\lambda_b. Khi đã cố định cả hai, mỗi đối tượng tự chọn phương án tốt nhất độc lập, O(n)O(n). Chặt nhị phân λa\lambda_a bên trong (để dùng không quá aa), rồi λb\lambda_b bên ngoài, tổng O(nlog⁡2C)O(n \log^2 C). Với giá trị thực, chặt theo số vòng cố định (50 vòng mỗi lớp) và chấp nhận sai số nhỏ của đề.

Cách lồng chỉ đúng khi bài vẫn lõm theo từng chiều khi chiều kia đã được nới lỏng; với xác suất, chọn độc lập và tổng tuyến tính như bài dưới, điều này thỏa.

Hỏi nhanh

Sau khi cố định cả hai khoản phạt, vì sao mỗi mục tiêu tự chọn phương án tốt nhất là đúng?

Ví dụHai Loại Bóng Săn

Đề. n≤2000n \le 2000 mục tiêu, có không quá aa bóng loại một (bắt với xác suất pip_i) và bb bóng loại hai (uiu_i); mỗi mục tiêu nhận tối đa một bóng mỗi loại. Kỳ vọng số mục tiêu bắt được lớn nhất, sai số 10−410^{-4}.

Lời giải. Hai giới hạn số bóng thì dùng hai mức phạt. Với hai mức phạt cố định, mỗi mục tiêu tự chọn phương án tốt nhất trong bốn, độc lập với nhau: không bóng, bóng một, bóng hai, cả hai (p+u−pup + u - pu). Hai lớp chặt nhị phân trên đoạn [0,1][0, 1].

Bước 1: đánh giá với hai mức phạt. Đếm luôn số bóng mỗi loại đã dùng.

C++
double c1 = p[i] - la, c2 = u[i] - lb, c3 = p[i] + u[i] - p[i] * u[i] - la - lb;
if (c1 > best) best = c1, x = 1, y = 0;
if (c2 > best) best = c2, x = 0, y = 1;
if (c3 > best) best = c3, x = 1, y = 1;
v += best, ca += x, cb += y;

Bước 2: lớp trong. Với lb cố định, chặt la 50 vòng cho tới khi số bóng loại một không quá AA, rồi trả g+la⋅Ag + la \cdot A.

C++
for (int it = 0; it < 50; it++) {
    double mid = (lo + hi) / 2;
    eval(mid, lb, v, ca, cb);
    if (ca > A) lo = mid;
    else hi = mid;
}
eval(hi, lb, v, ca, cb);
return v + hi * A;

Bước 3: lớp ngoài chặt lb theo cùng cách, dựa trên số bóng loại hai mà inner báo về. Xác suất là số thực nên không có phá hòa chính xác; chặt theo số vòng cố định và lấy cận hi để số bóng không vượt giới hạn. Sai số còn lại dưới 10−410^{-4} đề cho phép.

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

int n, A, B;
double p[2005], u[2005];

void eval(double la, double lb, double &v, int &ca, int &cb) {
    v = 0, ca = cb = 0;
    for (int i = 0; i < n; i++) {
        double best = 0;
        int x = 0, y = 0;
        double c1 = p[i] - la, c2 = u[i] - lb, c3 = p[i] + u[i] - p[i] * u[i] - la - lb;
        if (c1 > best) best = c1, x = 1, y = 0;
        if (c2 > best) best = c2, x = 0, y = 1;
        if (c3 > best) best = c3, x = 1, y = 1;
        v += best, ca += x, cb += y;
    }
}

double inner(double lb, int &cb) {
    double lo = 0, hi = 1, v;
    int ca;
    for (int it = 0; it < 50; it++) {
        double mid = (lo + hi) / 2;
        eval(mid, lb, v, ca, cb);
        if (ca > A) lo = mid;
        else hi = mid;
    }
    eval(hi, lb, v, ca, cb);
    return v + hi * A;
}

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

    cin >> n >> A >> B;
    for (int i = 0; i < n; i++) cin >> p[i];
    for (int i = 0; i < n; i++) cin >> u[i];
    double lo = 0, hi = 1;
    int cb;
    for (int it = 0; it < 50; it++) {
        double mid = (lo + hi) / 2;
        inner(mid, cb);
        if (cb > B) lo = mid;
        else hi = mid;
    }
    double ans = inner(hi, cb) + hi * B;
    cout << fixed << setprecision(10) << ans << '\n';
}
2 1 1
0.500 0.000
0.000 0.500

50⋅50⋅n=5⋅10650 \cdot 50 \cdot n = 5 \cdot 10^6 lượt đánh giá mục tiêu.

Khi đi thi

  • DP có chiều “số lần chọn tới KK” quá lớn: kiểm f(k)f(k) lồi (brute force in hiệu kề, hoặc lập luận luồng, đổi chỗ); nếu đúng, bỏ chiều bằng phạt λ\lambda và chặt nhị phân.
  • DP trả về cặp (giá trị, số lần chọn) với cách phá hòa cố định; khôi phục bằng g(λ)+λKg(\lambda) + \lambda K ở λ\lambda biên, không đòi số lần chọn bằng đúng KK.
  • Hai giới hạn: lồng hai lớp phạt khi bài tách được thành phần tử độc lập; số thực thì chặt theo số vòng cố định.

Tóm tắt

  • g(λ)=max⁡k(f(k)−λk)g(\lambda) = \max_k (f(k) - \lambda k) không có chiều kk; ff lõm thì mọi kk đều đạt được.
  • Phá hòa bằng chọn nhiều nhất; tìm λ\lambda biên mà số lần chọn vẫn ít nhất KK: lớn nhất nếu là phạt, nhỏ nhất nếu là thưởng.
  • Đáp án f(K)=g(λ)+λKf(K) = g(\lambda) + \lambda K.
  • Hàm chỉ lõm trên một nửa thì dùng đối xứng; hai ràng buộc thì lồng hai lớp phạt.

Bài tập

1
Phân Công Tầng Thápcanuc80k7803Chi phí mỗi tầng giảm và lồi theo số bò; với phạt λ\lambda, mỗi tầng nhận số bò lớn nhất mà lợi ích biên còn ít nhất λ\lambda; chặt nhị phân λ\lambda để tổng bằng KK.
LQDOJ
2
Cây Đường Xuyên Vùngcanuc80k7804Cộng λ\lambda vào mọi cạnh nối đỉnh đặc biệt với đỉnh thường rồi chạy Kruskal, hòa thì ưu tiên loại cạnh đó; chặt nhị phân λ\lambda để đúng WW cạnh.
LQDOJ
3
Bổ Sung Trạm Dịch Chuyểncanuc80k7805Chia một đoạn dài dd bằng xx trạm thêm cho chi phí lồi theo xx; với phạt mỗi trạm, mỗi đoạn tự chọn xx bằng chặt nhị phân; chặt nhị phân phạt sao cho năng lượng không quá mm.
LQDOJ
4
Chuẩn Hóa Kiểu Chữcanuc80k7806Giải riêng cho mục tiêu nhiều chữ hoa và nhiều chữ thường; với phạt λ\lambda mỗi thao tác, dpi=max⁡(dpi−1+[đuˊng kiểu],dpi−l+l−λ)dp_i = \max(dp_{i-1} + [\text{đúng kiểu}], dp_{i-l} + l - \lambda).
LQDOJ
5
Ghép Cặp Bánh Ba Vịcanuc80k7807Phạt mỗi cặp; DP theo chẵn lẻ số bánh đã chọn cho từng vị, cặp được ghép tối ưu khi cộng tốt nhất của cùng một vị.
LQDOJ
6
Lịch Chuẩn Bị Và Incanuc80k7808Phạt λ\lambda mỗi bài; với phạt cố định, ghép chuẩn bị với in tối ưu bằng hàng đợi ưu tiên (luồng tham lam); chặt nhị phân λ\lambda để đúng kk bài.
LQDOJ
7
Bình Phương Các Khốicanuc80k4100Đúng kk đoạn với tổng bình phương nhỏ nhất: Alien trick bỏ chiều kk, mỗi λ\lambda là DP bao lồi tuyến tính.
LQDOJ
8
Trường Trên Phốcanuc80k4105Đúng KK trường: Alien trick bỏ chiều số trường, chi phí một đoạn nhà tính bằng tổng tiền tố, DP bao lồi hay chia để trị.
LQDOJ

Đọc thêm

  • Lagrangian RelaxationUSACO GuideNới lỏng Lagrange, điều kiện lồi và bài luyện.
  • WQS 二分OI WikiChặt nhị phân phạt và khôi phục đáp án.