Chương 4·Bài 40

Lexicographic DP

Đề hỏi đối tượng hợp lệ thứ KK theo thứ tự từ điển, hoặc thứ hạng của một đối tượng cho trước, mà số đối tượng lớn tới 101510^{15}: không thể liệt kê. Cách làm chung là dựng đối tượng từ trái sang phải. Ở mỗi vị trí, các đối tượng chia thành từng khối theo ký tự đặt tại đó, khối của ký tự nhỏ đứng trước. Biết kích thước mỗi khối (bằng công thức tổ hợp hoặc quy hoạch động đếm phần đuôi), ta biết thứ KK rơi vào khối nào, trừ đi các khối đứng trước, rồi đi tiếp. Xếp hạng làm ngược lại: cộng kích thước các khối đứng trước.

Chia khối và giải mã thứ K

Các chuỗi hợp lệ sắp theo từ điển thì những chuỗi cùng ký tự đầu đứng liền nhau thành một khối, khối của ký tự nhỏ đứng trước. Gọi cnt(c)cnt(c) là số chuỗi bắt đầu bằng cc, tức số cách hoàn thiện phần còn lại sau khi đặt cc. Duyệt các ký tự cc từ nhỏ đến lớn: nếu K≤cnt(c)K \le cnt(c) thì chuỗi cần tìm nằm trong khối của cc, đặt cc và tìm chuỗi thứ KK trong khối đó; ngược lại K←K−cnt(c)K \gets K - cnt(c) và thử ký tự kế tiếp. Lặp lại cho từng vị trí. Mọi khó khăn dồn vào việc tính cntcnt.

Mỗi bước so K với số chuỗi trong khối bắt đầu bằng a; danh sách dưới tô các chuỗi còn khớp tiền tố đã đặt.

Ví dụChuỗi aab, aba, baa

Đề. Có AA chữ a và BB chữ b với A,B≤30A, B \le 30. In chuỗi thứ KK theo thứ tự từ điển trong mọi chuỗi dùng đúng số chữ đó.

Lời giải. Còn aa chữ a và bb chữ b thì số chuỗi bắt đầu bằng a là (a+b−1b)\binom{a + b - 1}{b}: sắp bb chữ b vào a+b−1a + b - 1 vị trí còn lại. KK không quá số đó thì đặt a, không thì trừ đi và đặt b. (6030)≈1,2⋅1017\binom{60}{30} \approx 1{,}2 \cdot 10^{17} vẫn vừa ll; dựng bảng tổ hợp bằng tam giác Pascal để khỏi chia.

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

ll C[65][65];

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

    for (ll i = 0; i < 65; i++) {
        C[i][0] = 1;
        for (ll j = 1; j <= i; j++) C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
    }
    ll a, b, k;
    cin >> a >> b >> k;
    string res;
    while (a + b > 0) {
        ll c = a ? C[a + b - 1][b] : 0;
        if (k <= c) {
            res += 'a';
            a--;
        } else {
            k -= c;
            res += 'b';
            b--;
        }
    }
    cout << res << '\n';
}
2 2 4

Xếp hạng: cộng các khối đứng trước

Thứ hạng của một đối tượng ss là 1 cộng số đối tượng đứng trước nó. Một đối tượng tt đứng trước ss khi chúng giống nhau tới vị trí i−1i - 1 rồi ti<sit_i < s_i. Vậy duyệt từng vị trí ii, với mỗi ký tự c<sic < s_i đặt được ở đó (cùng tiền tố của ss), cộng số cách hoàn thiện. Đây đúng là quá trình giải mã chạy ngược, và hai hàm luôn kiểm được lẫn nhau: giải mã thứ hạng vừa tính phải ra lại đối tượng ban đầu.

Với hoán vị của 1..n1..n: sau khi đã dùng các số ở vị trí trước, mỗi số chưa dùng nhỏ hơn sis_i mở một khối gồm (n−i)!(n - i)! hoán vị. Đó là khai triển Cantor: thứ hạng là 1+∑ici⋅(n−i)!1 + \sum_i c_i \cdot (n - i)! với cic_i là số số chưa dùng nhỏ hơn sis_i.

