Cho một dãy gồm số. Tìm độ dài lớn nhất của một dãy con tăng, tức là dãy thu được bằng cách xóa một số phần tử (có thể không xóa) và giữ nguyên thứ tự các phần tử còn lại, sao cho các giá trị tăng nghiêm ngặt. Bài toán này gọi là dãy con tăng dài nhất (longest increasing subsequence, viết tắt LIS), và ta cần giải trong .
Cách quy hoạch động thẳng gọi là độ dài dãy con tăng dài nhất kết thúc tại phần tử thứ , mỗi nhìn lại mọi phần tử đứng trước, mất . Nếu giữ thêm, với mỗi độ dài , giá trị cuối nhỏ nhất của một dãy con tăng dài , thì mỗi phần tử mới chỉ cần một lần chặt nhị phân trên mảng này. Trang này đi qua các bài quy hoạch động mà trạng thái là một vị trí hay một cặp vị trí trên một hoặc hai dãy: đoạn con tổng lớn nhất, LIS, xâu con chung dài nhất, khoảng cách sửa xâu và đường đi trên lưới.
IĐoạn con tổng lớn nhất
Cho dãy gồm các số nguyên có thể âm. Tìm đoạn con (các phần tử liên tiếp, khác dãy con ở chỗ không được bỏ phần tử giữa chừng) không rỗng có tổng lớn nhất. Thử mọi cặp đầu cuối rồi tính tổng bằng tổng tiền tố (bài 0.3) mất . Cũng đã biết cách làm : với mỗi vị trí cuối , lấy tổng tiền tố trừ đi tổng tiền tố nhỏ nhất trước đó.
Quy hoạch động nhìn bài toán theo cách khác. Gọi là tổng lớn nhất của một đoạn con kết thúc đúng tại . Đoạn này hoặc chỉ gồm , hoặc là một đoạn kết thúc tại được nối thêm ; đoạn kết thúc tại tốt nhất có tổng , nên
Ta chỉ cần giá trị của bước trước, nên một biến là đủ. Cách làm này thường gọi là thuật toán Kadane.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 3e5 + 5;
ll n, a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
ll cur = 0, ans = -1e18;
for (ll i = 1; i <= n; i++) {
cur = max(cur, 0LL) + a[i];
ans = max(ans, cur);
}
cout << ans << '\n';
}
8
-2 1 -3 4 -1 2 1 -5
6
Độ phức tạp: , với cần khoảng phép tính.
Hỏi nhanh
Một bạn viết chương trình tìm tổng lớn nhất của một đoạn con không rỗng, nhưng khởi tạo đáp án bằng . Bấm Chạy để xem chương trình in gì với dãy ba số :
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, a[100];
signed main() {
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
ll cur = 0, ans = 0;
for (ll i = 1; i <= n; i++) {
cur = max(cur, 0LL) + a[i];
ans = max(ans, cur);
}
cout << ans << '\n';
}
3
-3 -1 -2
0
Đáp án đúng là (đoạn chỉ có số ), nhưng chương trình in . Vì sao?
cur tính đúng tổng lớn nhất của đoạn kết thúc tại mỗi vị trí (, , ), nhưng ans = max(ans, cur) bắt đầu từ nên không bao giờ nhận số âm. Chỉ cần khởi tạo ans bằng một số rất âm như , hoặc bằng . Ba số cộng lại cho , nhỏ hơn ; mảng phần tử không liên quan vì .
Thêm trạng thái khi có thêm thao tác
Đề có thể cho thêm một thao tác, và khi đó trạng thái cần nhớ thêm thao tác đã làm tới đâu. Xét ví dụ sau.
Ví dụĐoạn Nhân Một Lần
Đề. Cho dãy (, ) và số nguyên (). Được chọn nhiều nhất một đoạn liên tiếp rồi nhân mọi phần tử trong đó với (cũng có thể không chọn). Sau đó lấy tổng lớn nhất của một đoạn con liên tiếp, đoạn con được phép rỗng (tổng ).
Lời giải. Với đoạn được lấy tổng, mỗi phần tử của nó nằm ở một trong ba vùng liên tiếp: trước vùng nhân, trong vùng nhân, hoặc sau vùng nhân; có vùng có thể rỗng. Ta gọi , , là tổng lớn nhất của một đoạn kết thúc tại vị trí hiện tại, mà phần tử cuối cùng đang ở vùng trước, vùng trong, vùng sau tương ứng.
Khi thêm phần tử , mỗi vùng chỉ có thể tiếp nối từ vùng của nó hoặc vùng ngay trước:
- : vẫn ở vùng trước, hoặc bắt đầu đoạn mới tại .
- : bị nhân; đoạn đến từ vùng trước, từ chính vùng trong, hoặc bắt đầu tại .
- : sang vùng sau, từ vùng trong hoặc từ vùng sau.
Đáp án là (đoạn rỗng) và lớn nhất của các giá trị , , sau mỗi phần tử. Cả ba cập nhật dùng giá trị cũ của bước trước, nên trong code tính xong rồi mới gán.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 3e5 + 5;
ll n, x, a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> x;
for (ll i = 1; i <= n; i++) cin >> a[i];
ll f0 = 0, f1 = -1e18, f2 = -1e18, ans = 0;
for (ll i = 1; i <= n; i++) {
ll g0 = max(f0, 0LL) + a[i];
ll g1 = max(max(f0, f1), 0LL) + x * a[i];
ll g2 = max(f1, f2) + a[i];
f0 = g0, f1 = g1, f2 = g2;
ans = max(ans, max(f0, max(f1, f2)));
}
cout << ans << '\n';
}
6 -3
4 -5 2 -1 3 -4
23
Độ phức tạp: , với cần khoảng phép tính.
IIDãy con tăng dài nhất: O(n²)
Ta bắt đầu với cách quy hoạch động thẳng. Gọi là độ dài dãy con tăng dài nhất kết thúc đúng tại . Cần ghi nhớ phần tử cuối trong định nghĩa, vì chỉ biết nó ta mới biết có nối thêm được hay không. Xét phần tử đứng ngay trước trong dãy tối ưu: nếu không có thì dãy chỉ gồm , độ dài ; nếu đó là với và thì phần còn lại là một dãy tăng kết thúc tại , dài nhất có thể là . Vậy
trong đó thứ hai lấy giá trị lớn nhất trong các thỏa điều kiện viết bên dưới, và số dành cho trường hợp không có nào. Đáp án là lớn nhất. Với dãy ta có , đáp án .
Mảng ghi phần tử đứng trước trong dãy tốt nhất. Đi ngược từ phần tử có lớn nhất theo rồi đảo lại, ta in được chính dãy con; đó là truy vết (reconstruction).
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 5005;
ll n, a[N], dp[N], par[N], seq[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> a[i];
ll best = 1;
for (ll i = 1; i <= n; i++) {
dp[i] = 1;
for (ll j = 1; j < i; j++)
if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
par[i] = j;
}
if (dp[i] > dp[best]) best = i;
}
ll len = 0;
for (ll i = best; i; i = par[i]) seq[len++] = a[i];
cout << dp[best] << '\n';
for (ll i = len - 1; i >= 0; i--) cout << seq[i] << ' ';
cout << '\n';
}
8
5 2 8 6 3 6 9 7
4
2 3 6 9
Độ phức tạp: , với cần khoảng phép tính.
Với cách này cần tới phép tính, quá chậm.
IIIDãy con tăng dài nhất: O(n log n)
Ta có thể thấy được điểm lãng phí của cách trên: mỗi xét lại mọi đứng trước, trong khi nhiều dãy cùng độ dài chỉ khác nhau ở phần tử cuối. Hai dãy con tăng cùng dài , dãy có phần tử cuối nhỏ hơn dễ được nối thêm hơn: mọi phần tử nối được vào dãy kia thì cũng nối được vào dãy này. Vì vậy, với mỗi độ dài ta chỉ cần nhớ phần tử cuối nhỏ nhất của một dãy con tăng dài , gọi là (mảng đánh số từ trong code, nên ứng với độ dài ).
Mảng tăng nghiêm ngặt: một dãy con tăng dài kết thúc tại chứa một dãy dài kết thúc tại giá trị nhỏ hơn . Khi đọc phần tử mới , ta tìm vị trí đầu tiên trong có giá trị không nhỏ hơn :
- Nếu không có vị trí nào, lớn hơn mọi phần tử cuối, ta nối vào dãy dài nhất và dài thêm .
- Nếu có vị trí , thì là phần tử cuối nhỏ hơn (hoặc bằng) cho dãy dài , nên ghi đè .
Vì tăng nên vị trí đó tìm được bằng chặt nhị phân. Độ dài cuối cùng của là đáp án. Cần lưu ý 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 tối ưu, ta ghi thêm cho mỗi phần tử vị trí của phần tử đứng trước nó ngay lúc chèn, rồi đi ngược như ở mục trước.
Ví dụNhững Mốc Tăng Dần
Đề. Cho dãy số nguyên. In độ dài lớn nhất của một dãy con tăng nghiêm ngặt.
Lời giải. Dùng mảng ở trên. Hàm lower_bound trả về vị trí đầu tiên có giá trị không nhỏ hơn , đúng như bước tìm của ta. Với dãy tăng nghiêm ngặt, hai giá trị bằng nhau không được cùng nằm trong một dãy, nên cần lower_bound: khi bằng một phần tử cuối đã có, ta ghi đè lên chính nó thay vì nối thêm.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n, a[N], tail[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll len = 0;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
ll p = lower_bound(tail, tail + len, a[i]) - tail;
tail[p] = a[i];
if (p == len) len++;
}
cout << len << '\n';
}
8
5 1 4 2 3 9 6 7
5
Độ phức tạp: , với cần khoảng phép tính.
Hỏi nhanh
Ta thử thay lower_bound bằng upper_bound trong chương trình trên, rồi chạy với ba số bằng nhau . Bấm Chạy để xem chương trình in gì:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, a[100], tail[100];
signed main() {
cin >> n;
ll len = 0;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
ll p = upper_bound(tail, tail + len, a[i]) - tail;
tail[p] = a[i];
if (p == len) len++;
}
cout << len << '\n';
}
3
2 2 2
3
Với bài dãy con tăng nghiêm ngặt, đáp án đúng cho dãy này là . Chương trình in vì sao?
upper_bound trả về vị trí đầu tiên có giá trị lớn hơn . Khi và , vị trí đó là cuối mảng, nên được nối thêm và độ dài tăng, nghĩa là cho phép hai số bằng nhau đứng cạnh nhau. Đó chính là cách cài cho dãy không giảm. Mảng tail chỉ được đọc trong phạm vi len nên giá trị khởi tạo không ảnh hưởng, và upper_bound không luôn trả về cuối mảng (nó trả về cuối mảng chỉ khi mọi phần tử trong tail không lớn hơn ).
Cùng ý này còn giải được nhiều bài khác. Ví dụ, cho khung hình chữ nhật, chọn nhiều khung nhất sao cho chúng lồng được vào nhau: sắp các khung theo chiều rộng tăng dần (với chiều rộng bằng nhau thì xếp chiều cao giảm dần), rồi tìm dãy con tăng dài nhất theo chiều cao.
IVXâu con chung dài nhất
Cho hai xâu và . Tìm một dãy con chung dài nhất, tức là xâu vừa là dãy con của vừa là dãy con của (xóa bớt ký tự nhưng giữ thứ tự), gọi là bài xâu con chung dài nhất (longest common subsequence, LCS). Với có dãy con của mỗi xâu, nên không thể liệt kê.
Trạng thái là một cặp tiền tố: là độ dài dãy con chung dài nhất của ký tự đầu của và ký tự đầu của . Xét hai ký tự cuối và :
với hàng và cột bằng . Khi hai ký tự khác nhau, một dãy con chung không thể dùng cả hai (nếu dùng thì cả hai là ký tự cuối của dãy, nên phải bằng nhau), nên phải bỏ một trong hai: bỏ cho , bỏ cho . Khi hai ký tự bằng nhau, ghép chúng làm ký tự cuối của dãy không bao giờ thiệt hơn: nếu dãy tối ưu dùng nhưng ghép với ký tự khác của , ta đổi cách ghép sang mà dãy vẫn dài như cũ.
Truy vết từ ô : hai ký tự bằng nhau thì ghi lại ký tự đó và đi chéo; khác nhau thì đi sang ô kề có giá trị lớn hơn (ô trên hoặc ô trái). Mỗi lần ghi, giảm đúng nên ta ghi đúng ký tự; đảo ngược xâu ghi được là xong. Cần giữ cả bảng để truy vết.
Ví dụChuỗi Chung Dài Nhất
Đề. Cho hai chuỗi và , mỗi chuỗi dài tối đa . In một dãy con chung dài nhất (bất kỳ nếu có nhiều); nếu độ dài lớn nhất bằng thì in dòng rỗng.
Lời giải. Dùng đúng công thức và cách truy vết ở trên. Bảng có ô; với kiểu int khoảng MB. Chuỗi đánh số từ (s[i - 1]) nhưng bảng đánh số từ , vì là độ dài của tiền tố.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
string s, t;
int dp[3005][3005];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s >> t;
ll n = s.size(), m = t.size();
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
string ans;
ll i = n, j = m;
while (i > 0 && j > 0) {
if (s[i - 1] == t[j - 1]) {
ans += s[i - 1];
i--;
j--;
} else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
else j--;
}
reverse(ans.begin(), ans.end());
cout << ans << '\n';
}
abcde
ace
ace
Độ phức tạp: , với cần khoảng phép tính.
VKhoảng cách sửa xâu
Cho hai xâu và . Mỗi bước được chèn một ký tự vào vị trí bất kỳ, xóa một ký tự, hoặc thay một ký tự bằng ký tự khác, mỗi bước tốn . Tìm số bước ít nhất để biến thành , gọi là khoảng cách sửa xâu (edit distance). Với .
Cũng dùng cặp tiền tố: là số bước ít nhất để biến ký tự đầu của thành ký tự đầu của . Biến xâu rỗng thành ký tự cần lần chèn, và ngược lại cần lần xóa, nên và . Xét hai ký tự cuối và :
- Nếu , ta giữ chúng nguyên: .
- Nếu khác nhau, bước cuối cùng của một phương án tối ưu là một trong ba loại: xóa (còn biến ký tự đầu thành ký tự đầu, ), chèn vào cuối (còn biến ký tự đầu thành ký tự đầu, ), hoặc thay thành (). Tốn thêm bước:
Mỗi hàng chỉ dùng hàng ngay trước nó, và bài này chỉ hỏi số bước chứ không cần in phương án, nên ta giữ hai hàng thay cho cả bảng ô.
Ví dụBiến Đổi Hai Xâu
Đề. Cho hai xâu và (, gồm chữ in hoa). Mỗi thao tác chèn, xóa hoặc thay một chữ cái tốn . In số thao tác ít nhất để biến thành .
Lời giải. Dùng công thức ở trên. Hàng của lưu ở d[i & 1], hàng trước ở d[(i - 1) & 1]; trước khi tính hàng , đặt ô đầu hàng bằng .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
string a, b;
ll d[2][5005];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> a >> b;
ll n = a.size(), m = b.size();
for (ll j = 0; j <= m; j++) d[0][j] = j;
for (ll i = 1; i <= n; i++) {
ll p = i & 1, q = p ^ 1;
d[p][0] = i;
for (ll j = 1; j <= m; j++) {
if (a[i - 1] == b[j - 1]) d[p][j] = d[q][j - 1];
else d[p][j] = 1 + min(min(d[q][j], d[p][j - 1]), d[q][j - 1]);
}
}
cout << d[n & 1][m] << '\n';
}
STONE
MONEY
3
Độ phức tạp: thời gian, bộ nhớ, với cần khoảng phép tính.
VIĐường đi trên lưới
Khi trạng thái là một ô của bảng, công thức chuyển thường lấy từ các ô kề đã tính. Xét bảng số nguyên (có thể âm). Đi từ ô tới ô , mỗi bước sang phải hoặc xuống dưới. Tìm đường có tổng các ô đi qua lớn nhất và in đường đó. Số đường là , quá nhiều để duyệt.
Gọi là tổng lớn nhất của một đường từ tới . Bước cuối tới đến từ ô trên hoặc ô trái:
Duyệt từng hàng từ trên xuống, mỗi hàng từ trái sang phải, thì khi tới cả hai ô nguồn đã có. Các ô ngoài bảng (hàng , cột ) phải là , không phải : nếu đặt , một đường “xuất phát từ ngoài bảng” sẽ thắng mỗi khi mọi đường thật có tổng âm.
Muốn in đường, ta đi ngược từ . Tại mỗi ô chọn ô nguồn có lớn hơn (đó chính là ô đã cho ra ), ghi lại bước vừa lùi, dừng khi tới rồi đảo chuỗi bước.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll R, C, a[105][105], dp[105][105];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> R >> C;
for (ll i = 1; i <= R; i++)
for (ll j = 1; j <= C; j++) cin >> a[i][j];
for (ll i = 0; i <= R; i++)
for (ll j = 0; j <= C; j++) dp[i][j] = -1e18;
for (ll i = 1; i <= R; i++)
for (ll j = 1; j <= C; j++) {
if (i == 1 && j == 1) dp[i][j] = a[i][j];
else dp[i][j] = a[i][j] + max(dp[i - 1][j], dp[i][j - 1]);
}
string path;
ll i = R, j = C;
while (i > 1 || j > 1) {
if (dp[i - 1][j] >= dp[i][j - 1]) {
path += 'D';
i--;
} else {
path += 'R';
j--;
}
}
reverse(path.begin(), path.end());
cout << dp[R][C] << '\n' << path << '\n';
}
3 4
2 -3 4 1
-1 5 -2 3
3 -4 6 -1
9
DRRDR
Độ phức tạp: , với cần khoảng phép tính.
Đường tối ưu đi xuống, phải, phải, xuống, phải: .
Hỏi nhanh
Bảng một hàng ba ô . Một chương trình dùng công thức cho mọi ô, kể cả ô , nhưng khởi tạo các ô viền (hàng , cột ) bằng . Nó in ra gì cho ô ?
Tính tay: ; ; . Viền giả vờ rằng có đường đi vào từ phía trên với tổng , nên đường được “nhảy qua” ô . Đường duy nhất thật có tổng . Viền phải là vì ô ngoài bảng không có đường nào đi qua; viền chỉ vô hại khi mọi ô đều không âm.
Cùng khuôn này, thay max bằng phép cộng và đặt ô cấm bằng ta đếm được số đường đi.
Ví dụĐường Đi Qua Lưới
Đề. Lưới (), mỗi ô là ô trống . hoặc ô cấm *. Từ ô góc trên bên trái, mỗi bước đi sang phải hoặc xuống dưới, không bước vào ô cấm. Đếm số đường đi tới ô góc dưới bên phải, modulo .
Lời giải. Gọi là số đường tới . Ô cấm có ; ô trống khác ô xuất phát có (lấy dư), vì mọi đường tới có đúng một bước cuối, từ ô trên hoặc ô trái. Ô xuất phát có nếu nó là ô trống. Viền ngoài bảng bằng , vì không có đường nào đi vào từ ngoài; đây là bài đếm nên viền mới đúng.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7;
ll n, dp[1005][1005];
char g[1005][1005];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= n; j++) cin >> g[i][j];
if (g[1][1] == '.') dp[1][1] = 1;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= n; j++) {
if (g[i][j] == '*' || (i == 1 && j == 1)) continue;
dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) % MOD;
}
cout << dp[n][n] << '\n';
}
5
.....
.**..
...*.
*....
.....
9
Độ phức tạp: , với cần khoảng phép tính.
Khi hai người cùng đi từ hai đầu và phải cùng đến một ô, ta có thể xét hai người đi đồng thời từng bước: sau bước, mỗi người nằm trên đường chéo , nên trạng thái chỉ cần hai tọa độ hàng thay vì bốn tọa độ.
Bài nâng cao
Xem 2 bài nâng cao
Ví dụHai Hành Trình Và Một Điểm Hẹn
Đề. Bảng (), ô có giá trị . Người thứ nhất đi từ tới , mỗi bước xuống hoặc sang phải. Người thứ hai đi từ tới , mỗi bước lên hoặc sang phải. Hai người phải gặp nhau tại đúng một ô chung, ô này không được tính điểm; mọi ô khác trên hai hành trình được cộng vào tổng điểm. Tìm tổng điểm lớn nhất.
Lời giải. Ta cần giá trị tốt nhất của đường đi từ mỗi góc tới mỗi ô, nên tính bốn bảng bằng đúng công thức đường đi lớn nhất ở mục trên, mỗi bảng từ một góc: từ , từ , từ và từ , trong đó và chạy theo chiều ngược lại của người đi. Ô gặp nằm bên trong bảng (không nằm ở hàng hoặc cột biên). Người thứ nhất vào ô gặp theo hướng sang phải hoặc xuống, người thứ hai theo hướng sang phải hoặc lên; vì mỗi người chỉ đi qua ô đó một lần và hai người không dùng chung ô nào khác, chỉ có hai cách ghép không giao nhau:
- Người thứ nhất đi vào từ ô trên và ra ở ô dưới ; người thứ hai vào từ ô trái và ra ở ô phải .
- Người thứ nhất vào từ ô trái và ra ở ô phải ; người thứ hai vào từ ô dưới và ra ở ô trên .
Tổng điểm của cấu hình thứ nhất là , cấu hình thứ hai là . Đáp án là giá trị lớn nhất trên mọi ô trong và cả hai cấu hình.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1005;
ll n, m, a[N][N], d1[N][N], d2[N][N], d3[N][N], d4[N][N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= m; j++) cin >> a[i][j];
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= m; j++) d1[i][j] = a[i][j] + max(d1[i - 1][j], d1[i][j - 1]);
for (ll i = n; i >= 1; i--)
for (ll j = m; j >= 1; j--) d2[i][j] = a[i][j] + max(d2[i + 1][j], d2[i][j + 1]);
for (ll i = n; i >= 1; i--)
for (ll j = 1; j <= m; j++) d3[i][j] = a[i][j] + max(d3[i + 1][j], d3[i][j - 1]);
for (ll i = 1; i <= n; i++)
for (ll j = m; j >= 1; j--) d4[i][j] = a[i][j] + max(d4[i - 1][j], d4[i][j + 1]);
ll ans = 0;
for (ll i = 2; i < n; i++)
for (ll j = 2; j < m; j++) {
ll x = d1[i - 1][j] + d2[i + 1][j] + d3[i][j - 1] + d4[i][j + 1];
ll y = d1[i][j - 1] + d2[i][j + 1] + d3[i + 1][j] + d4[i - 1][j];
ans = max(ans, max(x, y));
}
cout << ans << '\n';
}
3 3
2 4 6
8 0 10
12 14 16
72
Độ phức tạp: , với cần khoảng phép tính.
Ví dụGhép Đúng K Khối Chung
Đề. Cho hai xâu và () và số . Chọn đúng xâu không rỗng sao cho trong cả lẫn , chúng xuất hiện dưới dạng các đoạn con liên tiếp, đôi một không giao nhau và theo đúng thứ tự (đoạn của nằm trước đoạn của trong từng xâu). Tối đa hóa tổng độ dài . Input bảo đảm có cách chọn.
Lời giải. Đây là LCS thêm một chiều đếm số khối. Với mỗi cặp tiền tố và số khối ta nhớ hai giá trị. là tổng độ dài lớn nhất khi dùng đúng khối trong ký tự đầu của và ký tự đầu của . giống nhưng ép ký tự và ghép với nhau thành ký tự cuối của khối thứ (chỉ xảy ra khi ). Khi , ký tự này hoặc nối thêm vào khối đang dở, hoặc mở khối mới:
và không có giá trị (đặt ) nếu . Còn và . Đáp án là . Hàng chỉ dùng hàng , nên ta giữ hai hàng theo để bảng không quá lớn.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll NEG = -1e9;
ll n, m, K, f[2][1005][11], g[2][1005][11];
string s, t;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> K >> s >> t;
for (ll j = 0; j <= m; j++)
for (ll k = 0; k <= K; k++) {
g[0][j][k] = k == 0 ? 0 : NEG;
f[0][j][k] = NEG;
}
for (ll i = 1; i <= n; i++) {
ll p = i & 1, q = p ^ 1;
for (ll k = 0; k <= K; k++) {
g[p][0][k] = k == 0 ? 0 : NEG;
f[p][0][k] = NEG;
}
for (ll j = 1; j <= m; j++) {
g[p][j][0] = 0;
f[p][j][0] = NEG;
for (ll k = 1; k <= K; k++) {
if (s[i - 1] == t[j - 1]) f[p][j][k] = 1 + max(f[q][j - 1][k], g[q][j - 1][k - 1]);
else f[p][j][k] = NEG;
g[p][j][k] = max(max(g[q][j][k], g[p][j - 1][k]), f[p][j][k]);
}
}
}
cout << g[n & 1][m][K] << '\n';
}
6 6 2
abcxyz
abqxyz
5
Độ phức tạp: , với và cần khoảng phép tính.
Khi đi thi
- Dãy con (có thể bỏ phần tử) nghĩ tới LIS và LCS; đoạn con (liên tiếp) nghĩ tới Kadane và tổng tiền tố.
- LIS với lớn thì viết bản : nghiêm ngặt dùng
lower_bound, không giảm dùngupper_bound. - Bảng vượt bộ nhớ thì giữ hai hàng; muốn truy vết thì phải giữ cả bảng (hoặc lưu lựa chọn gọn hơn).
Tóm tắt
- Đoạn con tổng lớn nhất: ; có thao tác thêm thì thêm trạng thái cho từng giai đoạn của thao tác.
- LIS: là độ dài dài nhất kết thúc tại cho ; mảng phần tử cuối nhỏ nhất theo độ dài và chặt nhị phân cho .
- LCS và khoảng cách sửa xâu dùng bảng trên hai tiền tố; truy vết bằng cách đi ngược bảng từ ô đáp án.
- Đường đi trên lưới: bước cuối đến từ ô trên hoặc ô trái; bài tối ưu có viền , bài đếm có viền .
- Nhiều đường đi xuất phát từ nhiều góc thì tính một bảng cho mỗi góc rồi ghép tại ô gặp.