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 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à , bằng tổng các khoảng nằm giữa và của nhóm. Vậy khi đi từ tới , mọi nhóm đang mở (đã có , chưa có ) cộng thêm đúng . Với nhóm đang mở, tổng chi phí tăng , một con số chỉ phụ thuộc trạng thái. Nếu đề chỉ hỏi tổng không vượt , chiều chi phí bị chặn bởi dù các giá trị lớn.
Với phần tử và 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 (), nhập vào một trong nhóm mà không đóng ( cách), đóng một trong nhóm bằng chính nó ( cách, còn ). Cuối cùng phải không còn nhóm nào mở.
Ví dụChia Nhóm Cân Bằng
Đề. 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 , modulo .
Lời giải. Sắp tăng dần. là số cách khi có nhóm đang mở và tổng chi phí đã trả là . Trước khi xét học sinh , mọi trạng thái nhận thêm , vượt thì bỏ. Rồi áp bốn lựa chọn trên. Đáp án là . Độ phức tạp .
#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
2
Hai lựa chọn “đứng riêng” và “nhập vào một nhóm” cùng giữ nguyên , nên gộp thành hệ số .
Đoạn mở trên dãy
Cùng ý tưởng dùng theo vị trí: chọn các đoạn để 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í , số đoạn phủ bằng số đoạn mở từ trước cộng (có mở đoạn mới tại hay không); sau đó có thể đóng một trong các đoạn đang phủ , 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í có đoạn đang phủ, đề cho phép đóng tối đa một đoạn ở . Đóng một đoạn có bao nhiêu cách?
Các đoạn đang phủ có đầu trái khác nhau nên là các đoạn khác nhau; chọn đoạn nào kết thúc ở cho ra tập đoạn khác nhau. Đoạn vừa mở tại cũng có thể đóng ngay, thành đoạn một phần tử .
Ví dụNhững Đoạn Nâng Mức
Đề. Dãy số và mức . 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 ; không hai đoạn chung đầu trái, không hai đoạn chung đầu phải. Đếm số tập, modulo .
Lời giải. Vị trí phải được phủ đúng lần ( thì vô nghiệm). là số cách khi đoạn còn mở sau vị trí trước. Tại : cần mở thêm đoạn; rồi không đóng (còn đoạn mở) hoặc đóng một trong đoạn (còn ). Đáp án là sau vị trí .
#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
2
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ị 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 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.
Vì 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 biết ngay khi chèn: gộp hai khối thì cả hai hàng xóm nhỏ hơn, là đỉnh; tạo khối mới thì cả hai hàng xóm (chèn sau) lớn hơn, 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
Đề. ô. Đếm các thứ tự thăm mọi ô một lần, bắt đầu ở , kết thúc ở , 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 .
Lời giải. Chèn tăng dần, là số cách sau khi chèn với khối. Mỗi giá trị ở giữa phải là đỉnh hoặc đáy, nên 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 và 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 : tạo khối mới có khe hợp lệ ( là số khối sau khi chèn, không chen trái hay phải đã có); gộp có cách chọn cặp khối liền nhau trong khối. Đáp án .
#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
1
Thành phần cùng chi phí khoảng cách
Gộp hai ý trên: đếm hoán vị có . 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ừ lên , mỗi đầu trống góp thêm đúng vào tổng. Với khối và đầu mút đã cố định (), số đầu trống là , chi phí tăng : 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ó khối và đã cố định đầ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?
Ba khối có 6 đầu, một đầu đã là đầu mút của cả hoán vị nên còn 5 đầu trống. Mỗi đầu trống sau này nối với một giá trị lớn hơn, nên mỗi đầu góp thêm 5: tổng .
Ví dụDãy Độ Cao Êm
Đề. độ cao khác nhau tới 1000. Đếm hoán vị có , modulo .
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 : đang có 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ả , và đầu mút của cả hoán vị đã cố định. Mẹo trả chi phí trước: khi chuyển từ lên , 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ố , nên mọi trạng thái cộng ngay. Riêng đá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).
dp[1][0][0] = 1;
dp[1][0][1] = 2;
Bước 2: cộng chi phí trước. Vượt thì bỏ trạng thái.
ll nk = k + (2 * j - l) * (A[i] - A[i - 1]);
if (nk > L) continue;
Bước 3: năm cách đặt . Khối mới ở giữa ( khe); khối mới làm đầu mút ( cách, tăng); nối vào một đầu trống của một khối ( cách); nối ra ngoài cùng và cố định làm đầu mút ( cách); gộp hai khối liền nhau ( cách).
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 trên mọi : còn đúng một khối, cả hai đầu mút đã cố định.
#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
2
, khoảng 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 để kiểm hệ số của từng chuyển tiếp: sai một hệ số hay 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: nhóm mở nhân .
- 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
< và >.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.Đọc thêm
- Connected Component DPCodeforces blogKỹ thuật chèn thành các thành phần với nhiều bài mẫu.
- Một số kĩ thuật tối ưu hoá thuật toán Quy Hoạch ĐộngVNOI WikiCác kỹ thuật quy hoạch động nâng cao, viết bằng tiếng Việt.
- T - PermutationAtCoder DP ContestBài chèn hoán vị theo chuỗi so sánh, nền của kỹ thuật này.