Chương 2·Bài 26

Advanced DP

Nhiều bài quy hoạch động có công thức đúng ngay từ đầu nhưng quá chậm: dp[i]\mathrm{dp}[i] lấy max hay tổng trên mọi j<ij < i thỏa một điều kiện, tức O(n2)O(n^2). Bài này không thêm trạng thái mới mà làm cho bước chuyển nhanh hơn. Quan sát xem tập các jj được xét có hình dạng gì (một tiền tố của miền giá trị, một đoạn liền, một cửa sổ trượt), rồi chọn cấu trúc trả lời đúng câu hỏi đó: mảng đuôi, Fenwick, tổng tiền tố, hàng đợi đơn điệu, cây phân đoạn.

Mảng đuôi cho dãy con tăng

Công thức quen thuộc của dãy con tăng dài nhất (bài 12) là dp[i]=1+max⁡dp[j]\mathrm{dp}[i] = 1 + \max \mathrm{dp}[j] trên các j<ij < i có aj<aia_j < a_i, O(n2)O(n^2). Đổi góc nhìn: thay vì nhớ độ dài tốt nhất kết thúc tại từng vị trí, nhớ với mỗi độ dài kk phần tử cuối nhỏ nhất của một dãy con tăng dài kk, gọi là tail[k - 1] (đánh số từ 0). Đuôi càng nhỏ thì càng dễ nối thêm, nên các dãy có đuôi lớn hơn cùng độ dài bị trội và không cần nhớ.

Mảng tail tăng nghiêm ngặt: một dãy dài k+1k + 1 có đuôi tt chứa một dãy dài kk có đuôi nhỏ hơn tt. Đọc aia_i, tìm vị trí đầu tiên trong tail có giá trị ≥ai\ge a_i bằng tìm kiếm nhị phân. Không có thì aia_i nối vào dãy dài nhất, tail dài thêm 1; có thì aia_i là đuôi nhỏ hơn cho độ dài đó, ghi đè. Độ dài cuối của tail là đáp án.

Mỗi phần tử mới tìm ô đầu tiên của tail không nhỏ hơn nó: ghi đè (cam) hoặc nối thêm (xanh lá).

tail không phải một dãy con thật của đầu vào: các giá trị trong nó có thể đến từ những dãy khác nhau. Muốn in một dãy con tối ưu, lưu thêm với mỗi ii vị trí của phần tử đứng trước nó lúc chèn, rồi lần ngược. Dãy con không giảm thì dùng upper_bound thay cho lower_bound.

Hỏi nhanh

Đang có tail = 1 2 6 9 và đọc tiếp số 4. Mảng tail thành gì?

Ví dụNhững Mốc Tăng Dần

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số. Tìm độ dài dãy con tăng nghiêm ngặt dài nhất.

Lời giải. Mảng đuôi với lower_bound, O(nlog⁡n)O(n \log n). Công thức O(n2)O(n^2) cần 4⋅10104 \cdot 10^{10} phép ở giới hạn này. Với ví dụ, tail lần lượt là 5, 1, 1 4, 1 2, 1 2 3, 1 2 3 9, 1 2 3 6, 1 2 3 6 7.

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

ll n;
vector<ll> tail;

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

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        ll p = lower_bound(tail.begin(), tail.end(), x) - tail.begin();
        if (p == (ll)tail.size()) tail.push_back(x);
        else tail[p] = x;
    }
    cout << tail.size() << '\n';
}
8
5 1 4 2 3 9 6 7

Fenwick trên miền giá trị

Chuyển tiếp “lấy max (hay tổng) dp[j]\mathrm{dp}[j] trên mọi j<ij < i có aj<aia_j < a_i” có hai điều kiện. Điều kiện j<ij < i tự thỏa nếu xử lý ii từ trái sang và chỉ ghi dp[j]\mathrm{dp}[j] vào cấu trúc sau khi tính xong. Điều kiện aj<aia_j < a_i thành một tiền tố trên miền giá trị: lập Fenwick (bài 16) chỉ số theo giá trị, ghi dp[j]\mathrm{dp}[j] vào ô aja_j, hỏi tiền tố [1,ai−1][1, a_i - 1]. Giá trị lớn thì nén về 1..n1..n trước.