Gõ một hoán vị; mỗi cột cộng c_i khối, mỗi khối có (n − i)! hoán vị.

Hỏi nhanh

Hoán vị 3 1 23\ 1\ 2 của {1,2,3}\{1, 2, 3\} có thứ hạng bao nhiêu (đánh số từ 1)?

Ví dụSố hiệu hoán vị

Đề. Một hoán vị của 1..n1..n với n≤12n \le 12 (độ dài lấy từ dòng đầu) và một thứ hạng pp. In thứ hạng của hoán vị đã cho và hoán vị ở thứ hạng pp.

Lời giải. Xếp hạng bằng khai triển Cantor: ở vị trí ii, mỗi số chưa dùng nhỏ hơn aia_i mở ra một khối (n−i)!(n - i)! hoán vị đứng trước hoán vị đã cho. Giải mã làm ngược lại với hạng tính từ 0: thương cho (n−i)!(n - i)! là chỉ số của số cần đặt trong các số chưa dùng, phần dư đi tiếp cho các vị trí sau.

Bước 1: đọc hoán vị không biết trước độ dài. Đề không cho nn riêng: độ dài là số số trên dòng đầu, nên đọc cả dòng bằng getline rồi tách số.

C++
string line;
getline(cin, line);
stringstream ss(line);
ll x;
while (ss >> x) a[++n] = x;

Bước 2: xếp hạng. f[i] =i!= i!, tới 12!≈4,8⋅10812! \approx 4{,}8 \cdot 10^8. c đếm số chưa dùng nhỏ hơn a[i].

C++
ll rk = 1;
for (ll i = 1; i <= n; i++) {
    ll c = 0;
    for (ll v = 1; v < a[i]; v++) c += !used[v];
    rk += c * f[n - i];
    used[a[i]] = 1;
}

Bước 3: giải mã. Làm việc với p−1p - 1. c-- == 0 tìm số chưa dùng thứ c (đếm từ 0).

C++
p--;
for (ll i = 1; i <= n; i++) {
    ll c = p / f[n - i];
    p %= f[n - i];
    for (ll v = 1; v <= n; v++)
        if (!used[v] && c-- == 0) {
            used[v] = 1;
            cout << v << ' ';
            break;
        }
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, a[15], f[15], used[15];

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

    string line;
    getline(cin, line);
    stringstream ss(line);
    ll x;
    while (ss >> x) a[++n] = x;
    ll p;
    cin >> p;
    f[0] = 1;
    for (ll i = 1; i <= n; i++) f[i] = f[i - 1] * i;
    ll rk = 1;
    for (ll i = 1; i <= n; i++) {
        ll c = 0;
        for (ll v = 1; v < a[i]; v++) c += !used[v];
        rk += c * f[n - i];
        used[a[i]] = 1;
    }
    cout << rk << '\n';
    for (ll v = 1; v <= n; v++) used[v] = 0;
    p--;
    for (ll i = 1; i <= n; i++) {
        ll c = p / f[n - i];
        p %= f[n - i];
        for (ll v = 1; v <= n; v++)
            if (!used[v] && c-- == 0) {
                used[v] = 1;
                cout << v << ' ';
                break;
            }
    }
    cout << '\n';
}
3 1 2
6

O(n2)O(n^2).

Đếm đuôi bằng quy hoạch động

