Chương 4·Bài 43·Nâng cao

Open and Close Interval Trick

Có những bài đếm (hoặc tối ưu) cách chia, cách ghép hay hoán vị mà chi phí phụ thuộc vào khoảng cách giữa các phần tử, như độ chênh lớn nhất trừ nhỏ nhất của một nhóm, hay tổng ∣fi−fi+1∣|f_i - f_{i+1}| của một hoán vị. Thay vì quyết định cả nhóm một lần, ta xét các phần tử theo thứ tự giá trị (hay thứ tự vị trí), và giữ trong trạng thái số nhóm đang mở: những nhóm đã có phần tử nhỏ nhất nhưng chưa có phần tử lớn nhất. Mỗi phần tử mới chỉ có vài lựa chọn với các nhóm đang mở: mở nhóm mới, nhập vào một nhóm, đóng một nhóm, hay đứng riêng. Chi phí khoảng cách được trả dần: mỗi khi giá trị tăng một khoảng, mọi nhóm đang mở cùng dài thêm khoảng đó.

Nhóm đang mở và chi phí trả dần

Sắp các giá trị tăng dần. Độ chênh của một nhóm là max⁡−min⁡\max - \min, bằng tổng các khoảng ai−ai−1a_{i} - a_{i-1} nằm giữa min⁡\min và max⁡\max của nhóm. Vậy khi đi từ ai−1a_{i-1} tới aia_i, mọi nhóm đang mở (đã có min⁡\min, chưa có max⁡\max) cộng thêm đúng ai−ai−1a_i - a_{i-1}. Với jj nhóm đang mở, tổng chi phí tăng j⋅(ai−ai−1)j \cdot (a_i - a_{i-1}), một con số chỉ phụ thuộc trạng thái. Nếu đề chỉ hỏi tổng không vượt KK, chiều chi phí bị chặn bởi KK dù các giá trị lớn.

Mỗi chữ là nhóm của giá trị; thanh dưới là đoạn [min, max] của nhóm. Khung cam là khoảng đang xét, chỉ nhóm đang mở được cộng.

Với phần tử aia_i và jj nhóm đang mở, có bốn lựa chọn: đứng riêng (nhóm một người, mở rồi đóng ngay), mở nhóm mới (j+1j + 1), nhập vào một trong jj nhóm mà không đóng (jj cách), đóng một trong jj nhóm bằng chính nó (jj cách, còn j−1j - 1). Cuối cùng phải không còn nhóm nào mở.

Ví dụChia Nhóm Cân Bằng

Đề. n≤200n \le 200 học sinh có giá trị tới 500. Đếm số cách chia thành các nhóm sao cho tổng (max − min) của các nhóm không vượt K≤1000K \le 1000, modulo 109+710^9 + 7.

Lời giải. Sắp tăng dần. dp[j][k]dp[j][k] là số cách khi có jj nhóm đang mở và tổng chi phí đã trả là kk. Trước khi xét học sinh i≥2i \ge 2, mọi trạng thái nhận thêm j⋅(ai−ai−1)j \cdot (a_i - a_{i-1}), vượt KK thì bỏ. Rồi áp bốn lựa chọn trên. Đáp án là ∑kdp[0][k]\sum_k dp[0][k]. Độ phức tạp O(n2K)=4⋅107O(n^2 K) = 4 \cdot 10^7.

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

const ll MOD = 1e9 + 7;
ll n, K, a[205], dp[205][1005], nd[205][1005];

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

    cin >> n >> K;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    dp[0][0] = 1;
    for (ll i = 1; i <= n; i++) {
        ll gap = i > 1 ? a[i] - a[i - 1] : 0;
        for (ll j = 0; j <= n; j++)
            for (ll k = 0; k <= K; k++) nd[j][k] = 0;
        for (ll j = 0; j < i; j++)
            for (ll k = 0; k <= K; k++) {
                if (!dp[j][k]) continue;
                ll nk = k + j * gap;
                if (nk > K) continue;
                ll v = dp[j][k];
                nd[j][nk] = (nd[j][nk] + v * (j + 1)) % MOD;
                nd[j + 1][nk] = (nd[j + 1][nk] + v) % MOD;
                if (j) nd[j - 1][nk] = (nd[j - 1][nk] + v * j) % MOD;
            }
        for (ll j = 0; j <= n; j++)
            for (ll k = 0; k <= K; k++) dp[j][k] = nd[j][k];
    }
    ll ans = 0;
    for (ll k = 0; k <= K; k++) ans = (ans + dp[0][k]) % MOD;
    cout << ans << '\n';
}
3 1
1 2 4

