# Ôn tập TS10 & THTB 2025 - Đề 2: Editorial
>[!Note] Thông tin
>Sau đây là lời giải của Đề 2 trong chuỗi đề ôn tập TS10 & THTB 2025.
>
>**Bạn đọc có thể nộp và chấm bài** [TẠI ĐÂY](https://chuyentin-brvt.online/contest/algi_mockts10_2025_de2)
>
>:::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: Truy đuổi
### Lời giải
Mô phỏng lại cuộc truy đuổi bằng cách đặt hai biến tượng trưng cho vị trí của rô-bốt và kẻ trộm. Sau mỗi giây, thay đổi hai vị trí này và kiểm tra khoảng cách.
**Độ phức tạp thời gian**: $O(n)$.
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
int n, k, p;
int main() {
cin >> n >> k >> p;
int ans = 0;
long long robot = 0, thief = 0;
while (n--) {
long long x;
cin >> x;
robot += x;
thief += k;
if (abs(robot - thief) <= p) {
ans++;
}
}
cout << ans;
return 0;
}
```
:::
## Bài 2: Bài toán khó
### Subtask 1
>[!Tip] Nhận xét
> Vì $\text{BCNN}(A, B) = n$ nên ta có $A, B$ là ước của $n$.
Thử duyệt qua mọi cặp ước của $n$, kiểm tra cặp số có thỏa mãn điều kiện không, sau đó lấy đáp án nhỏ nhất.
**Độ phức tạp thời gian:** $O \left( \sqrt n ^ 2 \right) = O(n)$.
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
long long n, m, res = 1e18;
long long __lcm(long long a, long long b) {
return (a / __gcd(a, b) * b);
}
int main() {
cin >> m >> n;
for (long long i = 1; i*i <= n; i++)
for (long long j = 1; j*j <= n; j++) {
if (__gcd(i, j) == m && __lcm(i, j) == n)
res = min(res, i + j);
if (__gcd(n/i, j) == m && __lcm(n/i, j) == n)
res = min(res, n/i + j);
if (__gcd(i, n/j) == m && __lcm(i, n/j) == n)
res = min(res, i + n/j);
if (__gcd(n/i, n/j) == m && __lcm(n/i, n/j) == n)
res = min(res, n/i + n/j);
}
if (res == 1e18) res = -1;
cout << res;
return 0;
}
```
:::
### Subtask 2
>[!Tip] Nhận xét
> Vì $\text{UCLN}(A, B) = m$ nên ta có $A, B$ là bội của $m$, viết lại thành:
> - $A = m \times x$
> - $B = m \times y$
> > Với $x, y$ là các số nguyên dương.
Lại có:
$$
\begin{flalign}
\text{BCNN}(A, B) &= \frac {A \times B}{\text{UCLN}(A, B)} = \frac {m \times x \times m \times y}{m} = m \times x \times y \\
\Leftrightarrow n = m \times x \times y \\
\Leftrightarrow x \times y = \frac n m
\end{flalign}
$$
Như vậy, nếu $n$ không chia hết cho $m$ thì không tồn tại đáp án thỏa mãn, ngược lại ta có $x$ và $y$ là ước của $\frac n m$ đồng thời $x \times y = \frac n m$.
Duyệt các cặp $(x,y)$ bằng cách duyệt qua các ước của $\frac n m$, tính $A = m \times x$, và $B = m \times y$, sau đó kiểm tra điều kiện $\text{UCLN}(A, B) = m$
**Độ phức tạp thời gian:** $O \left( \sqrt \frac{n}{m} \right)$.
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
long long m, n;
int main() {
cin >> m >> n;
if (n % m != 0) {
cout << -1;
return 0;
}
long long s = n / m;
long long ans = LLONG_MAX;
for (long long x = 1; x * x <= s; ++x) {
if (s % x == 0) {
long long y = s / x;
long long a = m*x, b = m*y;
if (__gcd(a, b) == m) {
ans = min(ans, a + b);
}
}
}
cout << (ans == LLONG_MAX ? -1 : ans);
return 0;
}
```
:::
## Bài 3: Mật mã
### Lời giải
Bài toán này chỉ kiểm tra khả năng sử dụng kiểu dữ liệu xâu.
>[!Important] Chữ số và kí tự
> Cần phải hiểu, trong kiểu dữ liệu xâu, kí tự `9` khác với giá trị số 9. Ký tự này mang một giá trị mã ASCII để thực hiện tính.
>
> **VD:**
> `9` = $57$ trong bảng mã ASCII.
> `A` = $65$ trong bảng mã ASCII.
>
> Để thao tác với số `9`, ta cần lấy giá trị $9$ của nó bằng cách lấy kí tự `9` - `0`:
> - `9` = $57$ trong bảng mã ASCII.
> - `0` = $48$ trong bảng mã ASCII.
> - `9` - `0` = $57 - 48$ = $9$.
> > Nguyên lí này dựa vào việc các số `0`, `1`, `2`, ..., `9` được sắp xếp liền kề và tăng dần trong bảng mã ASCII, có giá trị từ $48$ đến $57$
Trong bảng mã ASCII, các chữ cái in hoa `A`, `B`, ..., `C` được xếp liền kề nhau. Do đó, khi dịch các chữ cái ta chỉ việc cộng thêm vào ký tự một khoảng cần dịch.
>[!Caution] Lưu ý
> Ta cần mô phỏng việc xoay vòng bảng chữ cái (khi hết ký tự `Z` thì quay lại kí tự `A`).
>
> Dễ dàng thực hiện điều này bằng câu lệnh điều kiện.
**Độ phức tạp thời gian:** $O \left( L \right)$ với $L$ là độ dài xâu.
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
string s;
int main() {
cin >> s;
string res = "";
for (int i = 0; i < s.size(); i += 2) {
//Lấy số bước dịch chuyển
int step = s[i+1] - '0';
//Nếu cộng vào vượt ra khỏi `Z`, cần quay lại `A`.
if (s[i] + step > 'Z') step -= 26;
res.push_back(s[i] + step);
}
cout << res;
return 0;
}
```
:::
## Bài 4: Tuyến xe buýt
### Subtask 1
Với $k = 2$, ta thử mọi cặp vị trí để mở trạm.
Giả sử ta mở hai trạm $i, j \; (i \lt j)$, ta có:
- Hành khách đi qua cổng $i$ để đến trạm $i, i+1, \dots, j-1$.
- Hành khách đi qua cổng $j$ để đến trạm $j+1, j+2, \dots, n, 1, 2, \dots, i-1$.
Để dễ xử lý, nhất là trường hợp thứ hai, đoạn bị cắt ra làm hai phần (cuối mảng và đầu mảng), thì ta thực hiện ==nhân đôi mảng ban đầu==. Khi đó, vị trí $n+1, n+2, \dots$ cũng tương ứng với vị trí $1, 2, \dots$
>[!Tip] Tính nhanh tổng quãng đường
> Để tính nhanh được giá trị này, ta cần lưu hai ==mảng cộng dồn== như sau:
> - $S$ là mảng cộng dồn thông thường với $S_i = A_1 + A_2 + \dots + A_i$.
> - $F$ là mảng cộng dồn "bậc thang" với $F_i = A_1 + A_2\times 2 + A_3 \times 3 + \dots + A_i \times i$.
>
> Khi đó, từ trạm $i$, tổng chi phí để đến các trạm $i+1, i+2, \dots, j$ là:
> $$
> \begin{flalign}
> &r_{i+1} \times 1 + r_{i+2} \times 2 + \dots + r_j \times (j - i) \\
> &= (r_i \times i + r_{i+1} \times (i+1) + r_{i+2} \times (i+2) + \dots + r_j \times j) - i \times (r_i + r_{i+1} + \dots + r_j) \\
> &= F_j - F_{i-1} - i \times (S_j - S_{i-1})
> \end{flalign}
> $$
**Độ phức tạp thời gian:** $O(n^2)$
:::success
:::spoiler Code
```cpp = 1
#include <bits/stdc++.h>
using namespace std;
const int K = 7, N = 100;
const long long MX = 1e18;
int n, k, A[N+1];
long long S[2*N+1], F[2*N+1], res = MX;
long long cost(int i, int j) {
if (j < i) j += n;
return F[j] - F[i-1] - (S[j] - S[i-1])*i;
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> A[i];
S[i] = S[i-1] + A[i];
F[i] = F[i-1] + (long long)i*A[i];
}
for (int i = n+1; i <= 2*n; i++) {
S[i] = S[i-1] + A[i-n];
F[i] = F[i-1] + (long long)i*A[i-n];
}
for (int i = 1; i <= n; i++)
for (int j = i+1; j <= n; j++)
res = min(res, cost(i, j-1) + cost(j, i-1));
cout << res;
return 0;
}
```
:::
### Subtask 2
>[!Tip] Bài toán con
> Xét bài toán đơn giản hơn như sau:
>
> *Cho $n$ trạm xe buýt xếp thành một hàng thẳng được đánh số từ $1$ đến $n$ từ trái sang phải. Từ trạm $i$ chỉ có thể đi đến trạm $i+1$. Chỉ có $k$ trạm được mở, mỗi trạm có chỉ tiêu $r_i$ hành khách. **Tìm tổng quãng đường ít nhất các hành khách phải di chuyển mà vẫn đạt chỉ tiêu.***
>
> Bản chất, bài toán này là bài toán ==chia đoạn thành $k$ nhóm liên tiếp==. Trong đó, để ghép một đoạn liên tiếp $i, i+1, i+2, \dots, j$ thành một nhóm sẽ mất chi phí là tổng quãng đường để các hành khách xuất phát từ trạm $i$ và đến trạm $i+1, i+2, \dots, j$.
>
> **Đó là:**
> $$
> r_{i+1} \times 1 + r_{i+2} \times 2 + \dots + r_j \times (j - i)
> $$
>
> Để tính chi phí này nhanh cho nhiều đoạn, có thể áp dụng ==mảng cộng dồn== như subtask trước, hoặc duyệt ngược và duy trì tổng hậu tố (xem code để rõ hơn).
:::success
Nếu xem xét kỹ, để giải bài toán gốc, ta chỉ cần giải bài toán con như trên với vị trí bắt đầu lần lượt là $1, 2, \dots ,n$. Sau đó lấy ==đáp án nhỏ nhất==.
**VD:** Với $n$ = 5. Đặt vị trí bắt đầu là $3$, ta có $n$ trạm xe buýt thẳng hàng là:
$$
3, 4, 5, 1, 2
$$
:::
==**SAU ĐÂY LÀ HƯỚNG DẪN GIẢI BÀI TOÁN CON**==
Áp dụng ==quy hoạch động==.
- **Định nghĩa:** ==$dp_{i, j}$== là tổng quãng đường nhỏ nhất các hành khách phải đi thỏa mãn $n$ trạm đều đạt chỉ tiêu, khi đã mở cửa $i$ trạm, và $j$ trạm đầu tiên đạt chỉ tiêu.
- **Khởi gán:**
- ==$dp_{0, 0} = 0$==: Khi chưa mở cửa trạm nào thì cũng không trạm nào đạt chỉ tiêu.
- ==Mọi giá trị $dp$ còn lại== gán bằng $\infty$.
- **Công thức:**
- ==$dp_{i, j} = \min(dp_{i-1, t-1} + C(t, j))$== với $1 \le t \le j$ và $C(t, j)$ là tổng quãng đường để hành khách đi từ trạm $t$ đến các trạm $t+1, t+2, \dots, j$. Tức là thử các TH sau:
- Ghép trạm $j$ thành một nhóm
- Ghép trạm $j, j-1$ thành một nhóm
- ...
- Ghép giạm $1, 2, \dots, j$ thành một nhóm
- **Kết quả:** ==$dp_{k, n}$==: Mở cửa $k$ trạm, và cả $n$ trạm đều đạt chỉ tiêu.
**Độ phức tạp thời gian:** $O(kn^3)$
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int K = 10, N = 100;
const long long MX = 1e18;
int n, k, A[N+1], B[N+1];
long long dp[K+1][N+1], res = MX;
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> A[i];
for (int beg = 1; beg <= n; beg++) {
for (int i = 1; i <= n; i++) {
int pos = beg + i - 1;
if (pos > n) pos -= n;
B[i] = A[pos];
}
for (int p = 0; p <= k; p++)
for (int i = 0; i <= n; i++)
dp[p][i] = MX;
dp[0][0] = 0;
for (int p = 1; p <= k; p++)
for (int i = 1; i <= n; i++) {
long long sum = 0, cost = 0;
for (int j = i; j >= 1; j--) {
cost += sum;
sum += B[j];
dp[p][i] = min(dp[p][i], dp[p-1][j-1] + cost);
}
}
res = min(res, dp[k][n]);
}
cout << res;
return 0;
}
```
:::