Khi ràng buộc phức tạp hơn “dùng mỗi thứ một số lần”, số cách hoàn thiện không còn công thức gọn. Nhưng nó thường chỉ phụ thuộc vào vị trí và một ít trạng thái của tiền tố: chiều cao hiện tại của đường đi, số ngoặc đang mở, số bit 0 liên tiếp ở cuối. Quy hoạch động ways[i][h]ways[i][h] là số cách đi từ vị trí ii với trạng thái hh tới hết, tính từ cuối về đầu. Giải mã và xếp hạng giữ nguyên khuôn, chỉ thay cnt(c)cnt(c) bằng ways[i+1][h′]ways[i + 1][h'] với h′h' là trạng thái sau khi đặt cc.

Khi điều kiện lại nói về cả đối tượng, như “độ sâu lớn nhất đúng bằng kk”, thêm vào trạng thái phần tiền tố đã đạt (độ sâu lớn nhất tới giờ), hoặc đếm bằng hiệu: số đối tượng có độ sâu không quá kk trừ số có độ sâu không quá k−1k - 1.

Hỏi nhanh

Đếm dãy ngoặc có độ sâu lớn nhất đúng bằng kk, vì sao lấy hiệu “không quá kk” trừ “không quá k−1k - 1” thay vì đếm thẳng?

Ví dụDãy Catalan Xếp Hạng Và Truy Ngược

Đề. Dãy Catalan cấp n≤15n \le 15 dài 2n+12n + 1, đầu và cuối bằng 0, các số không âm, hai số kề hơn kém đúng 1. In thứ hạng của một dãy cho trước và dãy ở thứ hạng kk.

Lời giải. Khuôn chung của xếp hạng theo thứ tự từ điển: đếm số cách hoàn thành dãy từ mỗi trạng thái, rồi đi dọc dãy, mỗi lần dãy chọn một nhánh lớn hơn thì cộng số dãy của các nhánh nhỏ hơn. Ở đây từ giá trị hh chỉ đi tới h−1h - 1 (nhỏ hơn, đứng trước) hoặc h+1h + 1.

Bước 1: số cách hoàn thành. ways[i][h] là số cách điền từ vị trí ii đang có giá trị hh tới cuối; chỉ có đúng một cách kết thúc là ways[L][0] = 1. Mọi giá trị dưới số Catalan C15≈9,7⋅106C_{15} \approx 9{,}7 \cdot 10^6.

C++
ways[L][0] = 1;
for (ll i = L - 1; i >= 1; i--)
    for (ll h = 0; h <= n; h++) ways[i][h] = (h ? ways[i + 1][h - 1] : 0) + ways[i + 1][h + 1];

Bước 2: xếp hạng. Ở mỗi vị trí dãy đi lên trong khi đi xuống cũng hợp lệ (giá trị hiện tại dương), mọi dãy đi xuống tại đó đứng trước; cộng số cách của chúng.

C++
ll rk = 1;
for (ll i = 1; i < L; i++)
    if (c[i + 1] == c[i] + 1 && c[i] > 0) rk += ways[i + 1][c[i] - 1];

Bước 3: giải mã. Thử đi xuống trước vì giá trị nhỏ hơn đứng trước. kk không quá số cách đi xuống thì đi xuống, ngược lại trừ đi và đi lên.

C++
for (ll i = 1; i < L; i++) {
    ll down = h ? ways[i + 1][h - 1] : 0;
    if (k <= down) h--;
    else {
        k -= down;
        h++;
    }
    cout << ' ' << h;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, L, c[35], ways[35][20];

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

    cin >> n;
    L = 2 * n + 1;
    for (ll i = 1; i <= L; i++) cin >> c[i];
    ll k;
    cin >> k;
    ways[L][0] = 1;
    for (ll i = L - 1; i >= 1; i--)
        for (ll h = 0; h <= n; h++) ways[i][h] = (h ? ways[i + 1][h - 1] : 0) + ways[i + 1][h + 1];
    ll rk = 1;
    for (ll i = 1; i < L; i++)
        if (c[i + 1] == c[i] + 1 && c[i] > 0) rk += ways[i + 1][c[i] - 1];
    cout << rk << '\n';
    ll h = 0;
    cout << 0;
    for (ll i = 1; i < L; i++) {
        ll down = h ? ways[i + 1][h - 1] : 0;
        if (k <= down) h--;
        else {
            k -= down;
            h++;
        }
        cout << ' ' << h;
    }
    cout << '\n';
}
3
0 1 2 1 2 1 0
3

O(n2)O(n^2) cho bảng, O(n)O(n) cho mỗi lượt đi.

Ví dụDãy ngoặc cấp k

Đề. Các dãy ngoặc đúng độ dài N≤60N \le 60 có độ sâu lớn nhất đúng bằng kk, sắp theo từ điển với ( đứng trước ). In số dãy như vậy và thứ hạng của một dãy cho trước.

Lời giải. “Độ sâu lớn nhất đúng bằng kk” khó đếm thẳng, nhưng “không vượt LL” thì dễ: số dãy độ sâu đúng kk là số dãy không vượt kk trừ số dãy không vượt k−1k - 1. Xếp hạng theo khuôn từ điển: ở vị trí dãy đặt ), mọi dãy đặt ( tại đó đứng trước.

Bước 1: bảng “không vượt LL”. dp[L][r][b] là số cách viết rr ký tự còn lại từ số ngoặc mở bb, về 0 mà không lúc nào vượt LL. Mỗi bước mở thêm (nếu chưa vượt LL) hoặc đóng (nếu còn ngoặc mở).

C++
for (ll L = 0; L <= k; L++) {
    dp[L][0][0] = 1;
    for (ll r = 1; r <= n; r++)
        for (ll b = 0; b <= L; b++) dp[L][r][b] = (b + 1 <= L ? dp[L][r - 1][b + 1] : 0) + (b ? dp[L][r - 1][b - 1] : 0);
}
cout << dp[k][n][0] - dp[k - 1][n][0] << '\n';

Bước 2: xếp hạng. bal, mx theo dõi số mở và độ sâu lớn nhất của tiền tố thật. Ở vị trí dãy đặt ), thử đặt ( thay vào: số mở thành nb, độ sâu tiền tố thành nm. Nếu nm > k thì không có dãy nào; nếu tiền tố đã chạm kk thì đuôi chỉ cần không vượt kk; nếu chưa chạm thì đuôi phải chạm đúng kk, đếm bằng hiệu hai bảng.

C++
for (ll i = 0; i < n; i++) {
    if (s[i] == ')') {
        ll nb = bal + 1, nm = max(mx, nb), r = n - i - 1;
        if (nm == k) rk += dp[k][r][nb];
        else if (nm < k) rk += dp[k][r][nb] - dp[k - 1][r][nb];
    }
    bal += (s[i] == '(' ? 1 : -1);
    mx = max(mx, bal);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, k, dp[32][62][32];
string s;

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

    cin >> n >> k >> s;
    for (ll L = 0; L <= k; L++) {
        dp[L][0][0] = 1;
        for (ll r = 1; r <= n; r++)
            for (ll b = 0; b <= L; b++) dp[L][r][b] = (b + 1 <= L ? dp[L][r - 1][b + 1] : 0) + (b ? dp[L][r - 1][b - 1] : 0);
    }
    cout << dp[k][n][0] - dp[k - 1][n][0] << '\n';
    ll rk = 1, bal = 0, mx = 0;
    for (ll i = 0; i < n; i++) {
        if (s[i] == ')') {
            ll nb = bal + 1, nm = max(mx, nb), r = n - i - 1;
            if (nm == k) rk += dp[k][r][nb];
            else if (nm < k) rk += dp[k][r][nb] - dp[k - 1][r][nb];
        }
        bal += (s[i] == '(' ? 1 : -1);
        mx = max(mx, bal);
    }
    cout << rk << '\n';
}
6 2
(()())

O(k⋅N⋅k)O(k \cdot N \cdot k) cho bảng, O(N)O(N) cho xếp hạng.

Khi số đếm rất lớn

Thứ KK tới 101810^{18} mà số đối tượng có thể còn lớn hơn: số cách hoàn thiện tràn ll. Chỉ cần biết số cách có lớn hơn KK hay không, nên chặn trần: mọi phép cộng và nhân dừng ở một ngưỡng như 2⋅10182 \cdot 10^{18}. Khối có kích thước bị chặn vẫn được so sánh đúng với KK. Cũng hay gặp: số đối tượng tăng như dãy Fibonacci hay lũy thừa của 2, nên chỉ vài chục vị trí đầu cần đếm thật, còn lại số cách đã vượt mọi KK.

Ví dụKyoya Và Hoán Vị

Đề. Chia 1..n1..n (n≤50n \le 50) thành các khối liên tiếp dài 1 hoặc 2, khối dài 2 bị đảo. In hoán vị thứ k≤1018k \le 10^{18} theo từ điển trong các hoán vị tạo được.

Lời giải. Số cách chia mm phần tử là Fibonacci fmf_m, với f0=f1=1f_0 = f_1 = 1; f50≈2⋅1010f_{50} \approx 2 \cdot 10^{10} nên không cần chặn trần. Ở vị trí ii, khối dài 1 bắt đầu bằng ii, nhỏ hơn khối dài 2 bắt đầu bằng i+1i + 1; khối dài 1 có fn−if_{n - i} đuôi. kk không quá số đó thì đặt ii, không thì trừ đi và đặt i+1,ii + 1, i.

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

ll n, k, f[55];

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

    cin >> n >> k;
    f[0] = f[1] = 1;
    for (ll i = 2; i <= n; i++) f[i] = f[i - 1] + f[i - 2];
    for (ll i = 1; i <= n;) {
        if (k <= f[n - i]) {
            cout << i << ' ';
            i++;
        } else {
            k -= f[n - i];
            cout << i + 1 << ' ' << i << ' ';
            i += 2;
        }
    }
    cout << '\n';
}
3 3