Hai lựa chọn “đứng riêng” và “nhập vào một nhóm” cùng giữ nguyên jj, nên gộp thành hệ số j+1j + 1.

Đoạn mở trên dãy

Cùng ý tưởng dùng theo vị trí: chọn các đoạn [l,r][l, r] để mỗi vị trí được phủ đúng một số lần cho trước. Quét từ trái sang, trạng thái là số đoạn đang mở đi qua vị trí hiện tại. Tại vị trí ii, số đoạn phủ ii bằng số đoạn mở từ trước cộng (có mở đoạn mới tại ii hay không); sau đó có thể đóng một trong các đoạn đang phủ ii, chọn đoạn nào là một hệ số nhân. Nếu đề cấm hai đoạn chung đầu trái hay chung đầu phải, mỗi vị trí mở tối đa một và đóng tối đa một.

Hỏi nhanh

Tại vị trí ii có dd đoạn đang phủ, đề cho phép đóng tối đa một đoạn ở ii. Đóng một đoạn có bao nhiêu cách?

Ví dụNhững Đoạn Nâng Mức

Đề. Dãy n≤2000n \le 2000 số và mức h≤2000h \le 2000. Chọn một tập đoạn, mỗi đoạn tăng các phần tử của nó thêm 1, để cả dãy bằng hh; không hai đoạn chung đầu trái, không hai đoạn chung đầu phải. Đếm số tập, modulo 109+710^9 + 7.

Lời giải. Vị trí ii phải được phủ đúng di=h−aid_i = h - a_i lần (di<0d_i < 0 thì vô nghiệm). dp[j]dp[j] là số cách khi jj đoạn còn mở sau vị trí trước. Tại ii: cần mở thêm s=di−j∈{0,1}s = d_i - j \in \{0, 1\} đoạn; rồi không đóng (còn did_i đoạn mở) hoặc đóng một trong did_i đoạn (còn di−1d_i - 1). Đáp án là dp[0]dp[0] sau vị trí nn.

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

const ll MOD = 1e9 + 7;
ll n, h, a[2005], dp[2005], nd[2005];

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

    cin >> n >> h;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    dp[0] = 1;
    for (ll i = 1; i <= n; i++) {
        ll d = h - a[i];
        for (ll j = 0; j <= h; j++) nd[j] = 0;
        if (d >= 0)
            for (ll j = 0; j <= d; j++) {
                if (!dp[j] || d - j > 1) continue;
                nd[d] = (nd[d] + dp[j]) % MOD;
                if (d) nd[d - 1] = (nd[d - 1] + dp[j] * d) % MOD;
            }
        for (ll j = 0; j <= h; j++) dp[j] = nd[j];
    }
    cout << dp[0] << '\n';
}
2 1
0 0

Chèn giá trị tăng dần thành các thành phần

Đếm hoán vị có ràng buộc giữa các phần tử kề nhau (zích zắc, không hơn kém 1, tổng chênh lệch nhỏ): dựng hoán vị bằng cách chèn các giá trị 1,2,…,n1, 2, \dots, n theo thứ tự tăng dần. Ở mỗi thời điểm, những giá trị đã chèn tạo thành một số khối (thành phần liên thông) sẽ nằm theo đúng thứ tự trái phải trong hoán vị cuối, nhưng khoảng cách giữa các khối chưa biết, sẽ được lấp bởi các giá trị lớn hơn. Giá trị mới ii có ba lựa chọn:

  • tạo một khối mới ở một khe giữa hoặc hai bên các khối hiện có;
  • nối vào đầu trái hay đầu phải của một khối;
  • đứng giữa hai khối liền nhau và gộp chúng.
