Cây phân đoạn (phần 2) - Một số bài toán cơ bản ứng dụng Cây phân đoạn
Trong bài viết trước, chúng ta đã cùng nhau xem xét những kiến thức tổng quan về cấu trúc dữ liệu Cây phân đoạn, hay còn gọi là Segment Tree, Interval Tree. Các bạn có thể xem lại bài viết về chủ đề trên tại đây.
Ở bài viết này, tôi sẽ giới thiệu tới các bạn một số bài toán cơ bản sử dụng Cây phân đoạn, với mục tiêu làm rõ chức năng giảm độ phức tạp thuật toán của cấu trúc dữ liệu này, đồng thời giúp các bạn có cái nhìn tổng quan hơn về cách áp dụng nó trong các bài toán lập trình cụ thể.
Bài toán 1: Dãy con tăng dài nhất
Đề bài
Cho dãy số gồm phần tử . Một dãy con của dãy số là cách chọn ra hoặc một số phần tử (có thể không liên tiếp) của dãy.
Yêu cầu: Tìm dãy con tăng ngặt dài nhất của dãy
Input:
- Dòng đầu chứa số nguyên dương .
- Dòng thứ hai chứa số nguyên phân tách nhau bởi dấu cách.
Ràng buộc:
- .
- .
Output:
- In ra độ dài dãy con tăng ngặt dài nhất tìm được.
Sample Input:
5
2 1 4 3 5
Sample Output:
3
Ý tưởng
Đây là một bài toán rất cơ bản của quy hoạch động, và các bạn hoàn toàn có thể giải quyết nó trong thời gian bằng phương pháp Tìm kiếm nhị phân. Tuy nhiên, ở bài viết này tôi sẽ giới thiệu cách giải bài toán này bằng Cây phân đoạn. Tuy nhiên, chúng ta sẽ cần một kĩ thuật bổ trợ, đó là Rời rạc hóa.
Phương pháp rời rạc hóa
Rời rạc hóa là một lĩnh vực lớn có đối tượng nghiên cứu là các tập hợp rời rạc trong khoa học máy tính. Ứng dụng của của phương pháp rất lớn và thường được sử dụng trong rất nhiều kỳ thi lớn.
Khi lập trình, phương pháp rời rạc hóa thường được gọi là "nén số". Thật vậy, đúng như tên gọi, bản chất của phương pháp ta hiểu nôm na là đưa các vùng dữ liệu lớn về vùng dữ liệu nhỏ để dễ xử lý, sao cho vẫn thỏa mãn yêu cầu của bài toán đặt ra.
Kỹ thuật bổ trợ trong phương pháp này là đánh lại số thứ tự hay còn được gọi là nén số, được thực hiện như sau: Giả sử có một mảng gồm phần tử với giá trị thuộc đoạn (có thể lớn hơn nữa), ta sẽ "nén" nó về một mảng nhỏ hơn, có giá trị thuộc tập nhưng vẫn đảm bảo quan hệ lớn - bé của các phần tử trong mảng như ban đầu.
Lấy ví dụ, mảng . Ta sẽ nén nó về một mảng vẫn đảm bảo tương quan lớn - nhỏ giữa các phần tử.
Cách thực hiện như sau:
- Sử dụng thêm mảng là một mảng chứa các bản ghi gồm hai trường: Một trường lưu giá trị tương ứng trên mảng và một trường lưu vị trí .
- Sắp xếp lại mảng dựa trên trường giá trị .
- Khởi tạo một biến - đây chính là các giá trị số được nén. Sau đó duyệt qua từng phần tử trên mảng nếu thì cập nhật tăng thêm đơn vị, ngược lại thì giữ nguyên .
- Gán để có giá trị nén trên mảng .
void discretizing(int n, vector < int > &a)
{
// Sử dụng kiểu pair để thay cho bản ghi gồm hai trường.
vector < pair < int, int > > b(n + 1);
for (int i = 1; i <= n; ++i)
b[i] = {a[i], i};
sort(b.begin() + 1, b.end());
int d = 1;
a[b[1].second] = d;
for (int i = 2; i <= n; ++i)
a[b[i].second] = (b[i].first != b[i - 1].first) ? ++d : d;
}
Sử dụng Cây phân đoạn để giải bài toán LIS
Sau khi đã rời rạc hóa mảng , ta gọi là độ dài dãy con tăng dài nhất kết thúc tại phần tử .
Công thức truy hồi dĩ nhiên rất đơn giản:
Nhìn vào công thức trên, có thể thấy các được cập nhật từ những mà có phần tử kết thúc thuộc một đoạn cố định là . Nói cách khác, nếu coi các giá trị của mảng như một dãy các chỉ số từ tới và gán cho mỗi chỉ số một giá trị tương ứng của nó, thì việc tính công thức truy hồi sẽ trở thành tìm giá trị lớn nhất trên một đoạn chỉ số liên tiếp.
Từ đây, ta có phương pháp sử dụng Cây phân đoạn như sau:
- Dựng một cây phân đoạn với các chỉ số thể hiện các giá trị trên mảng . Chẳng hạn sẽ đại diện cho giá trị thỏa mãn . Cây phân đoạn này sẽ lưu lớn nhất của các dãy con tăng dài nhất kết thúc tại phần tử có giá trị .
- Mỗi khi cần tính một ta sẽ lấy giá trị lớn nhất mà có phần tử kết thúc dựa vào cây phân đoạn.
- Sau khi tính xong ta cập nhật giá trị vào vị trí trên cây.
Như vậy ta thu được thuật toán có độ phức tạp .
Cài đặt
#include <bits/stdc++.h>
#define int long long
using namespace std;
void enter(int &n, vector < int > &a, vector < pair < int, int > > &b)
{
cin >> n;
a.resize(n + 1);
b.resize(n + 1);
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
b[i] = {a[i], i};
}
}
// Rời rạc hóa mảng A.
void discretizing(int n, vector < int > &a, vector < pair < int, int > > &b)
{
sort(b.begin() + 1, b.end());
int d = 1;
a[b[1].second] = d;
for (int i = 2; i <= n; ++i)
a[b[i].second] = (b[i].first != b[i - 1].first) ? ++d : d;
}
// Cập nhật lên cây phân đoạn ở vị trí pos giá trị mới là val.
void update(int id, int u, int v, int pos, int val, vector < int > &st)
{
if (v < pos || u > pos)
return;
if (u == v && v == pos)
{
st[id] = val;
return;
}
int mid = (u + v) >> 1;
update(2 * id, u, mid, pos, val, st);
update(2 * id + 1, mid + 1, v, pos, val, st);
st[id] = max(st[2 * id], st[2 * id + 1]);
}
// Lấy giá trị lớn nhất trên đoạn [l, r] của cây phân đoạn.
int get(int id, int u, int v, int l, int r, vector < int > &st)
{
if (u > r || v < l)
return 0;
if (u >= l && v <= r)
return st[id];
int mid = (u + v) >> 1;
int get1 = get(2 * id, u, mid, l, r, st);
int get2 = get(2 * id + 1, mid + 1, v, l, r, st);
return max(get1, get2);
}
void solution(int n, vector < int > &a, vector < pair < int, int > > &b)
{
discretizing(n, a, b);
// Tính toán quy hoạch động dựa trên cây phân đoạn.
int res = 0;
vector < int > dp(n + 1), st(4 * n + 1);
for (int i = 1; i <= n; ++i)
{
dp[i] = get(1, 1, n, 1, a[i] - 1, st) + 1;
res = max(res, dp[i]);
update(1, 1, n, a[i], dp[i], st);
}
cout << res;
}
main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int n;
vector < int > a;
vector < pair < int, int > > b;
enter(n, a, b);
solution(n, a, b);
return 0;
}
Bài toán 2: Lắp radars
Đề bài
HN là một giám đốc của một công ty kỹ thuật số truyền hình, anh đang có kế hoạch triển khai hệ thống radars trên con đường chính của thành phố ABZ. Hiện tại, anh đang có trong tay một danh sách các địa điểm có thể cài đặt các radars, các địa điểm này được sắp xếp theo thứ tự không giảm, mỗi radar được lắp đặt sẽ cho công ty của anh một giá trị lợi nhuận không nhỏ. Để đảm bảo các radars không bị ảnh hưởng tới sóng thu phát của nhau thì khoảng cách giữa hai radar được lắp đặt không thể ít hơn . Lấy ví dụ, với danh sách lắp đặt hệ thống tại vị trí và và lợi nhuận tương ứng là và . Nếu thì khi lựa chọn các radar ở vị trí và để lắp đặt sẽ cho ra lợi nhuận bằng .
Yêu cầu: Cho trước danh sách các địa điểm và lợi nhuận, hãy giúp HN lựa chọn các địa điểm lắp đặt các hệ thống radar để lợi nhuận thu được là tối đa?
Input:
- Dòng đầu tiên chứa số nguyên dương – số lượng test cases.
- nhóm dòng tiếp theo, mỗi nhóm dòng chứa một tes case, cấu trúc như sau:
- Dòng đầu tiên chứa hai số nguyên dương và – số lượng hệ thống radars và giới hạn khoảng cách tối thiểu giữa hai radar liên tiếp được chọn.
- Dòng tiếp theo chứa số nguyên dương phân tách nhau bởi dấu cách – vị trí của radar thứ .
- Dòng cuối cùng chứa số nguyên dương phân tách nhau bởi dấu cách – lợi nhuận thu được nếu lắp đặt hệ thống radar thứ .
Ràng buộc:
- .
- .
- .
- .
Output:
- Trên dòng, dòng thứ đưa ra một số nguyên duy nhất là tổng lợi nhuận lớn nhất thu được ở test case thứ .
Sample Input:
3
2 1
1 2
3 2
3 2
1 2 3
2 4 3
5 5
1 5 10 15 17
5 20 10 15 25
Sample Output:
5
5
55
Ý tưởng
Đây là một bài toán quy hoạch động đơn giản. Gọi là lợi nhuận lớn nhất thu được khi đặt các radars từ vị trí tới vị trí . Công thức quy hoạch động rất đơn giản, ta xét hai trường hợp:
- Radar cuối cùng đặt tại vị trí thì các radars khác chỉ được phép đặt từ vị trí tới . Khi đó lợi nhuận thu được là:
- Không đặt radar nào vào vị trí thì lợi nhuận thu được là .
Tất nhiên vì ta cần lấy giá trị lợi nhuận lớn nhất, nên sẽ bằng giá trị lớn nhất trong hai trường hợp trên. Nhưng làm thế nào để tính toán được giá trị Nếu như duyệt lại các giá trị thì độ phức tạp sẽ lên tới kết hợp với việc duyệt từ tới thì sẽ không thể vượt qua được test cases. Giải pháp tốt hơn sẽ là sử dụng cây phân đoạn.
Ta sử dụng một cây phân đoạn để kiểm soát giá trị lớn nhất của các . Cụ thể hơn, cây phân đoạn sẽ kiểm soát các tọa độ từ tới và giá trị của các nút trên cây sẽ là giá trị lớn nhất của tọa độ mà nút đó quản lý.
Mỗi khi tính toán xong một ta update giá trị vào vị trí trên cây phân đoạn, như vậy thao tác lấy kết quả sẽ nhanh hơn rất nhiều.
Độ phức tạp thuật toán khi đó sẽ đạt được là .
Cài đặt
Cần lưu ý một số điểm sau:
- Cây phân đoạn kiểm soát các tọa độ do đó kích thước tối đa của nó phải là chứ không phải như ở các bài toán thường thấy.
- Khi update lên cây phân đoạn, vị trí cần update là vị trí chứ không phải vị trí . Đây là những thao tác dễ nhầm lẫn nên các bạn cần hết sức cẩn thận.
#include <bits/stdc++.h>
#define int long long
using namespace std;
void enter_testcase(int &n, int &k, vector < int > &a, vector < int > &b)
{
cin >> n >> k;
a.resize(n + 1);
b.resize(n + 1);
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
cin >> b[i];
}
void update(int id, int u, int v, int pos, int val, vector < int > &st)
{
if (v < pos || u > pos)
return;
if (u == v)
{
st[id] = val;
return;
}
int mid = (u + v) >> 1;
update(2 * id, u, mid, pos, val, st);
update(2 * id + 1, mid + 1, v, pos, val, st);
st[id] = max(st[2 * id], st[2 * id + 1]);
}
int get(int id, int u, int v, int l, int r, vector < int > &st)
{
if (u > r || v < l)
return 0;
if (u >= l && v <= r)
return st[id];
int mid = (u + v) >> 1;
int get1 = get(2 * id, u, mid, l, r, st);
int get2 = get(2 * id + 1, mid + 1, v, l, r, st);
return max(get1, get2);
}
void solution(int n, int k, vector < int > &a, vector < int > &b)
{
int maxa = *max_element(a.begin() + 1, a.end());
vector < int > dp(n + 1), st(4 * maxa + 1);
for (int i = 1; i <= n; ++i)
{
dp[i] = max(dp[i - 1], get(1, 1, maxa, 1, a[i] - k, st) + b[i]);
update(1, 1, maxa, a[i], dp[i], st);
}
cout << dp[n] << endl;
}
main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int ntest;
cin >> ntest;
while (ntest--)
{
int n, k;
vector < int > a, b;
enter_testcase(n, k, a, b);
solution(n, k, a, b);
}
return 0;
}
Bài toán 3: Dãy con liên tiếp
Đề bài
Cho dãy số nguyên gồm phần tử và một số nguyên . Một dãy con liên tiếp của dãy số là một cách chọn ra các phần tử liên tiếp nhau trong dãy và loại bỏ các phần tử còn lại.
Yêu cầu: Đếm số lượng dãy con liên tiếp khác nhau có trung bình cộng không nhỏ hơn Hai dãy con liên tiếp được gọi là khác nhau nếu như chúng có phần tử đầu hoặc cuối dãy là khác nhau.
Input:
- Dòng đầu tiên chứa hai số nguyên dương và .
- Dòng thứ hai chứa số nguyên dương phân tách nhau bởi dấu cách.
Ràng buộc:
- .
- .
- .
Output:
- In ra số lượng dãy con có trung bình cộng không nhỏ hơn tìm được.
Sample Input:
3 3
1 4 2
Sample Output:
2
Ý tưởng
Với giá trị nhỏ, ta có thể sử dụng hai vòng lặp để duyệt mọi dãy con liên tiếp, tính trung bình cộng và đếm số dãy thỏa mãn yêu cầu đề bài.
Độ phức tạp của cách làm trên là không đủ tốt với ràng buộc . Ta sẽ phải cải tiến thuật toán.
Không mất tính tổng quát, giả sử thì các dãy con liên tiếp có trung bình cộng lớn hơn hoặc bằng sẽ trở thành các dãy con liên tiếp có tổng lớn hơn hoặc bằng .
Xây dựng mảng tổng tiền tố của dãy sau khi xử lý . Với mỗi ta lưu hai thông tin là tổng các số từ tới và vị trí của tổng đó, sau đó sắp xếp lại các theo thông tin thứ nhất. Giả sử hai thông tin lần lượt là và .
Dễ thấy, sau khi sắp xếp lại, mỗi đều có thể ghép với mọi để tạo thành tổng dương. Bây giờ cần đếm xem trong các đó, có những vị trí nào thỏa mãn thì mới tạo thành một đoạn con hợp lệ (vị trí sau trừ đi vị trí trước trên mảng tổng tiền tố).
Sử dụng một cây phân đoạn, với mục đích để đếm số lượng giá trị xuất hiện trong đoạn . Khi xét tới số lượng vị trí ghép được với là số lượng giá trị có đã xuất hiện trước đó. Cây phân đoạn dùng để tính tổng đoạn này. Sau mỗi lần tính, cập nhật vị trí tăng thêm trên cây để chuẩn bị cho các vị trí tiếp theo.
Độ phức tạp: .
Cài đặt
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e6;
int st[maxn];
void enter(int &n, int &p, vector < int > &a, vector < pair < int, int > > &sum)
{
cin >> n >> p;
a.resize(n + 1);
sum.resize(n + 1);
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
a[i] -= p;
sum[i].first = sum[i - 1].first + a[i];
sum[i].second = i;
}
}
void update(int id, int u, int v, int pos, int value, int st[])
{
if (u > pos || v < pos)
return;
if (u == v && v == pos)
{
st[id] += 1;
return;
}
int mid = (u + v) >> 1;
update(id * 2, u, mid, pos, value, st);
update(id * 2 + 1, mid + 1, v, pos, value, st);
st[id] = st[id * 2] + st[id * 2 + 1];
}
int get(int id, int u, int v, int l, int r, int st[])
{
if (u > r || v < l)
return 0;
if (u >= l && v <= r)
return st[id];
int mid = (u + v) >> 1;
return get(id * 2, u, mid, l, r, st) + get(id * 2 + 1, mid + 1, v, l, r, st);
}
void solution(int &n, vector < int > &a, vector < pair < int, int > > &sum)
{
sort(sum.begin() + 1, sum.end());
int res = 0;
for (int j = 1; j <= n; ++j)
{
res += (sum[j].first >= 0);
res += get(1, 1, n, 1, sum[j].second - 1, st);
update(1, 1, n, sum[j].second, 1, st);
}
cout << res;
}
main()
{
int n, p;
vector < int > a;
vector < pair < int, int > > sum;
enter(n, p, a, sum);
solution(n, a, sum);
return 0;
}
Bài toán 4: Dãy số hình nón
Đề bài
Cho một dãy số gồm phần tử . Một dãy con (không liên tiếp) của dãy A là một cách chọn ra phần tử có chỉ số bất kỳ trong dãy. Dãy con đó được gọi là dãy con hình nón độ dài nếu như tồn tại một vị trí sao cho:
Yêu cầu: Hãy tìm dãy con hình nón có tổng các chữ số của các số trong dãy là lớn nhất có thể?
Input:
- Dòng đầu tiên chứa số nguyên dương .
- Dòng thứ hai chứa số nguyên dương phân tách nhau bởi dấu cách.
Ràng buộc:
- .
- .
Subtasks:
- Subtask ( số điểm): .
- Subtask ( số điểm): .
- Subtask ( số điểm): Không có ràng buộc gì thêm.
Output:
- Số nguyên duy nhất là tổng chữ số của tất cả các số trong dãy con tìm được.
Sample Input:
10
1 10 11 12 8 3 9 6 7 3
Sample Output:
28
Giải thích:
Dãy số tìm được là: .
Ý tưởng
Subtask 1
Dùng quay lui hoặc duyệt bit để thử chọn tất cả các dãy con và kiểm tra tính chất hình nón của chúng, sau đó tính tổng các chữ số của dãy đó.
Subtask 2
Ý tưởng của subtask này được phát triển từ bài toán tìm dãy con tăng dài nhất cơ bản. Ứng với mỗi đặt \text{dp_left}[i] và \text{dp_right}[i] lần lượt là độ dài dãy con tăng có tổng chữ số lớn nhất kết thúc tại vị trí nhưng theo chiều từ tới và từ về . Gọi luôn là hàm tính tổng các chữ số của (cần phải khởi tạo trước), ta có công thức quy hoạch động:
\begin{cases}\text{dp_left}[i] = \text{max}\big(\text{dp_left}[j] + D(a_i)\big), \forall j: 1 \le j < i. \\ \text{dp_right}[i] = \text{max}\big(\text{dp_right}[j] + D(a_i)\big), \forall j: i < j \le N. \end{cases}Sử dụng hai vòng lặp với độ phức tạp ta xây dựng được hai mảng \text{dp_left, dp_right} dựa trên quy hoạch động. Để tìm kết quả, ta nhận xét như sau: Dãy hình nón có thể xảy ra ở một trong hai trường hợp:
- Dãy kết thúc tại chỉ tăng dần hoặc giảm dần (nghĩa là hoặc là phần tử cuối cùng, hoặc là phần tử đầu tiên). Trường hợp này kết quả bằng \text{max}(\text{dp_left}[i],\text{dp_right}[i]).
- Dãy hình nón có là phần tử ở đỉnh dãy số, hai bên giảm dần xuống. Trường hợp này kết quả bằng: \text{max}\big(\text{dp_left}[i] + \text{dp_right}[i] - D(a_i)\big), do được tính hai lần trong cả hai giá trị .
- Kết quả cuối cùng sẽ bằng giá trị lớn nhất trong cả hai cách chọn trên.
Subtask 3
Vẫn dựa vào ý tưởng trên, tuy nhiên vì nên việc cập nhật các bảng phương án dựa trên vị trí không thể thỏa mãn ràng buộc thời gian. Ta sẽ thay đổi một chút:
- \text{dp_left}[i]: Dãy con tăng có tổng chữ số lớn nhất kết thúc tại giá trị tính từ tới .
- \text{dp_right}[i]: Dãy con tăng có tổng chữ số lớn nhất kết thúc tại giá trị tính từ về .
Nhận thấy, các \text{dp_left}[i] sẽ được cập nhật từ những \text{dp_left}[j] có tương tự với các \text{dp_right}[i]. Ta xây dựng một Segment Tree để tính giá trị lớn nhất trong tất cả các \text{dp_left}[j], nhưng vị trí của các nodes trên cây sẽ kiểm soát những giá trị thay vì vị trí (các bạn xem lại phần bài toán cơ bản của cây phân đoạn tôi đã viết ở bài viết trước). Xây dựng hoàn toàn tương tự với mảng \text{dp_right}[i]. Để cây phân đoạn có thể kiểm soát được các giá trị trong dãy số ban đầu, ta cần sử dụng thêm kĩ thuật rời rạc hóa (giống như ở bài toán 1), để nén các số trong dãy về giá trị không vượt quá . Tới đây, ta có công thức quy hoạch động:
\begin{cases}\text{dp_left}[i] = \text{get_max}\big(ST[a_j] + D(a_i)\big), \forall a_j: 1 \le a_j < a_i. \\ \text{dp_right}[i] = \text{get_max}\big(ST[a_j] + D(a_i)\big), \forall a_j: 1 \le a_j < a_i. \end{cases}Để tính \text{dp_left}[i], ta duyệt từ trái qua phải, còn \text{dp_right}[i] thì duyệt từ phải qua trái. Mỗi khi tính xong một giá trị thì dùng thao tác update để cập nhật giá trị của lên node thứ trên Segment Tree. Còn cách lấy kết quả cuối cùng vẫn giống như ở subtask 2. Như vậy ta xây dựng được giải thuật .
Cài đặt
#include <bits/stdc++.h>
#define task "CONE."
#define int long long
using namespace std;
const int maxn = 1e5 + 10;
int N, digitize_value, st[4 * maxn];
pair < int, int > a[maxn], b[maxn], dp[maxn];
unordered_map < int, int > new_value;
int get_digit_sum(int N) // Calculate total of the digits of N.
{
int digit_sum = 0;
while (N)
digit_sum += (N % 10), N /= 10;
return digit_sum;
}
void enter()
{
cin >> N;
// Here let a[i].first is the value of a[i], and a[i].second is the total of digits of a[i].
for (int i = 1; i <= N; ++i)
{
cin >> a[i].first;
a[i].second = get_digit_sum(a[i].first);
b[i] = {a[i].first, i};
}
}
// Digitize the array such that every a[i] becomes less or equal to N.
void digitizing()
{
sort(b + 1, b + 1 + N);
for (int i = 1; i <= N; ++i)
a[b[i].second].first = (b[i].first != b[i - 1].first) ? ++digitize_value : digitize_value;
}
// Update a pos on interval tree, set it by value and build tree.
void update(int id, int u, int v, int pos, int val)
{
if (u > pos || v < pos)
return;
if (u == v)
{
st[id] = val;
return;
}
int mid = (u + v) >> 1;
update(id * 2, u, mid, pos, val);
update(id * 2 + 1, mid + 1, v, pos, val);
st[id] = max(st[id * 2], st[id * 2 + 1]);
}
// Use IT to get the maximum value on range [l, r] (dp[i] will be
// updated by dp[j] which have 1 <= a[j] < a[i]).
int get(int id, int u, int v, int l, int r)
{
if (u > r || v < l)
return 0;
if (u >= l && v <= r)
return st[id];
int mid = (u + v) >> 1;
return (max(get(id * 2, u, mid, l, r), get(id * 2 + 1, mid + 1, v, l, r)));
}
void solution()
{
for (int i = 1; i <= N; ++i)
{
dp[i].first = get(1, 1, digitize_value, 1, a[i].first - 1) + a[i].second;
update(1, 1, digitize_value, a[i].first, dp[i].first);
}
memset(st, 0, sizeof st);
for (int i = N; i >= 1; --i)
{
dp[i].second = get(1, 1, digitize_value, 1, a[i].first - 1) + a[i].second;
update(1, 1, digitize_value, a[i].first, dp[i].second);
}
// Find last result.
int res = 0;
for (int i = 1; i <= N; ++i)
res = max(res, max(dp[i].first, dp[i].second));
for (int i = 1; i <= N; ++i)
res = max(res, dp[i].first + dp[i].second - a[i].second);
cout << res;
}
main()
{
ios_base::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
enter();
digitizing();
solution();
return 0;
}
Tài liệu tham khảo
All rights reserved