Fenwick hỏi tiền tố được cả max (khi giá trị trong ô chỉ tăng) lẫn tổng. Cùng khuôn đó giải dãy con tăng có trọng số lớn nhất, đếm số dãy con tăng, đếm dãy con tăng có đúng kk phần tử (mỗi độ dài một Fenwick). Điều kiện ∣aj−ai∣≥d|a_j - a_i| \ge d là hai tiền tố, hoặc một tiền tố và một hậu tố, trên miền giá trị; khi đó dùng cây phân đoạn hỏi đoạn.

Hàng dưới đánh số theo chiều cao. Mỗi bông hỏi max các ô thấp hơn (xanh dương) rồi ghi dp của mình vào ô của nó (cam).

Ví dụVườn Hoa Có Thứ Tự

Đề. n≤2⋅105n \le 2 \cdot 10^5 bông hoa, bông ii cao hih_i (một hoán vị của 1..n1..n) và có giá trị bib_i. Giữ lại các bông có chiều cao tăng dần từ trái sang, tổng giá trị lớn nhất.

Lời giải. dp[i]=bi+max⁡dp[j]\mathrm{dp}[i] = b_i + \max \mathrm{dp}[j] trên j<ij < i, hj<hih_j < h_i. Fenwick max theo chiều cao: hỏi tiền tố hi−1h_i - 1, rồi ghi dp[i]\mathrm{dp}[i] vào ô hih_i. Đáp án là max mọi dp\mathrm{dp}, tới 2⋅10142 \cdot 10^{14}, cần ll. Với ví dụ, bông thứ 6 cao 3 lấy max của các bông cao 1, 2 là 30 và cộng 30.

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

const ll N = 2e5 + 5;
ll n, h[N], b[N], bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] = max(bit[pos], val);
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res = max(res, bit[pos]);
    return res;
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> h[i];
    for (ll i = 1; i <= n; i++) cin >> b[i];
    ll ans = 0;
    for (ll i = 1; i <= n; i++) {
        ll cur = b[i] + get(h[i] - 1);
        update(h[i], cur);
        ans = max(ans, cur);
    }
    cout << ans << '\n';
}
6
4 1 5 2 6 3
8 10 4 20 7 30

Ví dụĐếm Cách Chọn Tăng

Đề. Dãy n≤2⋅105n \le 2 \cdot 10^5 số tới 10910^9. Đếm số dãy con không rỗng tăng nghiêm ngặt (khác nhau theo tập vị trí), modulo 109+710^9 + 7.

Lời giải. dp[i]\mathrm{dp}[i] là số dãy con tăng kết thúc tại ii: chính nó cộng mọi dãy kết thúc tại j<ij < i có xj<xix_j < x_i. Nén giá trị, Fenwick tổng modulo: hỏi tiền tố hạng ri−1r_i - 1, cộng 1, ghi vào ô rir_i. Hai giá trị bằng nhau có cùng hạng, nên tiền tố ri−1r_i - 1 tự loại chúng. Với ví dụ, dp=1,1,2,5\mathrm{dp} = 1, 1, 2, 5, tổng 9.

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

const ll N = 2e5 + 5, MOD = 1e9 + 7;
ll n, x[N], v[N], bit[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] = (bit[pos] + val) % MOD;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res = (res + bit[pos]) % MOD;
    return res;
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> x[i];
        v[i] = x[i];
    }
    sort(v + 1, v + n + 1);
    ll ans = 0;
    for (ll i = 1; i <= n; i++) {
        ll r = lower_bound(v + 1, v + n + 1, x[i]) - v;
        ll cur = (get(r - 1) + 1) % MOD;
        update(r, cur);
        ans = (ans + cur) % MOD;
    }
    cout << ans << '\n';
}
4
2 1 2 3

Chuyển tiếp từ một đoạn

Khi các jj hợp lệ của dp[i]\mathrm{dp}[i] tạo thành một đoạn liền [Li,Ri][L_i, R_i] và dp\mathrm{dp} cần tổng (hay chỉ cần biết có tồn tại), tổng tiền tố của chính mảng dp\mathrm{dp} trả lời trong O(1)O(1): tính dp[i]\mathrm{dp}[i] xong thì cập nhật Si=Si−1+dp[i]S_i = S_{i-1} + \mathrm{dp}[i]. Hai đầu Li,RiL_i, R_i thường đơn điệu theo ii, tìm bằng hai con trỏ (bài 06) hoặc tìm kiếm nhị phân. Cùng ý tưởng làm đếm số cách đi bậc thang với bước tối đa kk thành O(n)O(n), và đếm dãy nằm giữa hai biên thành tổng tiền tố theo giá trị.