Hàng là hoán vị cuối; sau khi chèn tới k, các ô xanh là giá trị ≤ k, ngoặc dưới là các khối.

Vì ii lớn hơn mọi giá trị đã chèn, hàng xóm chèn trước luôn nhỏ hơn nó, hàng xóm chèn sau luôn lớn hơn. Nên vai trò của ii biết ngay khi chèn: gộp hai khối thì cả hai hàng xóm nhỏ hơn, ii là đỉnh; tạo khối mới thì cả hai hàng xóm (chèn sau) lớn hơn, ii là đáy; nối vào một đầu thì một hàng xóm nhỏ, một hàng xóm lớn. Điểm đầu và điểm cuối cố định của hoán vị là khối đặc biệt: không gì được chèn vào bên trái điểm đầu hay bên phải điểm cuối.

Ví dụĐường Đi Zích Zắc

Đề. N≤2000N \le 2000 ô. Đếm các thứ tự thăm mọi ô một lần, bắt đầu ở ss, kết thúc ở ee, mà từ bước thứ hai hướng đi luân phiên (mỗi ô ở giữa là đỉnh cục bộ hoặc đáy cục bộ), modulo 109+710^9 + 7.

Lời giải. Chèn 1..N1..N tăng dần, dp[i][j]dp[i][j] là số cách sau khi chèn ii với jj khối. Mỗi giá trị ở giữa phải là đỉnh hoặc đáy, nên i≠s,ei \ne s, e chỉ được tạo khối mới (thành đáy) hoặc gộp hai khối (thành đỉnh); nối vào một đầu cho nó một hàng xóm nhỏ và một lớn, sai luật. Còn ss và ee chỉ có một hàng xóm: chúng tạo khối mới ở đầu mút hoặc nối vào khối ở đầu mút tương ứng. Với i≠s,ei \ne s, e: tạo khối mới có j−[i>s]−[i>e]j - [i > s] - [i > e] khe hợp lệ (jj là số khối sau khi chèn, không chen trái ss hay phải ee đã có); gộp có jj cách chọn cặp khối liền nhau trong j+1j + 1 khối. Đáp án dp[N][1]dp[N][1].

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

const ll MOD = 1e9 + 7;
ll n, s, e, dp[2005][2005];

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

    cin >> n >> s >> e;
    dp[0][0] = 1;
    for (ll i = 1; i <= n; i++)
        for (ll j = 1; j <= i; j++) {
            if (i == s || i == e) dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD;
            else {
                ll slots = j - (i > s) - (i > e);
                if (slots > 0) dp[i][j] = dp[i - 1][j - 1] * slots % MOD;
                dp[i][j] = (dp[i][j] + dp[i - 1][j + 1] * j) % MOD;
            }
        }
    cout << dp[n][1] << '\n';
}
3 1 2

Thành phần cùng chi phí khoảng cách

Gộp hai ý trên: đếm hoán vị có ∑∣fi−fi+1∣≤L\sum |f_i - f_{i+1}| \le L. Chèn giá trị theo thứ tự tăng dần thành các khối. Mỗi khối có hai đầu còn trống (trừ đầu đã cố định là đầu mút của cả hoán vị); mỗi đầu trống sau này sẽ nối với một giá trị lớn hơn, nên khi giá trị tăng từ Ai−1A_{i-1} lên AiA_i, mỗi đầu trống góp thêm đúng Ai−Ai−1A_i - A_{i-1} vào tổng. Với jj khối và ll đầu mút đã cố định (l≤2l \le 2), số đầu trống là 2j−l2j - l, chi phí tăng (2j−l)(Ai−Ai−1)(2j - l)(A_i - A_{i-1}): lại là “số đang mở nhân khoảng cách”. Trạng thái gồm số khối, chi phí đã trả và số đầu mút đã cố định.

Hỏi nhanh

Đang có j=3j = 3 khối và đã cố định l=1l = 1 đầu mút. Khi giá trị tăng thêm 5, tổng chênh lệch đã trả tăng thêm bao nhiêu?

Ví dụDãy Độ Cao Êm

