# Đề THT B - BRVT - 2024: Editorial
>[!Note] Thông tin
>Sau đây là lời giải của Kỳ thi Tin học trẻ bảng B 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_thtb_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: Hình chữ nhật (40 điểm)
### Tóm tắt đề bài
Trong mặt phẳng tọa độ $Oxy$, cho $n$ hình chữ nhật có các cạnh song song với trục tọa độ. Hình chữ nhật thứ $i$ được xác định bởi $4$ giá trị nguyên $a_i, b_i, c_i, d_i$ với $i = 1, 2, \dots , n$.
Trong đó $(a_i, b_i)$ là tọa độ đỉnh bên trái dưới cùng, còn $(c_i, d_i)$ là tọa độ đỉnh bên phải trên cùng của hình chữ nhật.
**Yêu cầu:** Hãy cho biết diện tích của hình chữ nhật lớn nhất mà phần diện tích đó thuộc tất cả $n$ hình chữ nhật đã cho.
**Dữ liệu đảm bảo:** $n \le 10^{4}$ và $|a_i|, |b_i|, |c_i|, |d_i| \le 10^9$.
### Lời giải
Gọi ==$(lx, ly)$== và ==$(rx, ry)$== lần lượt là tọa độ điểm ==trái dưới== và ==phải trên== của hình chữ nhật lớn nhất mà phần diện tích đó thuộc tất cả $n$ hình chữ nhật đã cho.
Dễ thấy:
- $lx = \max(a_1, a_2, \dots, a_n)$
- $ly = \max(b_1, b_2, \dots, b_n)$
- $rx = \max(c_1, c_2, \dots, c_n)$
- $ry = \max(d_1, d_2, \dots, d_n)$
Như vậy, ta duyệt qua $n$ hình chữ nhật đã cho để tính được tọa độ $(lx, ly)$ và $(rx, ry)$.
Khi đó, đáp án (tức là diện tích) là $(rx - lx) \times (ry - ly)$.
:::danger
**Lưu ý:** Trong trường hợp không tìm được hình chữ nhật nào thỏa, tức là $rx \lt lx$ hoặc $ry \lt ly$, thì cần in ra $0$.
:::
**Độ phức tạp thời gian:** $O \left( n \right)$.
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4;
long long n, lx = -1e9, ly = -1e9, rx = 1e9, ry = 1e9;
int main() {
cin >> n;
while (n--) {
long long a, b, c, d;
cin >> a >> b >> c >> d;
lx = max(lx, a);
ly = max(ly, b);
rx = min(rx, c);
ry = min(ry, d);
}
cout << max(0LL, rx - lx) * max(0LL, ry - ly);
return 0;
}
```
:::
## Bài 2: Số chính phương (30 điểm)
### Tóm tắt đề bài
Cho hai số nguyên dương $a$ và $b$.
**Yêu cầu:** Tìm số chính phương nhỏ nhất chia hết cho $a$ và $b$.
**Dữ liệu đảm bảo:** $a, b \le 10^4$.
**Subtasks:**
- $60\%$ số điểm ứng với $a, b \le 10^2$.
- $40\%$ số điểm không có ràng buộc gì thêm.
### Subtask 1
Ta biết một số ==chắc chắn thỏa mãn yêu cầu bài toán== là:
$$
\operatorname{BCNN}(a, b) ^ 2
$$
Trong trường hợp $a, b \le 10^2$ thì ==$\operatorname{BCNN}(a, b) ^ 2 \le (ab)^2 = 10^8$==.
Chính vì vậy, ta có thể duyệt qua mọi số để tìm đáp án.
**Độ phức tạp thời gian:** $O \left( (ab)^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int a, b;
int main() {
cin >> a >> b;
long long lim = 1LL*a*b*a*b;
for (long long i = 1; i <= lim; i++) {
long long t = (long long)sqrtl(i);
if (t*t != i) continue;
if (i % a == 0 && i % b == 0) {
cout << i;
break;
}
}
return 0;
}
```
:::
### Subtask 2
Kế thừa tư tưởng từ subtask trước, nhưng thay vì duyệt qua mọi số rồi lại phải kiểm tra số đó có phải số chính phương hay không, ta ==chỉ duyệt qua các số chính phương== mà thôi.
Ở đây nếu duyệt $i$ đến $ab$, ta sẽ thu được số chính phương tới $(ab)^2$.
**Độ phức tạp thời gian:** $O \left( ab \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int a, b;
int main() {
cin >> a >> b;
long long lim = 1LL*a*b*a*b;
for (int i = 1; 1LL*i*i <= lim; i++) {
long long t = 1LL*i*i;
if (t % a == 0 && t % b == 0) {
cout << t;
break;
}
}
return 0;
}
```
:::
## Bài 3: Phần thưởng (30 điểm)
### Tóm tắt đề bài
Có $m$ phần quà cần chia cho $n$ học sinh theo quy tắc: Em đứng trước sẽ được nhận số gói quà không ít hơn số gói quà của em đứng ở phía sau nhận (có thể có em không nhận phần quà nào).
Gọi $t_1, t_2, \dots, t_n$ lần lượt là tổng số gói quà em thứ $1, 2, \dots, n$ nhận được (tương ứng với thứ hạng từ cao xuống thấp) thì $t_1 \ge t_2 \ge \dots \ge t_n \ge 0$ và $t_1 + t_2 + \dots + t_n = m$.
**Yêu cầu:** Hãy cho biết có bao nhiêu cách phát quà khác nhau.
**Dữ liệu đảm bảo**: $m, n \le 1000$.
### Lời giải
Áp dụng ==quy hoạch động==:
- **Định nghĩa:** ==$dp_{i, j, k}$== là số cách phát hết $j$ gói quà cho $i$ học sinh đầu tiên và học sinh thứ $i$ nhận $k$ gói quà.
- **Khởi gán:** ==$dp_{1, j, j} = 1$==, có duy nhất một cách phát hết $j$ gói quà cho một học sinh là cho học sinh đó nhận hết.
- **Công thức:** ==$dp_{i, j, k} = dp_{i-1, j-k, k} + dp_{i-1, j-k, k+1} + \dots + dp_{i-1, j-k, j-k}$==, thực hiện phát $k$ gói quà cho bạn thứ $i$ và tổng gói quà đã chia là $j$:
- ==$dp_{i-1, j-k, k}$==: số cách phát $j-k$ gói quà cho $i-1$ bạn, bạn $i-1$ được $k$ gói.
- ==$dp_{i-1, j-k, k+1}$==: số cách phát $j-k$ gói quà cho $i-1$ bạn, bạn $i-1$ được $k+1$ gói.
- ...
- **Đáp án:** ==$dp_{n, m, 0} + dp_{n, m, 1} + \dots + dp_{n, m, m}$==.
>[!Caution] Vấn đề
> Thuật toán trên đang quá giới hạn về cả ==thời gian== và ==bộ nhớ==.
Vì nhận thấy các trạng thái ở $dp_i$ chỉ phụ thuộc vào $dp_{i-1}$, ta không cần phải lưu toàn bộ mảng quy hoạch động.
Đồng thời, ta có thể tính trước các trạng thái tác động đến $dp_i$ bằng mảng cộng dồn hậu tố (suffix sum).
Từ đó, ta chỉ cần lưu một mảng $dp$ và một mảng $suff$, mỗi mảng có $m^2$ phần tử.
>[!Tip] Về độ phức tạp thời gian
> Về lý thuyết, độ phức tạp thời gian là $nm^2$.
>
> Tuy nhiên, ta có thể cắt giảm số thao tác (nhất là ở vòng lặp) bằng cách chỉ duyệt $k$ đến $\frac j i$. Nếu chạy thử để đếm số thao tác lặp, có thể thấy chương trình vẫn chạy trong thời gian giới hạn dù với bộ dữ liệu tối đa.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3;
const long long M = 123456789;
long long m, n, dp[N+1][N+1], suff[N+1][N+1];
int main() {
cin >> m >> n;
for (int j = 0; j <= m; j++) {
for (int k = j; k >= 0; k--) {
suff[j][k] = 1;
}
}
for (int i = 2; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= m && k*i <= j; k++) {
dp[j][k] = suff[j-k][k];
}
}
for (int j = 0; j <= m; j++) {
suff[j][j/i+1] = 0;
for (int k = j/i; k >= 0; k--) {
suff[j][k] = (suff[j][k+1] + dp[j][k]) % M;
}
}
}
cout << suff[m][0];
return 0;
}
```
:::