---
# System prepended metadata

title: 'Đề HSG10-11 - BRVT - 2025: Editorial'

---

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