Nhỏ nhất theo từ điển bằng quy hoạch động từ cuối

Khi đề hỏi đối tượng nhỏ nhất theo từ điển trong một tập phức tạp, quy hoạch động có thể lưu thẳng chuỗi tốt nhất làm giá trị. Nhưng hướng duyệt quan trọng: so sánh từ điển do ký tự đầu quyết định, nên tiền tố tốt nhất chưa chắc mở rộng thành chuỗi tốt nhất, còn hậu tố tốt nhất thì luôn đúng khi nối thêm ở phía trước. Vì vậy quy hoạch động từ cuối về đầu: dp[i][… ]dp[i][\dots] là chuỗi nhỏ nhất dựng từ phần i..ni..n, rồi dp[i]=min⁡(bỏ i, si+dp[i+1][… ])dp[i] = \min(\text{bỏ } i, \ s_i + dp[i+1][\dots]).

Ví dụThẻ Chuỗi

Đề. N≤50N \le 50 thẻ ghi chuỗi chữ thường dài tới 50. Chọn đúng KK thẻ và nối theo thứ tự tùy ý để được chuỗi nhỏ nhất theo từ điển.

Lời giải. Hai ý. Thứ nhất, với một tập thẻ đã chọn, nối theo thứ tự sắp bằng phép so sánh x+y<y+xx + y < y + x là tốt nhất: đổi hai thẻ kề nhau sai thứ tự không bao giờ làm chuỗi lớn hơn (bài 03). Thứ hai, sau khi sắp mọi thẻ theo thứ tự đó, chọn tập bằng quy hoạch động từ cuối về đầu: dp[i][j]dp[i][j] là chuỗi nhỏ nhất dùng đúng jj thẻ trong các thẻ i..Ni..N. Từ đầu về cuối thì sai: tiền tố nhỏ nhất có thể dẫn tới chuỗi lớn hơn, còn hậu tố nhỏ nhất thêm một thẻ vào đầu vẫn là ứng viên tốt nhất.

