--- title: "🦑-[SUSHI]-🍣" tags: [algorithm, competitive-programming, solution] author: "**Phạm Minh Quân**" created: "**14/03/2025**" --- # 📌 **Đề bài** 🔗 **[Xem đề bài tại đây](https://drive.google.com/file/d/1fwF_rQPaDxhaipVECDuguQpIVsvgizV_/view)** Cho dãy sushi gồm **n** miếng (**`1`**: 🐟 cá ngừ, **`2`**: 🐍 lươn). Tìm đoạn con liên tiếp **dài nhất** thỏa mãn: - ✅ **Chỉ có hai loại sushi**. - ✅ **Số lượng mỗi loại bằng nhau**. - ✅ **Nửa đầu chỉ chứa một loại sushi**. --- # 📖 **Mục lục** [TOC] --- # 🏆 **Các Subtasks** ## 🔹 **Subtask 1: Bruteforce** 🛠️ - **Ràng buộc**: $N \leq 10^3$ - **Ý tưởng**: 📌 Ta sẽ duyệt qua **tất cả các đoạn con** của mảng. **Tính chất quan trọng:** - 🔹 Độ dài của đoạn **luôn chẵn** ✅ - 🔹 Nửa đầu đoạn chứa **toàn bộ một loại cá** - 🔹 Nửa sau đoạn chứa **toàn bộ loại cá còn lại** 🔥 **Nhận xét:** Nếu độ dài lẻ ❌, khi chia đôi sẽ có **một phần tử dư**, dẫn đến mất cân bằng. **Ví dụ:** - ✅ **Dãy hợp lệ:** `2 2 1 1` (độ dài **chẵn**). - ❌ **Dãy không hợp lệ:** 1 2 2 1 (Hai nửa **không cùng một loại**) - ❌ **Dãy không hợp lệ:** `2 1 1 2 1` (độ dài **lẻ**, có một số dư khiến dãy mất cân bằng). ⟶ **Độ dài đoạn cần tìm luôn là số chẵn**. --- ### 📌 Cách thực hiện Gọi **độ dài đoạn** là `len`, ta chia đoạn thành **hai phần bằng nhau**: - **Nửa đầu**: `[i → i + mid - 1]` - **Nửa sau**: `[i + mid → i + len - 1]` Trong đó, `mid = len / 2`. - <span style="background-color: #FFF3CD; color: #FF0000; padding: 3px 4px; border-radius: 5px;">Ta sẽ dùng mảng 2 mảng prefix để lưu lại số lượng 2 loại Sushi </span> Ta có **2 trường hợp** có thể xảy ra - TH1: **Nửa đầu** toàn **sushi 1**, **nửa sau** toàn **sushi 2** - TH2: **Nửa đầu** toàn **sushi 2**, **nửa sau** toàn **sushi 1** --- ### 📜 **Cài đặt thuật toán với độ phức tạp $O(N^2)$** ```cpp #include <bits/stdc++.h> #define int long long #define name "SUSHI" using namespace std; const int N = 2e6 + 9; int a[N], pre0[N], pre1[N]; void file() { ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0); freopen(name ".inp", "r", stdin); freopen(name ".out", "w", stdout); } int32_t main() { file(); int n; cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; pre0[i] = (a[i] == 1) ? pre0[i - 1] + 1 : pre0[i - 1]; pre1[i] = (a[i] == 2) ? pre1[i - 1] + 1 : pre1[i - 1]; } int maxx = 0; for (int len = 2; len <= n; len += 2) { for (int i = 1; i <= n - len + 1; i++) { int mid = len / 2; int sl1_1 = pre0[i + mid - 1] - pre0[i - 1]; int sl2_1 = pre1[i + len - 1] - pre1[i + mid - 1]; int sl1_2 = pre1[i + mid - 1] - pre1[i - 1]; int sl2_2 = pre0[i + len - 1] - pre0[i + mid - 1]; if ((sl1_1 == sl2_1 && sl1_1 + sl2_1 == len) || (sl2_2 == sl1_2 && sl1_2 + sl2_2 == len)) { maxx = max(maxx, len); } } } cout << maxx; } ``` ## 🔹 **Subtask 2: 🦾DSU**⚡ - **Ràng buộc**: $N \leq 10^5$ - **Ý tưởng**: 📌 Ta sẽ thừa kế ý tưởng của bài trước và sẽ cải tiến lên thành **DSU - Disjoint Set Union** **Tìm hiểu thêm về DSU** - 🔹 🔗 **[Xem tại đây](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union)** --- ### 💡Giải thuật tìm độ dài lớn nhất của đoạn hợp lệ ### 1️⃣ Kết nối các số liền kề Chúng ta sẽ **kết nối tất cả các số liền kề** tại vị trí $i$ nếu giá trị của chúng bằng $i$. Tức là nối các số trong hai đoạn: - **$[i \rightarrow i+1]$** - **$[i-1 \leftarrow i]$** Sau khi nối xong, chúng ta sẽ có: - **Mảng `par`**: Chứa thông tin về gốc của mỗi đoạn. - **Mảng `sz`**: Chứa số lượng phần tử trong đoạn đã nối. **Ví dụ:** ```plaintext Dãy ban đầu: [2, 2, 2, 1, 1, 2, 2] Mảng parent: [1, 1, 1, 2, 2, 3, 3] Mảng sz: [3, 3, 3, 2, 2, 2, 2] ``` --- ### 2️⃣ Xác định độ dài đoạn hợp lệ Giả sử chúng ta có đoạn số $A$ và $B$ nằm cạnh nhau: ```phaintext= A: 2 2 2 | B: 1 1 ``` Quy tắc xác định độ dài đoạn hợp lệ: - <span style="background-color: #FFF3CD; color: #FF0000; padding: 3px 4px; border-radius: 5px;">Độ dài hợp lệ = min(độ dài A, độ dài B) </span> 📌Vì sao lại lấy $\min$? > Nếu một đoạn lớn nhưng đoạn còn lại nhỏ, thì độ dài tối đa chỉ có thể là đoạn nhỏ nhân đôi. > Ví dụ: Nếu đoạn $A$ có 3 số 2, đoạn $B$ có 2 số 1, thì ta chỉ có thể tạo được đoạn 2 2 | 1 1, không thể mở rộng thêm. --- ### 3️⃣ Lưu ý quan trọng ❌ Không thể nối cả hai bên cùng lúc Khi nối hai số 1, ta chỉ có thể nối qua trái hoặc phải, chứ không thể nối cả hai bên. - Ví dụ sai ❌: ```plaintext= 2 2 1 1 2 2 (không hợp lệ) ``` - **Lý do**: Nếu nối cả hai bên mà số lượng phần tử không đồng đều, đoạn nối sẽ bị lỗi. → **Giải pháp**: Chỉ xét một phía **(trái hoặc phải)**. --- ### 4️⃣ Tóm tắt thuật toán 🔹 **Bước 1**: 🏗️ **Khởi tạo DSU** - Tạo mảng `parent` và `sz` để quản lý các tập hợp số liên tiếp. 🔹 **Bước 2**: 🔍 **Duyệt qua từng đoạn số** và tìm độ dài hợp lệ theo công thức: 🎯 **Công thức**: - <span style="background-color: #FFF3CD; color: #FF0000; padding: 3px 5px; border-radius: 5px;">📏 min(sz[A], sz[B]) × 2</span> 🔹 **Bước 3**: 📊 **Tìm max_length** - Duyệt toàn bộ dãy số để tìm giá trị **lớn nhất** của đoạn hợp lệ. --- ### 📜 **Cài đặt thuật toán với độ phức tạp $O(NlogN)$** ```cpp= #include <bits/stdc++.h> #define int long long #define name "SHUSHI" using namespace std; const int N = 1e5 + 9; int a[N], par[N], sz[N]; void file() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); freopen(name ".inp", "r", stdin); freopen(name ".out", "w", stdout); } void make_set(int n) { for (int i = 1; i <= n; i++) { par[i] = i; sz[i] = 1; } } int find_p(int v) { if (par[v] == v) return v; return par[v] = find_p(par[v]); } void uni(int u, int v) { u = find_p(u); v = find_p(v); if (u == v) return; if (sz[u] < sz[v]) swap(u, v); par[v] = u; sz[u] += sz[v]; } int32_t main() { file(); int n; cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; make_set(n); for (int i = 1; i <= n; i++) { if (i > 1 && a[i] == a[i - 1]) uni(i, i - 1); if (i < n && a[i] == a[i + 1]) uni(i, i + 1); } vector<int> q; q.push_back(0); for (int i = 1; i <= n; i++) { if (i < n && find_p(i) != find_p(i + 1)) { int minn = min(sz[find_p(i)], sz[find_p(i + 1)]); q.push_back(minn); } if (i > 1 && find_p(i) != find_p(i - 1)) { int minn = min(sz[find_p(i)], sz[find_p(i - 1)]); q.push_back(minn); } } cout << *max_element(q.begin(), q.end()) * 2; return 0; } ```