Ví dụChia Nhóm Theo Độ Chênh

Đề. n≤5⋅105n \le 5 \cdot 10^5 thẻ có giá trị. Hỏi có chia được toàn bộ thẻ thành các nhóm, mỗi nhóm ít nhất kk thẻ và chênh lệch lớn nhất trong nhóm không quá dd, hay không.

Lời giải. Sắp tăng dần; có cách chia thì có cách chia mà mỗi nhóm là một đoạn liền của dãy đã sắp (đổi chỗ hai thẻ lệch giữa hai nhóm không làm hỏng điều kiện). ok[i]\mathrm{ok}[i]: ii thẻ đầu chia được. Nhóm cuối là [j+1,i][j + 1, i], cần j≤i−kj \le i - k và ai−aj+1≤da_i - a_{j+1} \le d, tức j≥loi−1j \ge lo_i - 1 với loilo_i là thẻ đầu tiên có a≥ai−da \ge a_i - d. Vậy ok[i]\mathrm{ok}[i] đúng khi có ok[j]\mathrm{ok}[j] đúng với jj trong đoạn [loi−1,i−k][lo_i - 1, i - k]; loilo_i tăng theo ii nên dùng con trỏ, còn đoạn thì hỏi bằng tổng tiền tố của ok\mathrm{ok}. Với ví dụ, sau khi sắp là 1,2,3,10,11,12,121, 2, 3, 10, 11, 12, 12, chia sau thẻ thứ 3.

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

const ll N = 5e5 + 5;
ll n, k, d, a[N], ok[N], pre[N];

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

    cin >> n >> k >> d;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    ok[0] = pre[0] = 1;
    for (ll i = 1, lo = 1; i <= n; i++) {
        while (a[i] - a[lo] > d) lo++;
        ll L = lo - 1, R = i - k;
        if (R >= L) ok[i] = pre[R] - (L ? pre[L - 1] : 0) > 0;
        pre[i] = pre[i - 1] + ok[i];
    }
    cout << (ok[n] ? "YES" : "NO") << '\n';
}
7 3 2
12 1 11 3 10 2 12

Cửa sổ trượt và hàng đợi đơn điệu

Chuyển tiếp lấy max trên một cửa sổ [i−k,i−1][i - k, i - 1] trượt theo ii không dùng được tổng tiền tố. Hàng đợi đơn điệu (bài 06) giữ các chỉ số trong cửa sổ theo thứ tự, giá trị dp\mathrm{dp} giảm dần: thêm chỉ số mới ở cuối thì bỏ hết các chỉ số cuối có giá trị không lớn hơn, vì chúng vừa cũ hơn vừa kém hơn; chỉ số đầu hàng rời cửa sổ thì bỏ ở đầu. Đầu hàng luôn là max của cửa sổ. Mỗi chỉ số vào và ra một lần, nên cả lượt O(n)O(n) thay cho O(nk)O(nk).

Từ ô i được nhảy tối đa k ô; max của cửa sổ dp luôn nằm ở đầu hàng đợi.

Hỏi nhanh

Hàng đợi đơn điệu cho max đang chứa các chỉ số có giá trị 9,7,49, 7, 4 (từ đầu tới cuối). Thêm một chỉ số mới có giá trị 7, bỏ ở cuối mọi giá trị không lớn hơn nó. Hàng đợi thành gì?

Ví dụChọn Ảnh Phủ Mọi Cửa Sổ

Đề. n≤5000n \le 5000 ảnh có độ đẹp. Chọn đúng xx ảnh sao cho mọi đoạn kk ảnh liên tiếp chứa ít nhất một ảnh được chọn, tổng độ đẹp lớn nhất; không chọn được thì in −1-1.

Lời giải. Điều kiện tương đương: ảnh chọn đầu tiên ở vị trí ≤k\le k, ảnh cuối ở vị trí >n−k> n - k, và hai ảnh liên tiếp cách nhau không quá kk. dp[j][i]\mathrm{dp}[j][i] là tổng lớn nhất khi chọn jj ảnh, ảnh cuối ở ii: dp[j][i]=ai+max⁡dp[j−1][p]\mathrm{dp}[j][i] = a_i + \max \mathrm{dp}[j-1][p] trên p∈[i−k,i−1]p \in [i - k, i - 1], với dp[0][0]=0\mathrm{dp}[0][0] = 0 và mọi trạng thái khác của tầng 0 là −∞-\infty để ép ảnh đầu ở vị trí ≤k\le k. Mỗi tầng một lượt hàng đợi đơn điệu, tổng O(nx)=2,5⋅107O(nx) = 2{,}5 \cdot 10^7; chỉ giữ hai tầng để bộ nhớ O(n)O(n).

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

