# Đề HSG10-11 - BRVT - 2025: Editorial
>[!Note] Thông tin
>Sau đây là lời giải của Kỳ thi Chọn HSG10-11 tỉnh Bà Rịa - Vũng Tàu năm học 2024 - 2025.
>
>**Thời gian thi:** 08:00 - 11:00 ngày 27/03/2025.
>
>**Bạn đọc có thể nộp và chấm bài (test tự sinh)** [TẠI ĐÂY](https://chuyentin-brvt.online/contest/algi_hsg9_2025).
>
>:::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: Đếm số (4 điểm)
### Tóm tắt đề bài
Cho ba số nguyên dương $n, a, b$.
**Yêu cầu:** Đếm số lượng số nguyên $x \in [1, n]$ thỏa mãn $x \bmod a = x\bmod b$.
**Dữ liệu đảm bảo:** $1 \le n, a, b, \le 10^{18}$.
**Subtasks:**
- $75\%$ số điểm: $1 \le n, a, b \le 10^6$.
- $25\%$ số điểm: Không có ràng buộc gì thêm.
### Subtask 1
Duyệt qua từng số nguyên $x \in [1, n]$, nếu $x$ thỏa điều kiện, ta tăng đáp án lên $1$.
**Độ 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, a, b, res;
int main() {
cin >> n >> a >> b;
for (long long i = 1; i <= n; i++)
if (i % a == i % b)
res++;
cout << res;
return 0;
}
```
:::
### Subtask 2
>[!Tip] Nhận xét
> Giả sử ta có:
> $$
> x \bmod a = x \bmod b = r \; (r \in \operatorname{N})
> $$
> Khi đó:
> $$
> (x - r) \bmod a = (x - r) \bmod b) = 0
> $$
Nói cách khác, ==$x - r$ là bội của $\operatorname{BCNN}(a, b)$== với $r \in [0, \min(a, b) - 1]$.
Hay tương ứng với mỗi bội $t$ của $\operatorname{BCNN}(a, b)$, ta có $\min(a, b)$ đáp án, đó là:
$$
t, t + 1, \dots, t + \min(a, b) - 1
$$
>[!Caution] Cảnh báo
> Tuy nhiên, điều này không đúng với:
> - $t = 0$, vì không tính đáp án $0$.
> - $t + \min(a, b) - 1 \gt n$, vì có đáp án lớn hơn $n$.
> :::danger
> Chúng ta cần **xét riêng hai trường hợp** bội $t$ nhỏ nhất và lớn nhất trong khoảng $n$
> :::
Ta xét riêng ba trường hợp:
- $t = 0$ (bội nhỏ nhất), có $\min(a, b) - 1$ đáp án.
- $t + \min(a, b) - 1 \gt n$ (nếu có, $t$ này là bội lớn nhất), có $n - t + 1$ đáp án:
$$
t = \left \lfloor \frac {n} {\operatorname{BCNN}(a, b)} \right \rfloor \times \operatorname{BCNN}(a, b)
$$
- $t \gt 0$ và $t + \min(a, b) - 1 \le n$, số lượng đáp án là số lượng bội $t$ nhân cho số lượng $r$
- Số lượng bội $t$ của $\operatorname{BCNN}(a, b)$ thỏa $t \gt 0$ và $t + \min(a, b) - 1 \le n$:
$$
\left \lfloor \frac {n - \min(a, b) + 1} {\operatorname{BCNN}(a, b)} \right \rfloor
$$
- Số lượng $r$ (từ $0$ đến $\min(a, b) - 1$):
$$
\min(a, b)
$$
>[!Tip] Tính nhanh BCNN
> Ta có:
> $$
> \operatorname{BCNN}(a, b) = \frac {a \times b} {\operatorname{UCLN}(a, b)}
> $$
> Có thể tính nhanh $\operatorname{UCLN}(a, b)$ bằng câu lệnh `__gcd()` có sẵn của C++.
> :::danger
> **Lưu ý:** Với giới hạn dữ liệu lớn, nếu cứ mặc kệ mà tính BCNN, giá trị có thể lên đến $10^{36}$, dẫn đến tràn số.
>
> Do đó ta cần xử lý khéo bằng cách **dừng nếu nhận thấy số quá lớn**!
> :::
**Đáp án:**
$$
(\min(a, b) - 1) + \left( n - \left \lfloor \frac {n} {\operatorname{BCNN}(a, b)} \right \rfloor \times \operatorname{BCNN}(a, b) + 1 \right) + \left \lfloor \frac {n - \min(a, b) + 1} {\operatorname{BCNN}(a, b)} \right \rfloor \times \left[ \min(a, b) \right]
$$
**Độ phức tạp thời gian:** $O \left( \log \min(a, b) \right)$
> Độ phức tạp của thuật toán Euclid để tìm BCNN.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
long long n, a, b;
long long __lcm(long long x, long long y) {
long long z = x / __gcd(x, y);
if (z > n / y) return n + 1;
return (z * y);
}
int main() {
cin >> n >> a >> b;
long long t = __lcm(a, b);
if (a > b) swap(a, b);
long long res = min(n, (a - 1)) + ((max(0LL, n - a + 1) / t) * a);
if ((n / t) * t > 0 && (n / t) * t + a - 1 > n)
res += (n - (n / t) * t + 1);
cout << res;
return 0;
}
```
:::
## Bài 2: Tổng số nguyên tố (5 điểm)
### Tóm tắt đề bài
Cho $T$ truy vấn gồm hai số nguyên $a, b$.
**Yêu cầu:** Với mỗi $a, b$ tính tổng các số nguyên tố trong đoạn $[a, b]$.
**Dữ liệu đảm bảo:** $1 \le T, a, b \le 10^5$.
**Subtasks:**
- $20\%$ số điểm: $T = 1$ và $1 \le a, b \le 10^3$.
- $40\%$ số điểm: $T \le 10^3$ và $1 \le a, b \le 10^4$.
- $40\%$ số điểm: Không có ràng buộc gì thêm.
### Subtask 1
Duyệt qua từng số $x$ trong đoạn $[a, b]$, kiểm tra tính nguyên tố của $x$ bằng cách duyệt đến $\sqrt x$ để tìm ước số của $x$. Nếu $x$ là số nguyên tố, ta cộng thêm vào đáp án $x$.
**Độ phức tạp thời gian:** $O \left( \sqrt a + \sqrt{a + 1} + \dots + \sqrt b \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
int t, a, b;
bool isPrime(int num) {
if (num < 2) return 0;
for (int i = 2; i*i <= num; i++)
if (num % i == 0)
return 0;
return 1;
}
int main() {
cin >> t;
while (t--) {
cin >> a >> b;
int res = 0;
for (int i = a; i <= b; i++)
if (isPrime(i))
res += i;
cout << res << "\n";
}
return 0;
}
```
:::
### Subtask 2
Kế thừa tư tưởng từ subtask trước, tuy nhiên ta cần cải tiến việc kiểm tra tính nguyên tố của số $x$.
Nhận thấy $a, b \le 10^4$, ta có thể [**sàng số nguyên tố**](https://blog.28tech.com.vn/sang-so-nguyen-to) để kiểm tra trước tính nguyên tố của tất cả các số cần kiểm tra.
**Độ phức tạp thời gian:** $O \left( T \times (b - a) + b \log \log b \right)$.
>Duyệt qua đoạn $[a, b]$: $b - a$.
>Sàng số nguyên tố đến cận trên là $b$: $b \log \log b$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int MX = 1e4;
int t, a, b;
bool E[MX+1];
void sieve() {
E[0] = E[1] = 1;
for (int i = 2; i*i <= MX; i++)
if (!E[i])
for (int j = i*i; j <= MX; j += i)
E[j] = 1;
}
int main() {
sieve();
cin >> t;
while (t--) {
cin >> a >> b;
int res = 0;
for (int i = a; i <= b; i++)
if (!E[i])
res += i;
cout << res << "\n";
}
return 0;
}
```
:::
### Subtask 3
Kế thừa tư tưởng từ subtask trước, tuy nhiên ta cần cải tiến việc duyệt qua đoạn để tính tổng các số nguyên tố. Thay vào đó, ta có thể dùng [**mảng tiền tố (prefix sum)**](https://wiki.vnoi.info/algo/data-structures/prefix-sum-and-difference-array.md):
- Gọi $S_r$ là tổng các số nguyên tố nhỏ hơn hoặc bằng $r$.
- Khi đó tổng các số nguyên tố trong đoạn $[l, r]$ là: $S_r - S_{l-1}$
**Độ phức tạp thời gian:** $O \left( T + b \log \log b \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int MX = 1e5;
int t, a, b, S[MX+1];
bool E[MX+1];
void sieve() {
E[0] = E[1] = 1;
for (int i = 2; i <= MX; i++)
if (!E[i])
for (int j = i*i; j <= MX; j += i)
E[j] = 1;
}
int main() {
sieve();
for (int i = 1; i <= MX; i++) {
S[i] = S[i-1];
if (!E[i])
S[i] += i;
}
cin >> t;
while (t--) {
cin >> a >> b;
cout << S[b] - S[a-1] << "\n";
}
return 0;
}
```
:::
## Bài 3: Đoạn con đẹp (6 điểm)
### Tóm tắt đề bài
Cho dãy số nguyên dương $a_1, a_2, \dots, a_n$.
$S_{i, j}$ là dãy gồm các số liên tiếp $a_i, a_{i+1}, \dots, a_j$. Đoạn con này là ==đoạn con đẹp== nếu:
- ==$| j - i + 1 |$== là số chẵn.
- Có thể xáo trộn vị trí của các số trong đoạn để tạo thành ==palindrome==.
**Yêu cầu:** Cho $T$ truy vấn $i, j$, với mỗi truy vấn kiểm tra xem $S_{i, j}$ có đẹp không.
**Dữ liệu đảm bảo:** $1 \le n, T \le 10^6$ và $1 \le a_i \le 10^8$.
**Subtask:**
- $25\%$ số điểm: $1 \le T \le 3$ và $1 \le n \le 10^5$.
- $25\%$ số điểm: $1 \le n \le 10^5$ và $1 \le a_i \le 10$.
- $50\%$ số điểm: Không có ràng buộc gì thêm.
### Mô hình hóa bài toán
Bài toán thực chất yêu cầu kiểm tra ==trong đoạn $[i, j]$ có giá trị nào xuất hiện lẻ lần== không.
### Subtask 1
Gọi $cnt_i$ là số lần xuất hiện của giá trị $i$. Để duy trì $cnt$, ta có thể sử dụng cấu trúc dữ liệu `map` được cung cấp trong C++ STL.
Với mỗi truy vấn, duyệt từ $i$ đến $j$ để cập nhật số lần xuất hiện của các giá trị.
Cuối cùng, kiểm tra xem có giá trị nào xuất hiện lẻ lần hay không.
**Độ phức tạp thời gian:** $O\left( T \times n \log n \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5;
int t, n, A[N+1];
bool check(int l, int r) {
map<int, int> mp;
for (int i = l; i <= r; i++)
mp[A[i]]++;
for (int i = l; i <= r; i++)
if (mp[A[i]] % 2 != 0)
return 0;
return 1;
}
int main() {
cin >> n >> t;
for (int i = 1; i <= n; i++) cin >> A[i];
while (t--) {
int l, r;
cin >> l >> r;
if (check(l, r)) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
```
:::
### Subtask 2
Kế thừa tư tưởng từ subtask trước.
>[!Tip] Nhận xét
>- Có quá nhiều truy vấn, cần cải tiến việc tính số lần xuất hiện của các giá trị.
>- Lợi dụng đề cho $a_i \le 10$, ta có thể sử dụng [mảng cộng dồn (prefix sum)](https://wiki.vnoi.info/algo/data-structures/prefix-sum-and-difference-array.md).
Gọi $S_{x, i}$ là số lần xuất hiện của giá trị $x$ từ $1$ đến $i$.
Với mỗi truy vấn $(i, j)$, ta duyệt qua các giá trị $x$, kiểm tra số lần xuất hiện của $x$ trong đoạn trên là $S_{x, j} - S_{x, i-1}$ có thỏa mãn hay không.
**Độ phức tạp thời gian:** $O\left( n + T \times 10 \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5;
int t, n, A[N+1], S[11][N+1] ;
bool check(int l, int r) {
for (int x = 1; x <= 10; x++)
if ((S[x][r] - S[x][l-1]) % 2 != 0)
return 0;
return 1;
}
int main() {
cin >> n >> t;
for (int i = 1; i <= n; i++) cin >> A[i];
for (int x = 1; x <= 10; x++)
for (int i = 1; i <= n; i++) {
S[x][i] = S[x][i-1];
if (A[i] == x) S[x][i]++;
}
while (t--) {
int l, r;
cin >> l >> r;
if (check(l, r)) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
```
:::
### Subtask 3
>[!Note] Thuật toán
> Bài này yêu cầu sử dụng kỹ thuật [**XOR hashing**](https://codeforces.com/blog/entry/85900).
>
> Sau đây, lời giải sẽ sử dụng ký hiệu $\oplus$ thay cho phép XOR.
Ta có thể khai thác tính chất của phép XOR:
$$
\begin{flalign}
\underbrace{x \oplus x \oplus x \oplus \dots \oplus x}_{cnt_x \bmod 2 = 0} = 0 \\
\underbrace{x \oplus x \oplus x \oplus \dots \oplus x}_{cnt_x \bmod 2 = 1} = x
\end{flalign}
$$
Ngoài ra, khi ta cần lấy tổng XOR của một đoạn $[i, j]$, ta có:
$$
a_i \oplus a_{i+1} \oplus \dots \oplus a_j = (a_1 \oplus a_2 \oplus \dots \oplus a_j) \oplus (a_1 \oplus a_2 \oplus \dots \oplus a_{i-1})
$$
Một đoạn $[i, j]$ ==có thể== gồm toàn các giá trị xuất hiện chẵn lần nếu như
$$
a_i \oplus a_{i+1} \oplus \dots \oplus a_j = 0
$$
:::danger
**Lưu ý:** Thuật toán trên không **luôn luôn đúng**, vì có thể xảy ra **collision** (nói đơn giản là trùng giá trị).
**Ví dụ:** $(2 \oplus 5 = 7) \Rightarrow 2 \oplus 5 \oplus 7 = 7 \oplus 7 = 0$.
Để **hạn chế collision**, ta kết hợp **hashing**, nghĩa là gán mỗi giá trị $x$ thành một số ngẫu nhiên. Bạn đọc nên tìm hiểu thêm qua bài viết ở trên.
:::
**Độ phức tạp thời gian:** $O\left( T + n \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
mt19937_64 rng(time(0));
const int N = 1e6;
int t, n, A[N+1], S[N+1];
map<int, int> val;
int main() {
cin >> n >> t;
for (int i = 1; i <= n; i++) {
cin >> A[i];
if (!val.count(A[i])) val[A[i]] = rng();
A[i] = val[A[i]];
}
for (int i = 1; i <= n; i++)
S[i] = S[i-1] ^ A[i];
while (t--) {
int l, r;
cin >> l >> r;
if ((S[r] ^ S[l-1]) == 0) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
```
:::
## Bài 4: Lắp ráp thiết bị (5 điểm)
### Tóm tắt đề bài
Cho $n$ bộ nguồn với công suất lần lượt là $p_1, p_2, \dots, p_n$.
Cho $k$ thiết bị cần mức công suất lần lượt là $t_1, t_2, \dots, t_n$.
Nếu thiết bị $i$ được gắn với bộ nguồn $j$ thì giá trị $| p_i - t_j |$ được gọi là độ gây hại của thiết bị này.
**Yêu cầu:** Chọn ra $k$ bộ nguồn và ghép với $k$ thiết bị sao cho tổng độ gây hại là nhỏ nhất.
**Dữ liệu đảm bảo:**
- $1 \le k \le n \le 10^3$;
- $1 \le t_i \le 10^2$;
- $1 \le p_j \le 10^2$.
**Subtask:**
- $25\%$ số điểm: $1 \le k \le n \le 20$;
- $75\%$ số điểm: Không có ràng buộc gì thêm.
### Subtask 1
Áp dụng thuật toán quay lui, tạo từng bộ $k$ trong $n$ bộ nguồn rồi lại ghép với $k$ thiết bị.
::: danger
**Độ phức tạp thời gian theo cách trên:** $O\left(P^k_n\right)$.
Trong đó $P^k_n$ là chỉnh hợp chập $k$ của $n$ có công thức là $\frac{n!}{(n-k)!}$
Với độ phức tạp như trên ta chưa thể lấy được trọn điểm của subtask này.
:::
>[!Tip] Nhận xét
> Với mỗi cặp bộ nguồn $(i,j)$ thỏa $t_j \ge t_i$ và mỗi cặp thiết bị $(u,v)$ thỏa $p_u \le p_v$, ta ==luôn ghép thiết bị $i$ với bộ nguồn $u$ và thiết bị $j$ với bộ nguồn $v$== để cực tiểu hóa **độ gây hại**.
>[!Important] Giải thích tính đúng đắn của nhận xét trên
> Ta có **độ gây hại** của hai thiết bị lần lượt là:
>- $S_1 = |t_i - p_u| + |t_j - p_v|$
>- $S_2 = |t_i - p_v| + |t_j - p_u|$
>
>Ta xét hiệu của $S_1$ và $S_2$:
>$$
>S_2 - S_1 =(|t_i - p_v| + |t_j - p_u|) - (|t_i -p_u| + |t_j - p_v|)
>$$
>Do $t_i \le t_j$ và $p_u \le p_v$, nên ta chỉ xét hai trường hợp:
>:::info
>**Trường hợp 1: $t_i \le p_u \le p_v \le t_j$**
>- Khi đó, ta có:
> - $S_1 = p_u - t_i + t_j - p_v$
> - $S_2 = p_v - t_i + t_j - p_u$
>- Vậy hiệu của $S_1$ và $S_2$ là:
>$$
>\begin{flalign}
>S_2 - S_1 &= (p_v - t_i + t_j - p_u) - (p_u - t_i + t_j - p_v) \\
>&= 2(p_v - p_u) \ge 0 \\
>\Rightarrow S_1 \le S_2
>\end{flalign}
>$$
>:::
>:::info
>**Trường hợp 2: $p_u \le t_i \le t_j \le p_v$**
>- Khi đó,ta có:
> - $S_1 = t_i - p_u + p_v - t_j$
> - $S_2 = p_v - t_i + t_j - p_u$
>
>- Vậy hiệu của $S_1$ và $S_2$ là:
>$$
>\begin{flalign}
>S_2 - S_1 &= (p_v - t_i + t_j - p_u) - (t_i - p_u + p_v - t_j) \\
>&= 2(t_j - t_i) \ge 0 \\
>\Rightarrow S_1 \le S_2
>\end{flalign}
>$$
>:::
>:::success
>Vậy nhận xét trên ==đúng trong mọi trường hợp==.
>:::
Với nhận xét trên, ta có thể giải bài toán này như sau:
- Sắp xếp các giá trị của thiết bị và bộ nguồn theo giá trị tăng dần.
- Nếu thiết bị $i$ ghép với bộ nguồn $u$ thì ta luôn ghép thiết bị $j$ $(i \le j \le k)$ với bộ nguồn $v$ $(u \le v \le n)$.
> Nếu coi $i, j$ và $u, v$ nằm trên hai đường thẳng, việc ghép là nối hai điểm lại với nhau, thì sẽ không có đoạn thẳng nào cắt nhau.
**Độ phức tạp thời gian:** $O \left( C^k_n \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3, MX = 1e18;
int res = MX, n, k, P[N+1], T[N+1];
void backTrack(int id, int last, int cost) {
if (cost > res) return;
if (id > k) {
res = min(res, cost);
return;
}
for (int i = last + 1; i <= n; i++)
backTrack(id+1, i, cost + abs(P[i] - T[id]));
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> P[i];
for (int i = 1; i <= k; i++) cin >> T[i];
sort(P+1, P+1+n);
sort(T+1, T+1+k);
backTrack(1, 0, 0);
cout << res;
return 0;
}
```
:::
### Subtask 2
Kế thừa tư tưởng của subtask trước, nhưng ta áp dụng ==quy hoạch động==.
- **Định nghĩa:** ==$f_{i,j}$== là tổng độ gây hại nhỏ nhất khi xét $j$ bộ nguồn đầu tiên và đã ghép nối được cho $i$ thiết bị đầu tiên.
- **Khởi gán:** ==$f_{0,j}=0$ $(0 \le j \le n$)== vì để ghép nối $0$ thiết bị thì độ gây hại luôn là $0$.
- **Công thức:**
$$
f_{i,j} = \min\left(f_{i,j-1},f_{i-1,j-1}+|b_i - a_j| \right)
$$
- $f_{i,j} = f_{i,j-1}$ khi không sử dụng bộ nguồn $j$
- $f_{i,j} = f_{i-1,j-1} + |b_i - a_j|$ khi thiết bị $i$ được ghép với bộ nguồn $j$.
**Độ phức tạp thời gian:** $O\left(nk\right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3;
int n, m, k;
int p[N+5], t[N+5], f[N+5][N+5];
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
for (int i = 1; i <= k; i++) {
cin >> t[i];
}
sort(p+1, p+1+n);
sort(t+1, t+1+k);
//60 ~ INF
for (int i = 0; i <= k; i++)
for (int j = 0; j <= n; j++)
f[i][j] = 1e9;
for (int i = 0; i <= n; i++){
f[0][i] = 0;
}
for (int i = 1; i <= k; i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = min(f[i][j-1], f[i-1][j-1] + abs(t[i] - p[j]));
}
}
cout << f[k][n];
return 0;
}
```
:::