# Đề TS10 - BRVT - 2023: Editorial >[!Note] Thông tin >Sau đây là lời giải của Kỳ thi Tuyển sinh 10 trường THPT chuyên Lê Quý Đôn tỉnh Bà Rịa - Vũng Tàu năm học 2023 - 2024. > >**Bạn đọc có thể nộp và chấm bài (test tự sinh)** [TẠI ĐÂY](https://chuyentin-brvt.online/contest/algi_ts10_15_24). > >:::info >Chuẩn bị bởi ==team Logica==, thuộc khuôn khổ dự án [**The Algitect (ALGI Project)**](https://www.facebook.com/algitect.project) >::: [toc] ## Bài 1 ### Tóm tắt đề bài Một quyển sách gồm $N$ trang. Trong đó trang $1$ luôn nằm phía bên phải của trang bìa đầu, trang $N$ luôn nằm ở mặt bên trái của trang bìa cuối của quyển sách. Sau khi lật trang $1$ sẽ đến trang $2, 3$, rồi đến $4, 5$, ... Ngược lại, nếu lật từ trang $N$, ta sẽ đến trang $N-1, N-2$, rồi $N-3, N-4$, ... **Yêu cầu:** Cho $P$, tìm số bước ít nhất để lật đến trang $P$. **Dữ liệu đảm bảo:** $N \le 10^{18}$ và $1 \lt P \lt N$. **Ràng buộc:** - $75\%$ số điểm tương ứng với $1 \lt P \lt N \le 10^9$; - $25\%$ số điểm tương ứng với $10^9 \lt P \lt N \le 10^{18}$. ### Subtask 1 Mô phỏng lại quá trình lật sách, ==duyệt vòng lặp== để lật từ trang $1$ đến trang $P$, và từ trang $N$ đến trang $P$, kết quả là số lần lật ít hơn trong hai cách lật. **Độ phức tạp thời gian:** $O\left( n \right)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; long long n, p, kq1, kq2; void lm(){ long long i; for (i = 2; i <= p; i += 2) { kq1++; } if (p == n) { cout << 0; return; } for (i = n - 1; i >= p; i -= 2) kq2++; cout << min(kq1, kq2); } int main() { cin >> n >> p; if (p == 1) cout << 0; else lm(); return 0; } ``` ::: ### Subtask 2 >[!Tip] Nhận xét > Trừ trang $1$ và trang $n$, dễ thấy các trang có tính chất xuất hiện ==theo cặp==: > $$ > (2, 3), (4, 5), (6, 7), (8, 9), \dots > $$ > Nghĩa là, số bước lật đến trang $3, 5, 7, 9, \dots$ cũng bằng số bước lật đến trang $2, 4, 6, 8, \dots$ Để dễ xử lí, sau khi nhập vào $p$, nếu $p$ chẵn, ta trừ đi $1$ để được $p$ chẵn. **Đáp án:** ==$\min \left( \frac{p}{2}, \frac{n-p}{2} \right)$== **Độ phức tạp thời gian:** $O \left( 1 \right)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; int main() { long long n, p; cin >> n >> p; if (p % 2 != 0) { p--; } cout << min(p / 2, (n-p) / 2); return 0; } ``` ::: ## Bài 2 ### Tóm tắt đề bài Cho hai số $a$ và $b$ ở ==hệ nhị phân==. **Yêu cầu:** Đếm số lượng ==số chính phương== trong đoạn $[a, b]$. **Dữ liệu đảm bảo:** $1 < a < b \le 10^{18}$. **Ràng buộc:** - $75\%$ số điểm tương ứng với $1 \lt a \lt b\le10^9$. - $25\%$ số điểm tương ứng với $1 \lt a \lt b\le10^{18}$. ### Nhị phân và thập phân >[!Important] > Để xử lý được bài toán này, bắt buộc cần phải biến đổi $a$ và $b$ từ hệ nhị phân sang hệ thập phân. > > VD: $1110_2 = 14_{10}$ Trước tiên, cần nắm được ==mối quan hệ giữa số nhị phân và số thập phân==: $$ x_{n-1} x_{n-2} \dots x_1 x_0 = x_0 \times 2^0 + x_1 \times 2^1 + \dots + x_{n-2} \times 2^{n-2} + x_{n-1} \times 2^{n-1} $$ Trong đó, vế trái là ==số nhị phân== bất kỳ, và vế phải là ==biểu diễn thập phân== tương ứng của số nhị phân đã cho. >VD: $1110_2 = 0 \times 2^0 + 1 \times 2^1 + 1 \times 2^2 + 1 \times 2^3 = 14_{10}$ :::danger **Lưu ý:** Trong hệ nhị phân, các chữ số được đánh số từ $0$, và từ phải qua trái. ::: Để tính nhanh $2^0, 2^1, \dots, 2^{n-1}$, ta duyệt theo thứ tự các chữ số của số nhị phân, đồng thời duy trì một biến giá trị $2^i$. Sau khi duyệt qua một số, ta nhân thêm $2$ vào giá trị này. :::success :::spoiler Code ```cpp = 1 int binToDec(string s) { int ret = 0; long long k = 1; for (int i = s.length() - 1; i >= 0; i--) { ret += k * (s[i] - '0'); k *= 2; } return ret; } ``` ::: ### Subtask 1 Duyệt ==vòng lặp từ $a$ đến $b$== các số chính phương tối đa đến $b$, và kiểm tra xem số đó có lớn hơn hoặc bằng $a$ hay không. Nghĩa là duyệt $i$ từ $1$, số chính phương tương ứng là $i \times i$, đến khi $i \times i > b$ thì dừng, trong lúc đó kiểm tra số $i \times i$ để thêm vào đáp án. **Độ phức tạp thời gian:** $O \left( \sqrt b \right)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; string s1, s2; long long a, b; void string_to_num() { long long k = 1; for (int i = s1.length() - 1; i >= 0; i--) { a += k*(s1[i]-'0'); k *= 2; } k = 1; for (int i = s2.length() - 1; i >= 0; i--) { b += k*(s2[i]-'0'); k *= 2; } } int main() { cin >> s1 >> s2; string_to_num(); long long res = 0; for (long long i = 1; i*i <= b; i++) if (i*i >= a) res++; cout << res; } ``` ::: ### Subtask 2 >[!Tip] > Ký hiệu số chính phương là ==$k^2$== với ==$k \in N$==. Theo bài toán, ta có: ==$a \le k^2 \le b$== $$ \begin{flalign} &\Leftrightarrow \sqrt a \le k \le \sqrt b \\ &\Leftrightarrow \left\lceil \sqrt a \,\right\rceil \le k \le \left\lfloor \sqrt b \,\right\rfloor \end{flalign} $$ Như vậy, đáp án của bài toán là: $$ \left\lfloor \sqrt b \,\right\rfloor - \left\lceil \sqrt a \,\right\rceil + 1 $$ >[!Caution] Lưu ý > Cần sử dụng hàm `sqrtl` thay cho hàm `sqrt` để đảm bảo độ chính xác. Hoặc tốt hơn nữa, là kiểm tra lại bằng câu lệnh `if` sau khi sử dụng hàm để tính căn bậc hai. **Độ phức tạp thời gian:** $O \left( \log a + \log b \right)$. > Hàm `sqrt` hay `sqrtl` có chi phí thời gian là $\log$. > :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; string s1, s2; long long a, b; void string_to_num() { long long k = 1; for (int i = s1.length() - 1; i >= 0; i--) { a += k*(s1[i]-'0'); k *= 2; } k = 1; for (int i = s2.length() - 1; i >= 0; i--) { b += k*(s2[i]-'0'); k *= 2; } } int main() { cin >> s1 >> s2; string_to_num(); cout << floor(sqrtl(b)) - ceil(sqrtl(a)) + 1; return 0; } ``` ::: ## Bài 3 ### Tóm tắt đề bài Có $N$ học sinh tham gia trò chơi được xếp hàng thành một đường thẳng và được đánh số thứ tự từ $1$ đến $N$. Biết không được xếp $3$ bạn nam cùng đứng kế nhau. **Yêu cầu:** Hãy cho biết có bao nhiêu cách xếp hàng thỏa mãn điều kiện trên. **Dữ liệu đảm bảo:** $N \le 64$. **Ràng buộc:** - $25\%$ số test tương ứng với $1<N\le20$. - $75\%$ số test tương ứng với $20<N\le64$. ### Subtask 1 ==Quay lui== để duyệt các ==cách chọn== hợp lệ và cộng vào đáp án. **Độ phức tạp thời gian:** $O \left( 2^n \right)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; long long n, kq; void backTrack(long long i, long long tmp){ if (i == n + 1) { kq++; return; } if (tmp < 2) { backTrack(i + 1, tmp + 1); } backTrack(i + 1, 0); } int main() { cin >> n; backTrack(1, 0); cout << kq; return 0; } ``` ::: ### Subtask 2 Áp dụng ==quy hoạch động cơ bản==. Để dễ thao tác, ta coi học sinh nam là `1` và học sinh nữ là `0` - **Định nghĩa:** $dp_i$ là số cách xếp $i$ học sinh thỏa yêu cầu đề bài. - **Khởi gán:** - $dp_1 = 2$ (`1` / `0`) - $dp_2 = 4$ (`11` / `00` / `10` / `01`) - $dp_3 = 7$ (`110` / `000` / `100` / `010` / `001` / `101` / `011`) - **Công thức:** $dp_i = dp_{i-1} + dp_{i-2} + dp_{i-3}$, trong đó: - $dp_{i-1}$: Có thể ghép `0` vào một dãy đã hợp lệ mà chắc chắn vẫn hợp lệ. - $dp_{i-2}$: Có thể ghép `01` vào một dãy đã hợp lệ mà chắc chắn vẫn hợp lệ. - $dp_{i-3}$: Có thể ghép `011` vào một dãy đã hợp lệ mà chắc chắn vẫn hợp lệ. **Độ phức tạp thời gian:** $O(n)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; long long n, kq, dp[100]; int main() { cin >> n; dp[1] = 2; dp[2] = 4; dp[3] = 7; for (int i = 4; i <= n; ++i) { dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]; } cout << dp[n]; return 0; } ``` ::: ## Bài 4 ### Tóm tắt đề bài Hoàng có một chiếc thẻ nhớ ngoài dung lượng ==$K$ (Gigabyte - GB)== để lưu ảnh. Cho biết thông về số lượng các loại bức ảnh, mỗi loại sẽ có dung lượng $a_i$ và tính thẩm mỹ $b_i$. Biết rằng một loại ảnh có thể ==không được chọn== hoặc ==chọn với số lượng không hạn chế==. > **Ghi chú:** 1GB = 1024MB **Yêu cầu:** Cho biết tổng giá trị lớn nhất của tính thẩm mỹ thu được khi chọn các ảnh mà vẫn đảm bảo không vượt quá dung lượng? **Dữ liệu đảm bảo:** $2\le N \le 1000$, $1\le K \le 4$, $1\lt a_i \le 1024$ và $1\lt b_i \le 10^9$. **Ràng buộc:** - $50\%$ số điểm tương ứng với $2\le N\le500$, $K\le2$, $a_i\le1024$, $b_i\le32000$. - $50\%$ số điểm tương ứng với $2\le N\le1000$, $K\le4$, $a_i\le1024$, $b_i\le10^9$. ### Subtask 1 Áp dụng ==quy hoạch động cái túi== theo cách cơ bản. - **Định nghĩa:** $dp_{i,j}$ là tính thẩm mỹ lớn nhất khi chọn từ $i$ loại ảnh đầu tiên và tổng dung lượng bằng $j$. - **Khởi gán:** $dp_{0,0} = 0$ (khi không chọn ảnh nào thì tổng thẩm mỹ bằng $0$). - **Công thức:** Lấy kết quả tốt nhất trong hai trường hợp: - $dp_{i, j} = dp_{i-1, j}$ (không lấy loại ảnh thứ $i$, giữ độ thẩm mỹ đạt được với $i-1$ loại) - $dp_{i, j} = \max ( dp_{i-1, j - x \times A_i + x \times B_i} )$ với $x$ là số lượng ảnh loại $i$ ta lấy. **Độ phức tạp thời gian:** $O(n \times k^2)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; const int N = 1e3, K = 4*1024; int n, k; long long F[N+1][K+1], A[N+1], B[N+1]; int main() { cin >> n >> k; k *= 1024; for (int i = 1; i <= n; i++) { cin >> A[i] >> B[i]; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= k; j++) { F[i][j] = F[i-1][j]; if (j - A[i] >= 0) { for (int x = 1; x * A[i] <= j; x++) { F[i][j] = max(F[i][j], F[i-1][j - x*A[i]] + x*B[i]); } } } } cout << F[n][k]; return 0; } ``` ::: ### Subtask 2 >[!Tip] Nhận xét > Ở bài toán **cái túi** cụ thể này, ta không cần quan tâm đến ràng buộc về phần tử (ta có thể bỏ các đồ vật không giới hạn). > :::success > Do đó, có thể **bỏ chiều $i$** trong công thức quy hoạch động ban đầu. > ::: Áp dụng ==quy hoạch động cái túi== biến thể. - **Định nghĩa:** $dp_{j}$ là tính thẩm mỹ lớn nhất khi chọn ảnh để tổng dung lượng bằng $j$. - **Khởi gán:** $dp_{0} = 0$ (khi không chọn ảnh nào thì tổng thẩm mỹ bằng $0$). - **Công thức:** $dp_j = \max(dp_{j - A_i} + B_i) \; \forall \; A_i \le j$ (lấy thêm ảnh thứ $i$). **Độ phức tạp thời gian:** $O(n \times k)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> using namespace std; const int N = 1e3, K = 4*1024; int n, k; long long F[K+1], A[N+1], B[N+1]; int main() { cin >> n >> k; k *= 1024; for (int i = 1; i <= n; i++) { cin >> A[i] >> B[i]; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= k; j++) { if (j - A[i] >= 0) { F[j] = max(F[j], F[j - A[i]] + B[i]); } } } cout << F[k]; return 0; } ``` :::