const ll N = 5005, NEG = -1e18;
ll n, k, x, a[N], f[N], g[N], dq[N];

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

    cin >> n >> k >> x;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    for (ll i = 1; i <= n; i++) f[i] = NEG;
    for (ll j = 1; j <= x; j++) {
        ll h = 0, t = 0;
        g[0] = NEG;
        for (ll i = 1; i <= n; i++) {
            while (t > h && f[dq[t - 1]] <= f[i - 1]) t--;
            dq[t++] = i - 1;
            while (dq[h] < i - k) h++;
            g[i] = f[dq[h]] == NEG ? NEG : f[dq[h]] + a[i];
        }
        for (ll i = 0; i <= n; i++) f[i] = g[i];
    }
    ll ans = NEG;
    for (ll i = n - k + 1; i <= n; i++) ans = max(ans, f[i]);
    cout << (ans == NEG ? -1 : ans) << '\n';
}
6 3 2
4 9 1 8 2 7

Cây phân đoạn giữ cả mảng quy hoạch động

Có bài mà mỗi bước không chỉ tính một dp[i]\mathrm{dp}[i] mới, mà còn làm thay đổi cả một đoạn các giá trị cũ: thêm một khoảng có trọng số thì mọi lựa chọn “điểm chọn cuối nằm trong khoảng” cùng được cộng trọng số. Đặt cả mảng dp[0..n]\mathrm{dp}[0..n] lên cây phân đoạn có cộng đoạn lười và hỏi max đoạn (bài 17): mỗi bước là một câu hỏi max, một lần gán điểm và vài lần cộng đoạn, O(log⁡n)O(\log n) mỗi thao tác.

Khuôn này còn giải chia dãy thành kk đoạn với giá trị mỗi đoạn là số phần tử khác nhau: khi thêm aia_i, mọi đoạn bắt đầu sau lần xuất hiện trước của aia_i được cộng 1.

Ví dụChọn Điểm Để Kích Hoạt Các Khoảng

Đề. N≤2⋅105N \le 2 \cdot 10^5 vị trí, M≤2⋅105M \le 2 \cdot 10^5 khoảng [l,r][l, r] có trọng số aa (có thể âm). Chọn một tập vị trí; mỗi khoảng chứa ít nhất một vị trí được chọn cho điểm aa. Tìm tổng điểm lớn nhất.

Lời giải. Mỗi khoảng chỉ cần biết vị trí được chọn cuối cùng không quá rr của nó có ≥l\ge l hay không. Gọi val[j]\mathrm{val}[j] là điểm tốt nhất khi vị trí chọn cuối là jj (j=0j = 0: chưa chọn gì), chỉ tính các khoảng đã kết thúc. Quét ii từ 1 tới NN:

  • Chọn ii thì vị trí chọn trước đó có thể là bất kỳ j<ij < i: val[i]=max⁡(val[0..i−1])\mathrm{val}[i] = \max(\mathrm{val}[0..i-1]).
  • Mỗi khoảng [l,i][l, i] kết thúc ở ii được kích hoạt đúng khi vị trí chọn cuối nằm trong [l,i][l, i]: cộng aa vào val[l..i]\mathrm{val}[l..i].

Đáp án là max toàn mảng sau khi quét hết. Hai thao tác là “gán một ô bằng max một đoạn” và “cộng một đoạn”, nên dùng cây phân đoạn max có thẻ cộng.

Bước 1: cây max với thẻ cộng. Như cây lười ở bài 17: apply cộng vào giá trị và thẻ của nút; down đẩy thẻ cho hai con. Ở đây nút lưu max nên cộng vv vào cả đoạn làm max tăng đúng vv, không cần biết độ dài đoạn.

C++
void apply(ll id, ll val) {
    st[id] += val;
    lz[id] += val;
}

void down(ll id) {
    if (!lz[id]) return;
    apply(id * 2, lz[id]);
    apply(id * 2 + 1, lz[id]);
    lz[id] = 0;
}

Bước 2: gom khoảng theo đầu phải. at[r] là danh sách các khoảng kết thúc ở rr, mỗi khoảng lưu (l,a)(l, a).