Bước 1: sắp thẻ. Phép so sánh a+b<b+aa + b < b + a bắc cầu, nên sort dùng được.

C++
bool cmp(string a, string b) {
    return a + b < b + a;
}

Bước 2: chọn từ cuối về. ok[i][j] cho biết có cách chọn đúng jj thẻ không. Chuyển trạng thái: bỏ thẻ ii, hoặc lấy thẻ ii đứng đầu rồi nối với chuỗi tốt nhất của phần sau. Thẻ ii đứng trước mọi thẻ sau nó theo thứ tự đã sắp, nên đặt nó ở đầu là đúng thứ tự nối.

C++
for (ll i = n; i >= 1; i--) {
    ok[i][0] = 1;
    for (ll j = 1; j <= n - i + 1; j++) {
        if (ok[i + 1][j]) {
            dp[i][j] = dp[i + 1][j];
            ok[i][j] = 1;
        }
        if (ok[i + 1][j - 1]) {
            string t = s[i] + dp[i + 1][j - 1];
            if (!ok[i][j] || t < dp[i][j]) dp[i][j] = t;
            ok[i][j] = 1;
        }
    }
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, k;
string s[55], dp[55][55];
bool ok[55][55];

bool cmp(string a, string b) {
    return a + b < b + a;
}

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

    cin >> n >> k;
    for (ll i = 1; i <= n; i++) cin >> s[i];
    sort(s + 1, s + n + 1, cmp);
    ok[n + 1][0] = 1;
    for (ll i = n; i >= 1; i--) {
        ok[i][0] = 1;
        for (ll j = 1; j <= n - i + 1; j++) {
            if (ok[i + 1][j]) {
                dp[i][j] = dp[i + 1][j];
                ok[i][j] = 1;
            }
            if (ok[i + 1][j - 1]) {
                string t = s[i] + dp[i + 1][j - 1];
                if (!ok[i][j] || t < dp[i][j]) dp[i][j] = t;
                ok[i][j] = 1;
            }
        }
    }
    cout << dp[1][k] << '\n';
}
4 3
b
a
baa
ba

