Bài 26.2: Dãy con tăng dài nhất (LIS)
Bài 26.2: Dãy con tăng dài nhất (LIS)
Chào mừng các bạn quay trở lại với series blog về Cấu trúc dữ liệu và Giải thuật của FullhouseDev! Hôm nay, chúng ta sẽ cùng nhau "mổ xẻ" một bài toán cực kỳ kinh điển và quan trọng trong lập trình thi đấu cũng như khoa học máy tính: Bài toán tìm Dãy con tăng dài nhất hay còn gọi là Longest Increasing Subsequence (LIS).
Đây không chỉ là một bài toán lý thuyết mà còn có rất nhiều ứng dụng thực tế trong các lĩnh vực như phân tích dữ liệu, tin sinh học, hay thậm chí là tối ưu hóa trong sản xuất. Nắm vững cách giải bài toán LIS sẽ mở ra cho bạn những góc nhìn mới về cách tiếp cận các vấn đề liên quan đến dãy và chuỗi.
Dãy con tăng dài nhất là gì?
Trước hết, chúng ta cần hiểu rõ các khái niệm.
- Một dãy con (subsequence) của một dãy cho trước được tạo ra bằng cách xóa bỏ không hoặc nhiều phần tử khỏi dãy gốc mà không làm thay đổi thứ tự của các phần tử còn lại.
- Ví dụ: Dãy gốc là
[10, 9, 2, 5, 3, 7, 101, 18]. Các dãy con có thể là[10, 2, 7],[9, 5, 3, 1],[10, 9, 2, 5, 3, 7, 101, 18](chính nó), hoặc[](dãy rỗng).
- Ví dụ: Dãy gốc là
- Một dãy tăng (increasing sequence) là một dãy mà mỗi phần tử (trừ phần tử đầu tiên) đều lớn hơn phần tử đứng ngay trước nó.
- Ví dụ:
[2, 5, 7, 18, 101]là một dãy tăng.[10, 9, 2, 5]không phải là dãy tăng.
- Ví dụ:
- Dãy con tăng (increasing subsequence) là một dãy con của dãy gốc, đồng thời nó cũng là một dãy tăng.
- Ví dụ với dãy gốc
[10, 9, 2, 5, 3, 7, 101, 18]:[2, 5, 7, 18]là một dãy con tăng (được tạo từ các phần tử ở index 2, 3, 5, 7).[3, 7, 101]là một dãy con tăng (index 4, 5, 6).[2, 3, 7, 101]cũng là một dãy con tăng (index 2, 4, 5, 6).
- Ví dụ với dãy gốc
Bài toán Dãy con tăng dài nhất (LIS) yêu cầu chúng ta tìm một dãy con tăng có độ dài lớn nhất từ dãy ban đầu.
Với dãy ví dụ [10, 9, 2, 5, 3, 7, 101, 18], các dãy con tăng có thể là:
[2, 5, 7, 18] (độ dài 4)
[2, 3, 7, 18] (độ dài 4)
[2, 5, 7, 101] (độ dài 4)
[2, 3, 7, 101] (độ dài 4)
[2, 5, 101] (độ dài 3)
...
Và còn nhiều dãy khác. Dễ thấy, độ dài LIS cho dãy này là 4.
Làm thế nào để tìm được độ dài này một cách hiệu quả? Chúng ta hãy cùng xem xét các phương pháp.
Phương pháp 1: Quy hoạch động (Dynamic Programming) - Độ phức tạp O(N^2)
Đây là phương pháp phổ biến và dễ tiếp cận nhất để giải bài toán LIS. Ý tưởng chính là xây dựng một mảng DP, trong đó mỗi phần tử lưu trữ độ dài của dãy con tăng dài nhất kết thúc tại vị trí đó.
Gọi arr là dãy đầu vào có N phần tử. Chúng ta sẽ tạo một mảng dp cùng kích thước N.
dp[i]sẽ lưu trữ độ dài của dãy con tăng dài nhất kết thúc bằng phần tửarr[i].
Để tính dp[i], chúng ta cần xem xét tất cả các phần tử arr[j] đứng trước arr[i] (tức là j < i). Nếu arr[j] < arr[i], thì arr[i] có thể được thêm vào cuối một dãy con tăng kết thúc tại arr[j] để tạo thành một dãy con tăng mới kết thúc tại arr[i]. Độ dài của dãy mới này sẽ là dp[j] + 1.
Để dp[i] có giá trị lớn nhất, chúng ta cần tìm giá trị dp[j] lớn nhất trong số tất cả các j < i thỏa mãn arr[j] < arr[i].
Công thức truy hồi sẽ là:
dp[i] = 1 + max(dp[j]) với mọi 0 <= j < i sao cho arr[j] < arr[i].
Nếu không có j < i nào thỏa mãn arr[j] < arr[i] (ví dụ arr[i] là phần tử nhỏ nhất đứng sau nó), thì dãy con tăng dài nhất kết thúc tại arr[i] chỉ có mình nó, nên dp[i] = 1.
Giá trị khởi tạo: Mọi dp[i] ban đầu đều bằng 1 (vì mỗi phần tử arr[i] tự tạo thành một dãy con tăng độ dài 1).
Sau khi tính toán hết tất cả các giá trị dp[i] cho i từ 0 đến N-1, độ dài LIS của cả dãy gốc chính là giá trị lớn nhất trong mảng dp.
Hãy xem xét ví dụ arr = [10, 9, 2, 5, 3, 7, 101, 18]. N = 8.
dp[0](choarr[0] = 10): Không có phần tử nào trước nó.dp[0] = 1.dp[1](choarr[1] = 9):arr[0] = 10.10không nhỏ hơn9. Không cój < 1thỏa mãnarr[j] < arr[1].dp[1] = 1.dp[2](choarr[2] = 2):arr[0] = 10,arr[1] = 9. Cả hai đều không nhỏ hơn2.dp[2] = 1.dp[3](choarr[3] = 5):j = 0:arr[0] = 10.10không nhỏ hơn5.j = 1:arr[1] = 9.9không nhỏ hơn5.j = 2:arr[2] = 2.2nhỏ hơn5.dp[2] = 1. Có thể tạo dãy con tăng kết thúc bằng 5 có độ dàidp[2] + 1 = 1 + 1 = 2(ví dụ:[2, 5]).- Max
dp[j]choj < 3vàarr[j] < arr[3]làdp[2] = 1. dp[3] = 1 + dp[2] = 1 + 1 = 2.
dp[4](choarr[4] = 3):j = 0:arr[0] = 10(không).j = 1:arr[1] = 9(không).j = 2:arr[2] = 2.2 < 3.dp[2] = 1. Có thể tạo[2, 3], độ dàidp[2]+1 = 2.j = 3:arr[3] = 5(không).- Max
dp[j]choj < 4vàarr[j] < arr[4]làdp[2] = 1. dp[4] = 1 + dp[2] = 1 + 1 = 2.
dp[5](choarr[5] = 7):j = 0:arr[0] = 10(không).j = 1:arr[1] = 9(không).j = 2:arr[2] = 2.2 < 7.dp[2] = 1. Dãy:[2, 7].j = 3:arr[3] = 5.5 < 7.dp[3] = 2. Dãy:[..., 5, 7]. Lớn nhất kết thúc tại 5 là[2, 5]. Ghép 7 ta có[2, 5, 7].j = 4:arr[4] = 3.3 < 7.dp[4] = 2. Dãy:[..., 3, 7]. Lớn nhất kết thúc tại 3 là[2, 3]. Ghép 7 ta có[2, 3, 7].- Max
dp[j]choj < 5vàarr[j] < arr[5]làmax(dp[2], dp[3], dp[4]) = max(1, 2, 2) = 2. dp[5] = 1 + max(dp[j]...) = 1 + 2 = 3.
dp[6](choarr[6] = 101):- Xem xét tất cả
j < 6màarr[j] < 101. Tất cả đều nhỏ hơn 101. - Max
dp[j]choj = 0..5làmax(1, 1, 1, 2, 2, 3) = 3. dp[6] = 1 + 3 = 4. (Ví dụ dãy:[2, 3, 7, 101])
- Xem xét tất cả
dp[7](choarr[7] = 18):- Xem xét tất cả
j < 7màarr[j] < 18. arr[0]=10(dp[0]=1),arr[1]=9(dp[1]=1),arr[2]=2(dp[2]=1),arr[3]=5(dp[3]=2),arr[4]=3(dp[4]=2),arr[5]=7(dp[5]=3).arr[6]=101(không nhỏ hơn 18).- Max
dp[j]cho cácjthỏa mãn làmax(dp[0], dp[1], dp[2], dp[3], dp[4], dp[5]) = max(1, 1, 1, 2, 2, 3) = 3. dp[7] = 1 + 3 = 4. (Ví dụ dãy:[2, 3, 7, 18])
- Xem xét tất cả
Mảng dp cuối cùng sẽ là [1, 1, 1, 2, 2, 3, 4, 4].
Giá trị lớn nhất trong mảng dp là 4. Vậy độ dài LIS là 4.
C++ Code cho Phương pháp O(N^2)
#include <iostream>
#include <vector>
#include <algorithm>
int lengthOfLIS_O_N_Squared(const vector<int>& nums) {
if (nums.empty()) {
return 0;
}
int n = nums.size();
vector<int> dp(n, 1); // Khởi tạo mỗi dp[i] = 1 (LIS kết thúc tại i có ít nhất 1 phần tử là chính nó)
int max_lis_length = 1; // Độ dài LIS tối thiểu là 1 (nếu dãy không rỗng)
// Duyệt qua từng phần tử của dãy
for (int i = 1; i < n; ++i) {
// Với mỗi phần tử nums[i], duyệt qua TẤT CẢ các phần tử đứng trước nó
for (int j = 0; j < i; ++j) {
// Nếu nums[j] nhỏ hơn nums[i]
if (nums[j] < nums[i]) {
// Có thể mở rộng LIS kết thúc tại j bằng cách thêm nums[i] vào
// Cập nhật dp[i] nếu tìm thấy dãy con tăng dài hơn
dp[i] = max(dp[i], dp[j] + 1);
}
}
// Sau khi xem xét tất cả các j < i, dp[i] đã chứa độ dài LIS kết thúc tại i
// Cập nhật độ dài LIS tổng thể nếu dp[i] lớn hơn max_lis_length hiện tại
max_lis_length = max(max_lis_length, dp[i]);
}
return max_lis_length;
}
int main() {
vector<int> arr1 = {10, 9, 2, 5, 3, 7, 101, 18};
cout << "LIS length for [10, 9, 2, 5, 3, 7, 101, 18] (O(N^2)): "
<< lengthOfLIS_O_N_Squared(arr1) << endl; // Output: 4
vector<int> arr2 = {0, 1, 0, 3, 2, 3};
cout << "LIS length for [0, 1, 0, 3, 2, 3] (O(N^2)): "
<< lengthOfLIS_O_N_Squared(arr2) << endl; // Output: 4 ([0, 1, 2, 3] hoặc [0, 1, 3])
vector<int> arr3 = {7, 7, 7, 7, 7, 7, 7};
cout << "LIS length for [7, 7, 7, 7, 7, 7, 7] (O(N^2)): "
<< lengthOfLIS_O_N_Squared(arr3) << endl; // Output: 1 ([7])
vector<int> arr4 = {1, 2, 3, 4, 5};
cout << "LIS length for [1, 2, 3, 4, 5] (O(N^2)): "
<< lengthOfLIS_O_N_Squared(arr4) << endl; // Output: 5
vector<int> arr5 = {5, 4, 3, 2, 1};
cout << "LIS length for [5, 4, 3, 2, 1] (O(N^2)): "
<< lengthOfLIS_O_N_Squared(arr5) << endl; // Output: 1
return 0;
}
Giải thích Code O(N^2)
- Hàm
lengthOfLIS_O_N_Squarednhận một vectornumslàm đầu vào. - Chúng ta xử lý trường hợp dãy rỗng.
- Khởi tạo vector
dpcùng kích thước vớinums, tất cả giá trị ban đầu đều là1. Điều này là đúng vì mỗi phần tử riêng lẻ tạo thành một dãy con tăng có độ dài 1. - Biến
max_lis_lengthdùng để theo dõi độ dài LIS dài nhất tìm được cho đến hiện tại. Ban đầu nó là 1. - Vòng lặp ngoài chạy từ
i = 1đếnn-1. Vòng lặp này xem xét từng phần tửnums[i]để tínhdp[i]. - Vòng lặp trong chạy từ
j = 0đếni-1. Vòng lặp này duyệt qua tất cả các phần tửnums[j]đứng trướcnums[i]. - Bên trong vòng lặp trong, chúng ta kiểm tra điều kiện
nums[j] < nums[i]. Nếu điều kiện này đúng, có nghĩa lànums[i]có thể nối vào cuối một dãy con tăng kết thúc tạinums[j]. Độ dài của dãy mới này sẽ làdp[j] + 1. - Chúng ta cập nhật
dp[i]bằng giá trị lớn nhất giữadp[i]hiện tại vàdp[j] + 1. Việc này đảm bảo rằngdp[i]luôn lưu trữ độ dài của dãy con tăng dài nhất kết thúc tạinums[i]mà có phần tử đứng ngay trướcnums[i]lànums[j]. Bằng cách xét tất cả cácj < i,dp[i]sẽ tìm được độ dài tối ưu dựa trên các phần tử đứng trước nó. - Sau khi vòng lặp trong kết thúc (đã xét tất cả
j < i),dp[i]đã được tính chính xác. Chúng ta cập nhậtmax_lis_lengthnếudp[i]lớn hơn giá trị hiện tại củamax_lis_length. - Cuối cùng, hàm trả về
max_lis_length.
Độ phức tạp thời gian của phương pháp này là O(N^2) vì có hai vòng lặp lồng nhau, mỗi vòng lặp chạy tới N lần. Độ phức tạp không gian là O(N) để lưu trữ mảng dp. Với N lên đến vài nghìn, O(N^2) có thể chậm. Chúng ta có thể làm tốt hơn.
Phương pháp 2: Tối ưu hóa bằng Quy hoạch động kết hợp Tìm kiếm nhị phân - Độ phức tạp O(N log N)
Đây là phương pháp hiệu quả hơn và thường được sử dụng trong các bài toán có giới hạn N lớn (ví dụ: N = 10^5). Ý tưởng ở đây phức tạp hơn một chút, nhưng mang lại cải thiện đáng kể về hiệu suất.
Thay vì lưu trữ độ dài LIS kết thúc tại mỗi vị trí i, chúng ta sẽ duy trì một mảng (hay vector) gọi là tail. Mảng tail này có một tính chất đặc biệt:
tail[k]sẽ lưu trữ giá trị nhỏ nhất mà một dãy con tăng có độ dàik+1có thể kết thúc.
Chúng ta sẽ duyệt qua các phần tử của dãy đầu vào arr một lần. Khi xử lý phần tử arr[i]:
- Nếu
arr[i]lớn hơn phần tử cuối cùng của mảngtail, điều đó có nghĩa là chúng ta đã tìm thấy một phần tử có thể mở rộng dãy con tăng dài nhất hiện tại. Ta thêmarr[i]vào cuối mảngtail. Độ dài LIS tăng lên 1. - Nếu
arr[i]không lớn hơn phần tử cuối cùng của mảngtail, điều đó có nghĩa làarr[i]không thể trực tiếp mở rộng LIS dài nhất hiện tại. Tuy nhiên, nó có thể giúp tạo ra một dãy con tăng có cùng độ dài với một dãy con tăng nào đó đã tìm thấy trước đó, nhưng kết thúc bằng một giá trị nhỏ hơn (arr[i]thay vì giá trị cũ). Việc thay thế này là quan trọng vì nó giữ cho mảngtailcó các giá trị cuối nhỏ nhất có thể cho mỗi độ dài, từ đó tạo cơ hội cho các phần tử sau này mở rộng dãy con tăng.- Trong trường hợp này, chúng ta cần tìm vị trí trong mảng
tailmàarr[i]có thể thay thế. Vị trí đó chính là phần tử nhỏ nhất trongtailmà lớn hơn hoặc bằngarr[i]. Vì mảngtailluôn được giữ có thứ tự tăng dần (tại sao? Nếutail[k]là giá trị nhỏ nhất kết thúc IS độ dàik+1vàtail[k+1]là giá trị nhỏ nhất kết thúc IS độ dàik+2, thì chắc chắntail[k] < tail[k+1]vì một IS độ dàik+2có thể được tạo ra bằng cách thêm một phần tử vào IS độ dàik+1. Phần tử được thêm vào phải lớn hơn phần tử cuối của IS độ dàik+1. Do đó, giá trị cuối nhỏ nhất của IS độ dàik+2phải lớn hơn giá trị cuối nhỏ nhất của IS độ dàik+1), chúng ta có thể sử dụng tìm kiếm nhị phân để tìm vị trí này một cách hiệu quả (O(log N)). Sau khi tìm thấy vị trí, ta thay thế phần tử tại vị trí đó bằngarr[i].
- Trong trường hợp này, chúng ta cần tìm vị trí trong mảng
Độ dài LIS chính là kích thước cuối cùng của mảng tail.
Lưu ý quan trọng: Mảng tail không phải là một dãy con tăng thực tế của dãy gốc, mà nó chỉ là một công cụ để theo dõi các giá trị kết thúc nhỏ nhất cho các LIS có độ dài khác nhau.
Hãy theo dõi ví dụ arr = [10, 9, 2, 5, 3, 7, 101, 18] với phương pháp O(N log N).
Khởi tạo: tail là rỗng. Độ dài LIS L = 0.
arr[0] = 10:tailrỗng. Thêm10vàotail.tail = [10],L = 1.arr[1] = 9:9không lớn hơn phần tử cuối (10). Tìm kiếm nhị phân trongtailđể tìm phần tử nhỏ nhất >= 9. Đó là10ở index 0. Thay thế10bằng9.tail = [9],L = 1. (LIS độ dài 1 kết thúc bằng 9 là[9], nhỏ hơn[10]nên tốt hơn)arr[2] = 2:2không lớn hơn phần tử cuối (9). Tìm kiếm nhị phân trongtailđể tìm phần tử nhỏ nhất >= 2. Đó là9ở index 0. Thay thế9bằng2.tail = [2],L = 1. (LIS độ dài 1 kết thúc bằng 2 là[2])arr[3] = 5:5lớn hơn phần tử cuối (2). Thêm5vàotail.tail = [2, 5],L = 2. (LIS độ dài 2 kết thúc bằng 5 có thể là[2, 5])arr[4] = 3:3không lớn hơn phần tử cuối (5). Tìm kiếm nhị phân trongtailđể tìm phần tử nhỏ nhất >= 3. Đó là5ở index 1. Thay thế5bằng3.tail = [2, 3],L = 2. (LIS độ dài 2 kết thúc bằng 3 có thể là[2, 3]. Dãy con tăng dài nhất kết thúc bằng giá trị nhỏ hơn tốt hơn)arr[5] = 7:7lớn hơn phần tử cuối (3). Thêm7vàotail.tail = [2, 3, 7],L = 3. (LIS độ dài 3 kết thúc bằng 7 có thể là[2, 3, 7])arr[6] = 101:101lớn hơn phần tử cuối (7). Thêm101vàotail.tail = [2, 3, 7, 101],L = 4. (LIS độ dài 4 kết thúc bằng 101 có thể là[2, 3, 7, 101])arr[7] = 18:18không lớn hơn phần tử cuối (101). Tìm kiếm nhị phân trongtailđể tìm phần tử nhỏ nhất >= 18. Đó là101ở index 3. Thay thế101bằng18.tail = [2, 3, 7, 18],L = 4$. (LIS độ dài 4 kết thúc bằng 18 có thể là[2, 3, 7, 18]`. Vẫn giữ độ dài 4, nhưng kết thúc bằng giá trị nhỏ hơn.)
Kết thúc duyệt, kích thước của tail là 4. Vậy độ dài LIS là 4.
C++ Code cho Phương pháp O(N log N)
Trong C++, chúng ta có thể sử dụng vector cho mảng tail và lower_bound để thực hiện tìm kiếm nhị phân. lower_bound(first, last, val) trả về iterator trỏ đến phần tử đầu tiên trong phạm vi [first, last) mà không nhỏ hơn val.
#include <iostream>
#include <vector>
#include <algorithm>
int lengthOfLIS_O_N_Log_N(const vector<int>& nums) {
if (nums.empty()) {
return 0;
}
// tail[k] lưu trữ giá trị nhỏ nhất kết thúc một LIS có độ dài k+1
vector<int> tail;
// Duyệt qua từng phần tử trong dãy đầu vào
for (int num : nums) {
// Tìm kiếm vị trí của num trong mảng tail
// lower_bound tìm phần tử đầu tiên >= num
auto it = lower_bound(tail.begin(), tail.end(), num);
// Case 1: num lớn hơn tất cả các phần tử trong tail
// Điều này có nghĩa là num có thể mở rộng LIS dài nhất hiện tại
if (it == tail.end()) {
tail.push_back(num); // Thêm num vào cuối tail
}
// Case 2: num không lớn hơn tất cả các phần tử trong tail
// Điều này có nghĩa là num có thể thay thế một phần tử trong tail
// để tạo ra một LIS có cùng độ dài nhưng kết thúc bằng giá trị nhỏ hơn
else {
*it = num; // Thay thế phần tử tại vị trí it bằng num
}
// Kích thước của tail luôn là độ dài LIS tìm được cho đến hiện tại
}
// Độ dài LIS cuối cùng chính là kích thước của mảng tail
return tail.size();
}
int main() {
vector<int> arr1 = {10, 9, 2, 5, 3, 7, 101, 18};
cout << "LIS length for [10, 9, 2, 5, 3, 7, 101, 18] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr1) << endl; // Output: 4
vector<int> arr2 = {0, 1, 0, 3, 2, 3};
cout << "LIS length for [0, 1, 0, 3, 2, 3] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr2) << endl; // Output: 4
vector<int> arr3 = {7, 7, 7, 7, 7, 7, 7};
cout << "LIS length for [7, 7, 7, 7, 7, 7, 7] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr3) << endl; // Output: 1
vector<int> arr4 = {1, 2, 3, 4, 5};
cout << "LIS length for [1, 2, 3, 4, 5] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr4) << endl; // Output: 5
vector<int> arr5 = {5, 4, 3, 2, 1};
cout << "LIS length for [5, 4, 3, 2, 1] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr5) << endl; // Output: 1
vector<int> arr6 = {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15};
cout << "LIS length for [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15] (O(N log N)): "
<< lengthOfLIS_O_N_Log_N(arr6) << endl; // Output: 6 ([0, 2, 6, 9, 11, 15] hoặc [0, 4, 6, 9, 11, 15] etc.)
return 0;
}
Giải thích Code O(N log N)
- Hàm
lengthOfLIS_O_N_Log_Nnhận một vectornums. - Khởi tạo một vector
tailrỗng. Đây là vector sẽ lưu trữ các giá trị kết thúc nhỏ nhất cho các LIS có độ dài khác nhau. - Vòng lặp
for (int num : nums)duyệt qua từng phần tửnumtrong dãy đầu vào. - Đối với mỗi
num, chúng ta sử dụnglower_bound(tail.begin(), tail.end(), num)để tìm vị tríit.lower_boundtìm kiếm trongtail(đảm bảotailluôn tăng dần) và trả về iterator trỏ đến phần tử đầu tiên không nhỏ hơnnum.- Nếu
it == tail.end(), điều này có nghĩa lànumlớn hơn tất cả các phần tử hiện có trongtail. Vìtail[k]là giá trị nhỏ nhất kết thúc LIS độ dàik+1, nếunumlớn hơn phần tử cuối cùng củatail(là giá trị kết thúc nhỏ nhất của LIS dài nhất hiện tại), thìnumcó thể mở rộng LIS dài nhất thêm 1 phần tử. Ta thêmnumvào cuốitailbằngtail.push_back(num);. - Nếu
itkhông phải làtail.end(), điều này có nghĩa là chúng ta tìm thấy một phần tử*ittrongtailmà*it >= num. Ta thay thế phần tử tại vị tríitbằngnum(*it = num;). Việc này không làm tăng độ dài LIS tìm thấy cho đến nay (kích thướctailkhông đổi), nhưng nó giúp chúng ta có một dãy con tăng có cùng độ dài nhưng kết thúc bằng giá trịnumnhỏ hơn. Điều này rất quan trọng vì nó có thể cho phép các phần tử trong tương lai (lớn hơnnum) mở rộng LIS từ vị trí này, điều mà chúng không thể làm được nếu giá trị kết thúc vẫn là*it(lớn hơn). Vị tríitchính xác là vị trí mà một LIS kết thúc bằngnumsẽ có cùng độ dài với LIS kết thúc bằng*ittrước đó.
- Nếu
- Sau khi duyệt qua tất cả các phần tử, kích thước cuối cùng của vector
tailchính là độ dài của dãy con tăng dài nhất.
Độ phức tạp thời gian của phương pháp này là O(N log N) vì chúng ta duyệt qua N phần tử, và với mỗi phần tử, chúng ta thực hiện một phép tìm kiếm nhị phân trong tail có kích thước tối đa là N, mất O(log N). Độ phức tạp không gian là O(N) để lưu trữ vector tail.
Bài tập ví dụ:
Frog SPOJ.
Một con ếch có thể nhảy 1, 2, 3 bước để có thể lên đến một đỉnh cần đến. Hãy đếm số các cách con ếch có thể nhảy đến đỉnh.
Input Format
Số nguyên dương N mô tả số bước con ếch cần di chuyển để nhảy tới đỉnh.(1<=N<=40)Vì đáp án sẽ rất lớn nên bạn hãy mode với 1e9+7.
Constraints
.
Output Format
In ra kết quả của bài toán
Ví dụ:
Dữ liệu vào
7
Dữ liệu ra
44
Chào bạn,
Đây là một bài toán điển hình có thể giải bằng Quy hoạch động (Dynamic Programming) hoặc đệ quy có nhớ (Memoization).
Hướng dẫn giải ngắn gọn:
Xác định trạng thái: Gọi
dp[i]là số cách để con ếch nhảy đến được bước thứi. Mục tiêu của chúng ta là tìmdp[N].Tìm công thức truy hồi: Để đến được bước thứ
i, con ếch có thể nhảy từ các bước trước đó bằng một trong ba cách:- Nhảy 1 bước từ bước
i-1. - Nhảy 2 bước từ bước
i-2. - Nhảy 3 bước từ bước
i-3.
Vì vậy, số cách để đến bước
ichính là tổng số cách để đến bướci-1,i-2, vài-3. Công thức truy hồi là:dp[i] = dp[i-1] + dp[i-2] + dp[i-3](áp dụng khii >= 3).- Nhảy 1 bước từ bước
Xác định các trường hợp cơ sở (Base Cases):
dp[0]: Số cách để ở tại bước 0 (vị trí bắt đầu). Có 1 cách (không làm gì cả).dp[0] = 1.dp[1]: Số cách để đến bước 1. Chỉ có 1 cách: nhảy 1 bước từ 0.dp[1] = 1.dp[2]: Số cách để đến bước 2. Có 2 cách: nhảy (1, 1) hoặc nhảy (2).dp[2] = 2.
Xây dựng lời giải:
- Sử dụng một mảng (hoặc vector)
dpcó kích thướcN+1để lưu trữ kết quả cho từng bước. - Khởi tạo các trường hợp cơ sở:
dp[0],dp[1],dp[2](lưu ý xử lý các trường hợpN=1,N=2sao cho mảng không bị truy cập ngoài phạm vi). - Lặp từ
i = 3đếnN, tính giá trịdp[i]dựa trên công thức truy hồi:dp[i] = (dp[i-1] + dp[i-2] + dp[i-3]) % MOD, trong đóMOD = 1e9 + 7. Cần thực hiện phép modulo sau mỗi lần cộng để tránh tràn số.
- Sử dụng một mảng (hoặc vector)
Kết quả: Kết quả cuối cùng là
dp[N].
Comments