C++
for (ll i = 1; i <= m; i++) {
    ll l, r, a;
    cin >> l >> r >> a;
    at[r].push_back({l, a});
}

Bước 3: quét. Cây đánh số 0..N0..N. Gán val[i]\mathrm{val}[i] bằng cách cộng max của [0,i−1][0, i - 1] vào ô ii: ô ii chưa được chạm tới trước đó nên đang bằng 0. Rồi cộng trọng số của từng khoảng kết thúc ở ii.

C++
for (ll i = 1; i <= n; i++) {
    update(1, 0, n, i, i, get(1, 0, n, 0, i - 1));
    for (pair<ll, ll> p : at[i]) update(1, 0, n, p.first, i, p.second);
}
cout << get(1, 0, n, 0, n) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5, NEG = -1e18;
ll n, m, st[4 * N], lz[4 * N];
vector<pair<ll, ll>> at[N];

void apply(ll id, ll val) {
    st[id] += val;
    lz[id] += val;
}

void down(ll id) {
    if (!lz[id]) return;
    apply(id * 2, lz[id]);
    apply(id * 2 + 1, lz[id]);
    lz[id] = 0;
}

void update(ll id, ll l, ll r, ll u, ll v, ll val) {
    if (v < l || r < u) return;
    if (u <= l && r <= v) {
        apply(id, val);
        return;
    }
    down(id);
    ll mid = (l + r) >> 1;
    update(id * 2, l, mid, u, v, val);
    update(id * 2 + 1, mid + 1, r, u, v, val);
    st[id] = max(st[id * 2], st[id * 2 + 1]);
}

ll get(ll id, ll l, ll r, ll u, ll v) {
    if (v < l || r < u) return NEG;
    if (u <= l && r <= v) return st[id];
    down(id);
    ll mid = (l + r) >> 1;
    return max(get(id * 2, l, mid, u, v), get(id * 2 + 1, mid + 1, r, u, v));
}

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

    cin >> n >> m;
    for (ll i = 1; i <= m; i++) {
        ll l, r, a;
        cin >> l >> r >> a;
        at[r].push_back({l, a});
    }
    for (ll i = 1; i <= n; i++) {
        update(1, 0, n, i, i, get(1, 0, n, 0, i - 1));
        for (pair<ll, ll> p : at[i]) update(1, 0, n, p.first, i, p.second);
    }
    cout << get(1, 0, n, 0, n) << '\n';
}
4 4
1 2 5
2 4 7
1 4 -4
3 3 -10

Mỗi vị trí và mỗi khoảng tốn O(log⁡N)O(\log N): tổng O((N+M)log⁡N)O((N + M) \log N).

Khi đi thi

  • Viết công thức O(n2)O(n^2) trước, rồi hỏi: tập jj được xét là gì? Tiền tố giá trị thì Fenwick, đoạn liền cần tổng thì tổng tiền tố, cửa sổ cần max thì hàng đợi đơn điệu, còn cả đoạn giá trị cũ bị sửa thì cây phân đoạn lười.
  • Ghi dp[i]\mathrm{dp}[i] vào cấu trúc sau khi tính xong, để không tự chuyển từ chính nó; với điều kiện nghiêm ngặt trên giá trị, hai phần tử bằng nhau phải không nhìn thấy nhau.
  • Trạng thái không hợp lệ gán −∞-\infty và kiểm trước khi cộng; hai tầng liên tiếp là đủ thì chỉ giữ hai mảng.

Tóm tắt

  • Mảng đuôi giữ phần tử cuối nhỏ nhất cho mỗi độ dài; dãy con tăng dài nhất trong O(nlog⁡n)O(n \log n).
  • “Mọi jj trước có giá trị nhỏ hơn” là câu hỏi tiền tố trên Fenwick theo giá trị đã nén, cho cả max lẫn tổng.
  • Chuyển tiếp từ đoạn liền dùng tổng tiền tố của mảng dp\mathrm{dp}; cửa sổ trượt cần max dùng hàng đợi đơn điệu.
  • Khi một bước sửa cả đoạn giá trị cũ, giữ mảng dp\mathrm{dp} trên cây phân đoạn có cộng đoạn lười.

Bài tập