Mỗi ô là một chuỗi dài tới 50⋅5050 \cdot 50 ký tự, so sánh và nối tốn O(2500)O(2500): tổng khoảng 50⋅50⋅250050 \cdot 50 \cdot 2500 phép.

Khi đi thi

  • Thứ KK hay thứ hạng trong một tập sắp từ điển: dựng từng vị trí, đếm số đuôi sau mỗi lựa chọn. Viết hàm đếm trước, kiểm bằng vét cạn với kích thước nhỏ, rồi mới viết giải mã và xếp hạng.
  • Kiểm tra chéo: giải mã thứ hạng vừa tính phải ra lại đối tượng; tổng các khối ở vị trí đầu phải bằng tổng số đối tượng.
  • Số đếm có thể vượt ll: chặn trần ở khoảng 2⋅10182 \cdot 10^{18}, hoặc nhận ra số đếm tăng theo hàm mũ và chỉ vài vị trí đầu cần tính thật. Đề cho KK là số hàng trăm chữ số thì phải dùng số lớn.

Tóm tắt

  • Đối tượng cùng tiền tố đứng liền nhau thành khối; giải mã thứ KK bằng cách trừ các khối đứng trước, xếp hạng bằng cách cộng chúng.
  • Hoán vị: khai triển Cantor với giai thừa; tổ hợp, chuỗi có số ký tự cố định: hệ số nhị thức.
  • Ràng buộc phức tạp: quy hoạch động đếm đuôi theo (vị trí, trạng thái); điều kiện trên cả đối tượng thì thêm trạng thái hoặc đếm bằng hiệu.
  • Số đếm lớn: chặn trần.
  • Nhỏ nhất theo từ điển: quy hoạch động lưu chuỗi, duyệt từ cuối về đầu.

Bài tập