Đề. N≤100N \le 100 độ cao khác nhau tới 1000. Đếm hoán vị có ∑∣fi−fi+1∣≤L≤1000\sum |f_i - f_{i+1}| \le L \le 1000, modulo 109+710^9 + 7.

Lời giải. Sắp tăng dần và chèn dần các độ cao từ nhỏ tới lớn. Trạng thái dp[j][k][l]dp[j][k][l]: đang có jj khối (đoạn liên tiếp của hoán vị cuối cùng, chưa biết vị trí tương đối), chi phí đã trả kk, và ll đầu mút của cả hoán vị đã cố định. Mẹo trả chi phí trước: khi chuyển từ Ai−1A_{i-1} lên AiA_i, mỗi “đầu trống” của các khối (chỗ còn ghép thêm số lớn hơn) chắc chắn sẽ nối với một số ≥Ai\ge A_i, nên mọi trạng thái cộng (2j−l)(Ai−Ai−1)(2j - l)(A_i - A_{i-1}) ngay. Riêng N=1N = 1 đáp án là 1.

Bước 1: trạng thái đầu. Số nhỏ nhất đứng thành một khối: không cố định đầu mút nào (1 cách), hoặc cố định làm một trong hai đầu mút (2 cách).

C++
dp[1][0][0] = 1;
dp[1][0][1] = 2;

Bước 2: cộng chi phí trước. Vượt LL thì bỏ trạng thái.

C++
ll nk = k + (2 * j - l) * (A[i] - A[i - 1]);
if (nk > L) continue;

Bước 3: năm cách đặt AiA_i. Khối mới ở giữa (j+1−lj + 1 - l khe); khối mới làm đầu mút (2−l2 - l cách, ll tăng); nối vào một đầu trống của một khối (2j−l2j - l cách); nối ra ngoài cùng và cố định làm đầu mút (2−l2 - l cách); gộp hai khối liền nhau (j−1j - 1 cách).

C++
if (j + 1 - l > 0) add(nd[j + 1][nk][l], v * (j + 1 - l));
if (l < 2) add(nd[j + 1][nk][l + 1], v * (2 - l));
if (2 * j - l > 0) add(nd[j][nk][l], v * (2 * j - l));
if (l < 2) add(nd[j][nk][l + 1], v * (2 - l));
if (j >= 2) add(nd[j - 1][nk][l], v * (j - 1));

Đáp án là tổng dp[1][k][2]dp[1][k][2] trên mọi k≤Lk \le L: còn đúng một khối, cả hai đầu mút đã cố định.

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

const ll MOD = 1e9 + 7;
ll n, L, A[105], dp[105][1005][3], nd[105][1005][3];

void add(ll &x, ll v) {
    x = (x + v) % MOD;
}

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

    cin >> n >> L;
    for (ll i = 1; i <= n; i++) cin >> A[i];
    if (n == 1) {
        cout << 1 << '\n';
        return 0;
    }
    sort(A + 1, A + n + 1);
    dp[1][0][0] = 1;
    dp[1][0][1] = 2;
    for (ll i = 2; i <= n; i++) {
        for (ll j = 0; j <= i; j++)
            for (ll k = 0; k <= L; k++)
                for (ll l = 0; l < 3; l++) nd[j][k][l] = 0;
        for (ll j = 1; j < i; j++)
            for (ll k = 0; k <= L; k++)
                for (ll l = 0; l < 3; l++) {
                    ll v = dp[j][k][l];
                    if (!v) continue;
                    ll nk = k + (2 * j - l) * (A[i] - A[i - 1]);
                    if (nk > L) continue;
                    if (j + 1 - l > 0) add(nd[j + 1][nk][l], v * (j + 1 - l));
                    if (l < 2) add(nd[j + 1][nk][l + 1], v * (2 - l));
                    if (2 * j - l > 0) add(nd[j][nk][l], v * (2 * j - l));
                    if (l < 2) add(nd[j][nk][l + 1], v * (2 - l));
                    if (j >= 2) add(nd[j - 1][nk][l], v * (j - 1));
                }
        for (ll j = 0; j <= i; j++)
            for (ll k = 0; k <= L; k++)
                for (ll l = 0; l < 3; l++) dp[j][k][l] = nd[j][k][l];
    }
    ll ans = 0;
    for (ll k = 0; k <= L; k++) add(ans, dp[1][k][2]);
    cout << ans << '\n';
}
3 2
1 2 3