1
Khung Lồng Nhaucanuc80k2601Bỏ các khung không chứa được tấm nền, sắp theo chiều rộng, dãy con tăng nghiêm ngặt cả hai chiều trong O(n2)O(n^2) kèm truy vết.
LQDOJ
2
Sắp Xếp Bằng Phép Đảocanuc80k2604dp[i][0/1]\mathrm{dp}[i][0/1] là chi phí nhỏ nhất khi xâu ii giữ nguyên hoặc bị đảo; so với cả hai trạng thái của xâu trước.
LQDOJ
3
Một Lần Thay Đổicanuc80k2605Tính độ dài đoạn tăng kết thúc tại ii và bắt đầu tại ii; sửa aia_i nối được hai đoạn khi ai+1−ai−1≥2a_{i+1} - a_{i-1} \ge 2.
LQDOJ
4
Những Lớp Bị Bào Mòncanuc80k2606Số vòng của cột ii là min⁡(hi,d[i−1]+1,d[i+1]+1)\min(h_i, \mathrm{d}[i-1] + 1, \mathrm{d}[i+1] + 1); một lượt trái sang và một lượt phải sang.
LQDOJ
5
Các Bước Đến Đíchcanuc80k2607dp[n]\mathrm{dp}[n] là tổng sáu giá trị trước; nn tới 101810^{18} nên lũy thừa ma trận 6×66 \times 6.
LQDOJ
6
Dãy Nằm Giữa Hai Biêncanuc80k2611dp[i][v]\mathrm{dp}[i][v] là số cách với ci=vc_i = v, bằng tổng dp[i−1][u]\mathrm{dp}[i-1][u] với u≤vu \le v: tổng tiền tố theo giá trị, O(n⋅3000)O(n \cdot 3000).
LQDOJ
7
Thứ Tự Đồng Thuậncanuc80k2612xx đứng trước yy trong mọi hoán vị là cạnh x→yx \to y của một đồ thị không chu trình; đường dài nhất theo thứ tự của hoán vị đầu.
LQDOJ
8
Dãy Trạm Sau Xung Kích Hoạtcanuc80k2617Sắp theo tọa độ; dp[i]=1+dp[j]\mathrm{dp}[i] = 1 + \mathrm{dp}[j] với jj là trạm cuối cùng ngoài tầm phá của ii, tìm bằng tìm kiếm nhị phân; đáp án là nn trừ max dp\mathrm{dp}.
LQDOJ
9
Đoạn Nhân Một Lầncanuc80k2619Ba trạng thái: chưa vào đoạn nhân, đang trong đoạn nhân, đã ra khỏi; mỗi trạng thái là một tổng đoạn con lớn nhất.
LQDOJ
10
Đếm Dãy Con Tăng Theo Độ Dàicanuc80k2623Đếm dãy con tăng dài k+1k + 1: k+1k + 1 cây Fenwick, cây độ dài ℓ\ell hỏi tiền tố trên cây ℓ−1\ell - 1.
LQDOJ
11
Thu Thập Đá Quý Theo Nhịp Nhảycanuc80k2626Độ dài bước lệch khỏi dd không quá khoảng 250 vì 1+2+⋯+250>3⋅1041 + 2 + \dots + 250 > 3 \cdot 10^4; trạng thái là (vị trí, độ lệch).
LQDOJ
12
Dãy Nhảy Qua Các Cộtcanuc80k2632dp[i]=1+max⁡dp[j]\mathrm{dp}[i] = 1 + \max \mathrm{dp}[j] với ∣hj−hi∣≥d|h_j - h_i| \ge d: hai câu hỏi max trên cây phân đoạn theo độ cao đã nén, lưu chỉ số để truy vết.
LQDOJ
13
Nhảy Qua Dãy Độ Caocanuc80k2642Chỉ cần nhảy tới các đỉnh gần nhất trên hai ngăn xếp đơn điệu (cao hơn và thấp hơn); dp\mathrm{dp} theo thứ tự trái sang phải.
LQDOJ
14
Chia Dãy Theo Độ Đa Dạngcanuc80k2643dp[j][i]=max⁡pdp[j−1][p]+khaˊc(p+1,i)\mathrm{dp}[j][i] = \max_p \mathrm{dp}[j-1][p] + \mathrm{khác}(p + 1, i); thêm aia_i thì cộng 1 cho mọi pp từ lần xuất hiện trước của aia_i tới i−1i - 1 trên cây phân đoạn lười.
LQDOJ
15
Dãy Con Chung Tăng Nghiêm Ngặtcanuc80k2646Dãy con chung tăng dài nhất O(nm)O(nm): với mỗi aia_i, quét bb và giữ giá trị tốt nhất của các bj<aib_j < a_i đã gặp.
LQDOJ

Đọc thêm