1
Số hiệu tổ hợpcanuc80k4001Khối của số vv tại vị trí ii là (N−vK−i)\binom{N - v}{K - i}; số rất lớn nên cần cài cộng và so sánh số lớn dạng xâu hay mảng chữ số.
LQDOJ
2
Chuỗi Đẹp Thứ Kcanuc80k4003Chuỗi có đúng hai chữ b; chữ b đầu ở vị trí ii từ phải sang mở một khối i−1i - 1 chuỗi, tìm khối chứa kk rồi đặt chữ b thứ hai.
LQDOJ
3
Thứ tự từ điển của từ không lặp ký tựcanuc80k4004Từ dài MM không lặp trên NN chữ: khối của một chữ ở vị trí ii có (N−i)!(N−M)!\frac{(N - i)!}{(N - M)!} từ; kiểm từ có lặp chữ hay vượt bảng chữ thì báo không hợp lệ.
LQDOJ
4
Sắp Xếp Gần Đúngcanuc80k4006Hoán vị gần sắp xếp là các đoạn giảm liên tiếp nối nhau; có 2n−12^{n - 1} hoán vị, bit thứ ii của k−1k - 1 (từ cao) cho biết vị trí ii có nối với vị trí sau thành một đoạn giảm hay không.
LQDOJ
5
Chuỗi BAcanuc80k4007Gộp các dấu * liền nhau thành một nhóm cho tới c⋅kc \cdot k chữ b; thứ xx đọc như số trong hệ cơ số hỗn hợp từ nhóm cuối lên.
LQDOJ
6
Đếm Xâu Đối Xứng Không Vượt Scanuc80k4012Chuỗi đối xứng được quyết định bởi nửa đầu; đếm nửa đầu nhỏ hơn nửa đầu của SS như một số cơ số 26, cộng 1 nếu đối xứng của chính nửa đầu đó không vượt SS.
LQDOJ
7
Lại Về Thứ Tự Từ Điểncanuc80k4014Dựng từ trái sang, mỗi vị trí thử chữ nhỏ nhất còn lại; giữ được khi số vị trí sai tới giờ cộng số vị trí sai tối thiểu bắt buộc ở phần còn lại không vượt KK.
LQDOJ
8
Đừng Làm Dãy Concanuc80k4015Quy hoạch động từ cuối: độ dài chuỗi không phải dãy con ngắn nhất bắt đầu từ vị trí ii dùng lần xuất hiện kế tiếp của mỗi chữ; rồi dựng từ đầu, chọn chữ nhỏ nhất giữ đúng độ dài tối ưu.
LQDOJ
9
Ngôn Ngữ Học Reberlandcanuc80k4016Quy hoạch động từ cuối trên vị trí: đuôi từ ii tách được thành các đoạn dài 2 hoặc 3, hai đoạn liền nhau không giống nhau; gom mọi đoạn xuất hiện được vào set.
LQDOJ
10
Thứ tự dãy concanuc80k4017Đếm số dãy con dài jj bắt đầu sau vị trí ii bằng quy hoạch động; khối của phần tử tiếp theo được chọn đi theo giá trị tăng dần chứ không theo chỉ số.
LQDOJ
11
Đường Đi Nhỏ Nhấtcanuc80k4020Trước tiên quy hoạch động số chữ không phải a ít nhất trên đường tới mỗi ô, các ô đạt không quá KK coi như a; rồi lan từng đường chéo, mỗi bước chỉ giữ các ô có chữ nhỏ nhất.
LQDOJ
12
Số Đẹp Lớn Nhấtcanuc80k4022Thử giảm chữ số ở vị trí xa nhất từ phải có thể, phần còn lại điền 9 rồi chỉnh các chữ số cuối theo mặt nạ chẵn lẻ của tần suất; không được thì số có ít hơn hai chữ số, in toàn 9.
LQDOJ
13
Hoán Vị Nhỏ Nhất Với Số Nghịch Thế Cho Trướccanuc80k4024Dựng từ trái sang: đặt số nhỏ nhất chưa dùng khi phần còn lại vẫn đủ chỗ cho số nghịch thế cần thiết, tức m≤(n−i2)m \le \binom{n - i}{2}; không thì đặt số tạo đúng số nghịch thế cần.
LQDOJ
14
Mật mãcanuc80k4029ways[i][z]ways[i][z] là số chuỗi bit đuôi từ vị trí ii khi đang có zz bit 0 liên tiếp ở cuối; giải mã thứ XX với 0 đứng trước 1.
LQDOJ

Đọc thêm