# Đề 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;
}
```
:::