--- title: Standard Template Library (STL) (Phần 2: Sequential containers) --- Standard Template Library (STL) (Phần 2: Sequential containers) === --- ###### ✍️ Author: 2School Guideline ###### 📋 Content: [TOC] --- # Giới thiệu chung Tiếp tục chuỗi 4 bài viết về STL (Standard Template Library), trong phần 2 này chúng mình sẽ giới thiệu về **sequential containers** - các cấu trúc dữ liệu (CTDL) sử dụng **chỉ số (index)** *và* **con trỏ (iterator)** để truy cập các phần tử chứa trong chúng. Trong bài viết này, chúng mình sẽ giới thiệu về 2 CTDL dạng sequential container là **vector** và **deque**. Ngoài ra còn có thêm các CTDL dạng **sequential containers** khác là **array**, **list** và **forward list** *(từ C++11 trở đi)*. *Tuy nhiên chúng không được ứng dụng rộng rãi trong kì thi tuyển sinh chuyên tin 10 nên mọi người có thể tự tìm hiểu thêm nếu thích ^^.* # Iterator (con trỏ) ## 1. Khái niệm **Iterator** là một đối tượng có thể đi qua (hay trỏ vào) các phần tử nằm trong một CTDL dạng **container** (như vector, deque,...) mà không cần biết trật tự bên trong. Iterator cũng có thể truy cập giá trị của phần tử mà nó đang trỏ vào. ## 2. Các cú pháp, toán tử và hàm thông dụng ### a. Cách khai báo **Cú pháp:** `container <type>::iterator iterator_name` - `container`: loại CTDL dạng container (như vector, deque) mà bạn muốn iterator này trỏ vào. - `type`: kiểu dữ liệu của iterator trùng với kiểu dữ liệu của CTDL dạng container. - `iterator_name`: tên của iterator. **Ví dụ:** ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector <int>::iterator it1; deque <char>::iterator it2; return 0; } ``` Iterator `it1` có chức năng trỏ vào phần tử của một `vector <int>` bất kì, iterator `it2` có chức năng trỏ vào phần tử của một `deque <char>` bất kì. ### b. Các toán tử - `it` - trả về giá trị mà iterator `it` đang trỏ vào. - `it++` hay `++it` - di chuyển iterator `it` đến phần tử tiếp theo trong CTDL dạng container. - `it--` hay `--it` - di chuyển iterator `it`đến phần tử trước đó trong CTDL dạng container. - `==` và `!=` - so sánh vị trí 2 phần tử được trỏ vào bởi 2 iterator. - `=` - gán vị trí trỏ vào cho iterator. ### c. Các hàm - `container_name.begin()` - trả về iterator trỏ đến phần tử **đầu tiên** trong CTDL dạng container có tên `container_name`. - `container_name.end()` - trả về iterator trỏ đến phần tử **ngay sau** phần tử **cuối cùng** trong CTDL dạng container có tên `container_name`. - `container_name.rbegin()` - trả về iterator trỏ đến phần tử **cuối cùng** trong CTDL dạng container có tên `container_name`. - `container_name.rend()` - trả về iterator trỏ đến phần tử **ngay trước** phần tử **đầu tiên** trong CTDL dạng container có tên `container_name`. + Ví dụ: ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector <int> v = {1, 2, 3, 4}; cout << *v.begin() << "\n"; //output ra 1 cout << *v.end() << "\n"; //output ra 0 cout << *v.rbegin() << "\n"; //output ra 4 cout << *v.rend() << "\n"; //output ra 0 return 0; } ``` + Từ ví dụ trên ta có thể thấy `*v.end()` và `*v.rend()` trả về giá trị `0` vì `v.end()` và `v.rend()` trỏ vào các phần tử không nằm trong `v`. - `advance(it, x)` - di chuyển iterator `it` đến phần tử tiếp theo `x` lần (nếu `x` dương) hoặc đến phần tử trước đó `x` lần (nếu `x` âm) trong CTDL dạng container. - `next(it)` - di chuyển iterator `it` đến phần tử tiếp theo trong CTDL dạng container. - `prev(it)` - di chuyển iterator `it`đến phần tử trước đó trong CTDL dạng container. # Cách duyệt qua một CTDL dạng container bằng vòng lặp `for` ## 1. Sử dụng chỉ số (index) *(chỉ sử dụng cho CTDL dạng sequential container)* **Cú pháp:** ```cpp! for (int index_name = 0; index_name < container_name.size(); index_name++) ``` - `container_name` - tên của CTDL dạng container. - `index_name` - chỉ số của các phần tử trong `container_name`. - `container_name[index_name]` - giá trị của phần tử tại chỉ số `index_name` trong `container_name`. **Ví dụ:** ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector <int> v = {2, 3, 5, 7, 11, 13}; for (int i = 0; i < v.size(); i++) { // logic code } } ``` ## 2. Sử dụng vòng lặp for-each *(chỉ sử dụng từ C++ 11 trở lên)* **Cú pháp:** ```cpp! for (type element_name : container_name) ``` - `container_name` - tên của CTDL dạng container. - `type` - kiểu dữ liệu của phần tử trong `container_name`. - `element_name` - giá trị của phần tử trong `container_name`. **Ví dụ:** ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector <int> v = {2, 3, 5, 7, 11, 13}; for (int i : v) { // logic code } } ``` ## 3. Sử dụng iterator (con trỏ) **Cú pháp:** ```cpp! for (container <type>::iterator iterator_name = container_name.begin(); iterator_name != container_name.end(); iterator_name++) ``` - `container_name` - tên của CTDL dạng container. - `iterator_name` - con trỏ trỏ vào phần tử của `container_name`. - `*iterator_name`: giá trị của phần tử trong `container_name`. **Ví dụ:** ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector <int> v = {2, 3, 5, 7, 11, 13}; for (vector <int>::iterator it = v.begin(); it != v.end(); it++) { // logic code } } ``` # Vector (mảng động) ## 1. Cấu trúc và cơ chế hoạt động Như chúng mình đã giới thiệu trong bài viết *[Những kiến thức cơ bản (Phần 2)](/j6sPIVN8RrWbRb4HXVpPjQ)*, **vector** hoạt động tương tự như một mảng thông thường nhưng không cần khai báo trước kích thước và nó có thể **thêm vào** hay **xoá đi** phần tử ở **vị trí cuối**. Ngoài cách truy cập các phần tử chứa trong vector bằng **chỉ số (index)**, ta còn có thể truy cập bằng **con trỏ (iterator)**. ## 2. Các cú pháp và hàm thông dụng - `vector <int> v` - khai báo vector `v` với kiểu dữ liệu int, tức là các phần tử trong vector `v` có kiểu dữ liệu là int. - `v[x]` - truy cập phần tử ở **vị trí `x`** của vector `v`. ### a. Các hàm có độ phức tạp O(1) - `v.push_back(x)` - **thêm** phần tử `x` vào **vị trí cuối** của vector `v`. - `v.back()` - trả về **giá trị** của phần tử ở **vị trí cuối** của vector `v`. - `v.front()` - trả về **giá trị** của phần tử ở **vị trí đầu** của vector `v`. - `v.pop_back()` - **xoá đi** phần tử nằm ở **vị trí cuối** của vector `v`. - `v.empty()` - trả về **true** nếu vector `v` **rỗng** (không có phần tử nào) và **false** nếu vector `v` có **ít nhất 1 phần tử**. - `v.size()` - trả về **kích thước** (số lượng phần tử) của vector `v`. ### b. Các hàm có độ phức tạp O(logN) *($N$ là kích thước của vector `v`)* - `lower_bound(v.begin(), v.end(), x)` + Trả về **iterator** trỏ vào phần tử có giá trị **nhỏ nhất** mà **$\ge$** `x` trong vector `v` (nếu vector `v` được sắp xếp **tăng dần**). + Trả về **iterator** trỏ vào phần tử có giá trị **lớn nhất** mà **$\le$** `x` trong vector `v` (nếu vector `v` được sắp xếp **giảm dần**). - `upper_bound(v.begin(), v.end(), x)` + Trả về **iterator** trỏ vào phần tử có giá trị **nhỏ nhất** mà **$>$** `x` trong vector `v` (nếu vector `v` được sắp xếp **tăng dần**). + Trả về **iterator** trỏ vào phần tử có giá trị **lớn nhất** mà **$<$** `x` trong vector `v` (nếu vector `v` được sắp xếp **giảm dần**). ### c. Các hàm có độ phức tạp O(N) *($N$ là kích thước của vector `v`)* - `v.clear()` - **xoá toàn bộ** phần tử của vector `v`. ### d. Các hàm có độ phức tạp O(N * logN) *($N$ là kích thước của vector `v`)* - `sort(v.begin(), v.end())` - **sắp xếp** các phần tử trong vector `v` theo thứ tự **tăng dần**. - `sort(v.begin(), v.end(), greater <int>())` - **sắp xếp** các phần tử trong vector `v` theo thứ tự **giảm dần**. ## 3. Một số lưu ý khi sử dụng vector - Khi vector đang **rỗng** mà ta **truy cập** vào phần tử ở vị trí đầu hoặc cuối của nó thì code sẽ bị **lỗi**. Vì vậy để đảm bảo code chạy đúng, trước khi truy cập phần tử ở vị trí đầu hoặc cuối của vector ta cần kiểm tra xem nó có rỗng không. - Khi vector chỉ chứa **1** phần tử duy nhất thì `vector.front()` và `vector.back()` trả về giá trị của **cùng 1** phần tử. # Deque (hàng đợi hai đầu) ## 1. Cấu trúc và cơ chế hoạt động **Deque** là viết tắt của **d**ouble-**e**nded **que**ue. Mới nghe qua cái tên thì deque có vẻ giống một phiên bản dung hợp giữa **stack** và **queue** *(đọc về stack và queue tại [Standard Template Library (STL) (Phần 1: Container adapters)](/RLY9TS8CTGOsj3734L4z4A))* vì nó có khả năng **lấy giá trị**, **thêm vào** và **xoá đi** phần tử ở **vị trí đầu** lẫn **vị trí cuối**. ![Deque](https://hackmd.io/_uploads/HJ0MnlIEa.png) *(Ảnh gốc: GeeksforGeeks)* Tuy nhiên, ta còn có thể truy cập các phần tử trong deque bằng **chỉ số (index)** hay **con trỏ (iterator)**, thậm chí là **sắp xếp** deque và **tìm kiếm nhị phân** trên nó. Chính vì vậy, deque giống với một **vector hai đầu** hơn. ## 2. Các cú pháp và hàm thông dụng - `deque <int> dq` - khai báo deque `dq` với kiểu dữ liệu int, tức là các phần tử trong deque `dq` có kiểu dữ liệu là int. - `dq[x]` - truy cập phần tử ở **vị trí `x`** của deque `dq`. ### a. Các hàm có độ phức tạp O(1) - `dq.push_back(x)` - **thêm** phần tử `x` vào **vị trí cuối** của deque `dq`. - `dq.push_front(x)` - **thêm** phần tử `x` vào **vị trí đầu** của deque `dq`. - `dq.back()` - trả về **giá trị** của phần tử ở **vị trí cuối** của deque `dq`. - `dq.front()` - trả về **giá trị** của phần tử ở **vị trí đầu** của deque `dq`. - `dq.pop_back()` - **xoá đi* phần tử nằm ở **vị trí cuối** của deque `dq`. - `dq.pop_front()` - **xoá đi** phần tử nằm ở **vị trí đầu** của deque `dq`. - `dq.empty()` - trả về **true** nếu deque `dq` **rỗng** (không có phần tử nào) và **false** nếu deque `dq` có **ít nhất 1 phần tử**. - `dq.size()` - trả về **kích thước** (số lượng phần tử) của deque `dq`. ### b. Các hàm có độ phức tạp O(logN) *($N$ là kích thước của deque `dq`)* - `lower_bound(dq.begin(), dq.end(), x)` + Trả về **iterator** trỏ vào phần tử có giá trị **nhỏ nhất** mà **$\ge$** `x` trong deque `dq` (nếu deque `dq` được sắp xếp **tăng dần**). + Trả về **iterator** trỏ vào phần tử có giá trị **lớn nhất** mà **$\le$** `x` trong deque `dq` (nếu deque `dq` được sắp xếp **giảm dần**). - `upper_bound(dq.begin(), dq.end(), x)` + Trả về **iterator** trỏ vào phần tử có giá trị **nhỏ nhất** mà **$>$** `x` trong deque `dq` (nếu deque `dq` được sắp xếp **tăng dần**). + Trả về **iterator** trỏ vào phần tử có giá trị **lớn nhất** mà **$<$** `x` trong deque `dq` (nếu deque `dq` được sắp xếp **giảm dần**). ### c. Các hàm có độ phức tạp O(N) *($N$ là kích thước của deque `dq`)* - `dq.clear()` - **xoá toàn bộ** phần tử của deque `dq`. ### d. Các hàm có độ phức tạp O(N * logN) *($N$ là kích thước của deque `dq`)* - `sort(dq.begin(), dq.end())` - **sắp xếp** các phần tử trong deque `dq` theo thứ tự **tăng dần**. - `sort(dq.begin(), dq.end(), greater <int>())` - **sắp xếp** các phần tử trong deque `dq` theo thứ tự **giảm dần**. ## 3. Một số lưu ý khi sử dụng deque - Khi deque đang **rỗng** mà ta **truy cập** vào phần tử ở vị trí đầu hoặc cuối của nó thì code sẽ bị **lỗi.** Vì vậy để đảm bảo code chạy đúng, trước khi truy cập phần tử ở vị trí đầu của deque ta cần kiểm tra xem nó có rỗng không. - Khi deque chỉ chứa **1** phần tử duy nhất thì các thao tác với **vị trí đầu** và **vị tri cuối** của deque đều thao tác với cùng **1** phần tử. ## 4. Bài tập ví dụ ### Đề bài (Chủ đề: Tìm max min trên đoạn tịnh tiến) Cho một dãy $A$ gồm $N$ phần tử được đánh số từ $1$ đến $N$. Phần tử thứ $i$ có giá trị là $A[i]$. Cho $k$ là một số nguyên dương $(k \le N)$. Với mỗi phần tử $i$ $(k \le i \le N)$, tìm giá trị lớn nhất của các phần tử trong đoạn từ $i–k+1$ đến $i$ trên dãy $A$. $maxRange[i] =$ giá trị lớn nhất trong đoạn. ### Input - Dòng 1: chứa hai số nguyên dương $N≤10^5,k≤N$ cách nhau bởi dấu cách - Dòng 2: chứa N số nguyên dương $A_1,A_2,…,A_N$ $(∀i:A_i≤10^9)$ cách nhau bởi dấu cách ### Output In ra $N - k + 1$ dòng: - Dòng thứ $i$ in ra giá trị lớn nhất $maxRange[i]$ của các phần tử trong đoạn từ $i−k+1$ đến $i$. ### Sample input ``` 8 3 1 3 5 7 4 5 9 5 ``` ### Sample Output ``` 5 7 7 7 9 9 ``` ### Lời giải #### Thuật toán "trâu bò" - Đơn giản là đề nói gì chúng ta làm nấy, với mỗi $i$ từ $1 \rightarrow n - k + 1$ ta duyệt $j$ từ $i \rightarrow i + k - 1$ để tìm max và in ra kết quả. #### Thuật toán tối ưu sử dụng deque - Để đơn giản, ta hãy lấy 1 ví dụ sau: $n = 9, k = 3, a = [7, 3, 1, 2, 8, 6, 4, 1, 0]$. - Mục đích khi ta sử dụng deque ở đây là làm sao để lưu được phần tử lớn nhất của dãy $k$ phần tử liên tiếp ở đầu hàng đợi (hay cửa sổ $k$). - Ví dụ như ở cửa sổ $k$ đầu tiên là $[7, 3, 1]$, ta thấy được rằng ở cửa sổ này, kết quả sẽ là $7$, vậy $dq = [7]$. Thế nhưng liệu số $3, 1$ có còn là vô dụng? Hãy xem xét qua cửa sổ thứ $2$ là $[3, 1, 2]$, đáp án lúc này là $3$. Vậy nên ta nhận xét rằng $3, 1$ lúc này còn có thể là đáp án của một cửa sổ khác, ta cần lưu $3, 1$ này vào $dq$ hiện tại ($dq = [7, 3, 1]$). - Tiếp theo đó, ta xét ở cửa sổ thứ 2 ($[3, 1, 2]$). Ta sẽ loại bỏ phần tử ở đầu đi vì nó là kết quả của cửa sổ trước đó ($dq = [3, 1]$). Trước khi thêm $2$ vào deque, ta cần để ý một điều rằng, liệu $1$ có cần thiết cho các cửa sổ sau đó hay không? Câu trả lời là không, bởi vì tất cả các cửa sổ sau đó chứa $1$ thì đều sẽ chứa $2$, mà $1$ thì không thể là max vì $1 < 2$. Vậy nên lúc này ta sẽ loại $1$ ra khỏi deque và thêm $2$ vào deque ($dq = [3, 2]$). - Tương tự như vậy với các cửa sổ sau đó... - **Vậy nên ta kết luận lại như sau:** - Với mỗi $i$, nếu $dq$ đang trống (đồng nghĩa với việc không tồn tại một max nào trước đó và $a_i$ là max) ta đẩy $i$ vào $dq$. - Ta lần lượt loại bỏ các phần tử $\le a_i$ trong $dq$ và thêm $i$ vào $dq$. - Nếu phần tử ở đầu $dq$ có vị trí nằm ngoài cửa sổ thứ $i$ hiện tại (nằm ngoài đoạn $[i - k + 1, i]$) thì ta xoá phần tử ở đầu đi. :::spoiler **Code mẫu** ```cpp= #include <bits/stdc++.h> #define ll long long using namespace std; const int maxn = 1e5 + 5; int a[maxn]; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; for (int i = 1; i <= n; i++) cin >> a[i]; deque<int> dq; // Ta thêm cửa sổ đầu tiên vào hàng đợi for (int i = 1; i <= k; i++) { // Nếu deque không rỗng và phần tử ở cuối hàng đợi nhỏ hơn hoặc bằng phần tử hiện tại thì ta loại bỏ phần tử ở cuối hàng đợi while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back(); dq.push_back(i); } // Kết quả của cửa sổ đầu tiên cout << a[dq.front()] << endl; // Thêm các cửa sổ tiếp theo vào hàng đợi for (int i = k + 1; i <= n; i++) { while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back(); dq.push_back(i); // Nếu phần tử ở đầu hàng đợi có vị trí nằm ngoài đoạn [i - k + 1, i] thì ta loại bỏ if (dq.front() < i - k + 1) dq.pop_front(); // Kết quả của cửa sổ thứ i - k + 1 cout << a[dq.front()] << endl; } return 0; } ``` ::: # Bài tập áp dụng ## [Bài 1](https://cses.fi/problemset/task/1644) Cho dãy số nguyên a gồm $N$ phần tử và hai số nguyên dương $L, R \le N$. Bạn được yêu cầu tìm dãy con liên tiếp có tổng lớn nhất sao cho độ dài của nó nằm trong đoạn $[L, R]$. ### Input - Dòng đầu chứa ba số nguyên $N, L, R$ $(L \le R \le N \le 10^5)$. - Dòng thứ hai chứa $N$ số nguyên miêu tả dãy $a$. $(|a_i| \le 10^9)$. ### Output Một số nguyên – Tổng lớn nhất tìm được. ### Sample input ``` 9 2 3 40 -39 0 3 -5 0 3 0 1 ``` ### Sample output ``` 4 ``` ## [Bài 2](https://oj.vnoi.info/problem/qbrect) Cho một bảng kích thước $M \times N$, được chia thành lưới ô vuông đơn vị $M$ dòng $N$ cột $(1 \le M, N \le 1000)$ Trên các ô của bảng ghi số $0$ hoặc $1$. Các dòng của bảng được đánh số $1, 2, ..., M$ theo thứ tự từ trên xuống dưới và các cột của bảng được đánh số $1, 2, ..., N$ theo thứ tự từ trái qua phải. **Yêu cầu:** Hãy tìm một hình chữ nhật gồm các ô của bảng thoả mãn các điều kiện sau: - Hình chữ nhật đó chỉ gồm các số $1$. - Cạnh hình chữ nhật song song với cạnh bảng. - Diện tích hình chữ nhật là lớn nhất có thể. ### Input - Dòng $1$: Ghi hai số $M,N$ - Dòng thứ $i$ trong $M$ dòng tiếp theo: Ghi $N$ số mà số thứ $j$ là số ghi trên ô $(i, j)$ của bảng ### Output Gồm $1$ dòng duy nhất ghi diện tích của hình chữ nhật tìm được ### Sample Input ``` 11 13 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1 ``` ### Sample Output ``` 49 ``` # Nguồn tham khảo :::info [1] https://www.geeksforgeeks.org/the-c-standard-template-library-stl/ [2] https://www.geeksforgeeks.org/iterators-c-stl/ [3] https://cplusplus.com/reference/iterator/ [4] https://cplusplus.com/reference/vector/vector/ [5] https://cplusplus.com/reference/deque/deque/ [6] https://vnoi.info/wiki/algo/data-structures/deque-min-max.md :::