O(N2L)O(N^2 L), khoảng 10710^7 trạng thái mỗi tầng nhân 3.

Khi đi thi

  • Chi phí là max trừ min của nhóm, hay tổng chênh lệch giữa các phần tử kề nhau: sắp theo giá trị, trả chi phí theo “số đang mở nhân khoảng cách”, chiều chi phí bị chặn bởi giới hạn của đề.
  • Đếm hoán vị với ràng buộc giữa hai phần tử kề: chèn giá trị tăng dần thành các khối; kiểm từng lựa chọn có tạo ra cặp kề sai luật không, và thêm đầu mút cố định vào trạng thái.
  • Viết vét cạn cho n≤8n \le 8 để kiểm hệ số của từng chuyển tiếp: sai một hệ số jj hay j+1j + 1 là lỗi hay gặp nhất của kỹ thuật này.

Tóm tắt

  • Xét phần tử theo thứ tự giá trị hoặc vị trí, trạng thái là số nhóm hay đoạn đang mở.
  • Chi phí khoảng cách trả dần: jj nhóm mở nhân ai−ai−1a_i - a_{i-1}.
  • Bốn lựa chọn cho nhóm: đứng riêng, mở, nhập, đóng; hệ số là số nhóm có thể chọn.
  • Hoán vị: chèn giá trị tăng dần thành khối; tạo mới, nối, gộp; đầu mút cố định giới hạn số khe.

Bài tập

1
Hoán Vị Theo Chuỗi So Sánhcanuc80k1249Chèn lần lượt từng vị trí và giữ thứ hạng tương đối của phần tử cuối; dp[i][j]dp[i][j] với tổng tiền tố theo jj cho dấu < và >.
LQDOJ
2
Dọn Trống Các Vị Trí Ban Đầucanuc80k2613Người cần chuyển và ghế trống đều đã sắp; dp[i][j]dp[i][j] ghép ii người đầu với một số ghế trong jj ghế đầu, chi phí ∣p−q∣|p - q|.
LQDOJ
3
Cây Cố Vấncanuc80k4300Xét hạng tăng dần, trạng thái là số người đang chờ cố vấn; người mới nhận 0, 1 hoặc 2 người trong số đó (nhân tổ hợp), người hạng RR nhận 0, rồi chính họ vào hàng chờ, trừ gốc NN.
LQDOJ
4
Dãy Đèn Tự Kích Hoạtcanuc80k4302Các thiết bị bật tay chia thành các khối liền nhau ngăn bởi một thiết bị tự bật; số thứ tự bật một khối dài kk là 2k−12^{k-1}, trộn các khối bằng tổ hợp.
LQDOJ
5
Hoán Vị Tránh Số Liên Tiếpcanuc80k4304Chèn giá trị tăng dần thành khối; cặp hơn kém 1 bị cấm nên phải nhớ thêm giá trị vừa chèn có đứng cạnh giá trị trước nó không.
LQDOJ
6
Hành Trình Một Chiềucanuc80k4306Như đường zích zắc nhưng hướng đi ra quyết định bởi loại thành phố: loại L thì thành phố phải nằm bên phải hàng xóm sau, ràng buộc lựa chọn tạo mới hay gộp.
LQDOJ
7
Chuyến Đi Tối Ưucanuc80k4308Chèn ghế theo tọa độ tăng dần thành các khối, chi phí mỗi đầu nối được biết khi chèn (đi trái hay phải); thêm cờ đầu mút cho ss và ee.
LQDOJ
8
Đếm Đoạn Tổng Bằng Khôngcanuc80k4309Đoạn tổng 0 là hai tổng tiền tố bằng nhau; dựng các mức tiền tố từ cao xuống, số cặp bằng nhau là ∑(c2)\sum \binom{c}{2} trên mỗi mức, quy hoạch động theo số lần chạm mức hiện tại.
LQDOJ

Đọc thêm