# THI THỬ HSG TỈNH K10+11 - 2025 - II
>[!Note] Thông tin
>Sau đây là lời giải của đề thi thử Kỳ thi Chọn HSG tỉnh Bà Rịa - Vũng Tàu K10+11 lần II (được chuẩn bị theo **ma trận kiến thức của Sở GD&ĐT tỉnh Bà Rịa - Vũng Tàu**)
>
>**Thời gian:** 20:00 - 23:00 ngày 06/03/2025
>
>**Bạn đọc có thể nộp và chấm bài** [TẠI ĐÂY](https://chuyentinbrvt.online/contest/algi_mockhsg1011lan2_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: Liên lạc (4 điểm)
### Tóm tắt đề bài:
Cho ví trí của $n$ chiến binh trong mặt phẳng tọa độ $Ox$, chiến binh thứ $i$ ở vị trí $p_i$ và có thể liên lạc được với những người trong khoảng cách $\left[p_i-R,p_i+R\right]$.
**Yêu cầu:** Tìm $R$ bé nhất để mỗi chiến binh có thể liên lạc được với ==ít nhất $1$ chiến binh khác==.
**Subtask:**
- $25\%$ số điểm: $n \le 10^3$.
- $25\%$ số diểm: $n \le 10^5$ và $p_1 \le p_2 \le \dots \le p_n$.
- $50\%$ số diểm: $n \le 10^5$.
### Mô hình hóa bài toán:
Cho $n$ số nguyên $p_1, p_2, \dots, p_n$.
**Yêu cầu:** Tìm $R$ nhỏ nhất để với mỗi chỉ số $i$ luôn tìm được chỉ số $j \not = i$ thỏa ==$| p_i - p_j | \le R$==
>[!Tip]
>Gọi $R_i$ là khoảng cách $R$ nhỏ nhất để người lính thứ $i$ liên lạc được với ít nhất $1$ người khác
>
>Đáp án của bài toán khi đó là ==$\max(R_1,R_2,\dots,R_n)$==.
>
>:::success
>Như vậy, trọng tâm của bài là tìm $R_i$ với $i$ từ $1$ đến $n$.
>:::
### Subtask 1
Với mỗi phần tử $p_i$, duyệt qua mọi phần tử $p_j$ mà $j \not = i$, khi đó:
$$
R_i = \min(|p_i - p_j|)
$$
**Độ phức tạp thời gian:** $O \left( n^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int n, A[1005], ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
for (int i = 1; i <= n; i++) {
int R = INT_MAX;
for (int j = 1; j <= n; j++) {
if (i != j) {
R = min(R, abs(A[i] - A[j]));
}
}
ans = max(ans, R);
}
cout << ans;
}
```
:::
### Subtask 2
>[!Tip] Nhận xét
> Với mỗi $p_i$, giá trị $p_j$ để $|p_i - p_j|$ đạt giá trị nhỏ nhất là giá trị ==gần với== $p_i$ nhất.
Vì danh sách đã được sắp xếp, nên với mỗi $p_i$ thì giá trị $p_j$ tối ưu chỉ có thể rơi vào $p_{i+1}$ hoặc $p_{i-1}$ (hai số nằm kề $p_i$).
Như vậy, duyệt qua mọi $p_i$ với $i$ từ $1$ đến $n$, và tính:
$$
R_i = \min \left( p_i-p_{i-1}, p_{i+1}-p_i \right)
$$
**Độ phức tạp thời gian:** $O \left( n \right)$.
:::success
:::spoiler Code
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int n, A[100005], ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
ans = max(R[2] - R[1], R[n] - R[n-1])
for (int i = 2; i <= n-1; i++) {
int R1 = A[i] - A[i-1];
int R2 = A[i+1] - A[i];
ans = max(ans, min(R1, R2));
}
cout << ans;
}
```
:::
### Subtask 3
Kế thừa tư tưởng của subtask trước. Ta chỉ cần sắp xếp lại danh sách $p$, khi đó có thể áp dụng thuật toán y hệt.
**Độ phức tạp thời gian:** $O \left( n \log n \right)$.
:::success
:::spoiler Code
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int n, A[100005], ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
sort(A+1, A+1+n);
ans = max(R[2] - R[1], R[n] - R[n-1])
for (int i = 2; i <= n-1; i++) {
int R1 = A[i] - A[i-1];
int R2 = A[i+1] - A[i];
ans = max(ans, min(R1, R2));
}
cout << ans;
}
```
:::
## Bài 2: Chính phương (5 điểm)
### Tóm tắt đề bài:
Cho $n$ số nguyên dương $A_1, A_2, \dots, A_n$, đếm số cặp $i \lt j$ sao cho $A_i \cdot A_j$ là một ==số chính phương==.
**Subtask:**
- $50\%$ số điểm: $1 \le n \le 10^3$.
- $50\%$ số điểm: $1 \le n \le 3 \cdot 10^5$.
### Subtask 1
Dùng hai vòng lặp để xét mọi cặp $\left(i, j \right)$ và đếm số cặp có tích $A_i \cdot A_j$ là số chính phương.
>[!Tip]**Hướng dẫn kiểm tra số chính phương:**
>Vì ==số chính phương== là bình phương của một số nguyên, nên có thể kiểm tra một số $x$ có phải số chính phương hay không bằng cách kiểm tra $\sqrt{x}$ có nguyên hay không.
>
>:::success
>Tuy nhiên, trong Tin học thường hạn chế sử dụng kiểu số thức. Do đó có một cách kiểm tra nhanh gọn hơn: ==so sánh $\left \lfloor \sqrt{x} \right \rfloor ^ 2$ với $x$==.
>:::
>:::spoiler Hàm kiểm tra số chính phương mẫu
>```cpp=1
>bool Check(int x) {
> int tmp = floor(sqrt(x));
> if (tmp * tmp == x) {
> return true;
> }
> else {
> return false;
> }
>}
>```
>:::
**Độ phức tạp thời gian:** $O \left( n^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1003;
int n, A[N], res = 0;
bool check(int x) {
int tmp = floor(sqrt(x));
if (tmp * tmp == x) {
return true;
}
else {
return false;
}
}
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> A[i];
}
for (int i = 1; i <= n - 1; i++) {
for (int j = i+1; j <= n; j++) {
if (check(A[i] * A[j]) == true) {
res++;
}
}
}
cout << res;
return 0;
}
```
:::
### Subtask 2
>[!Tip] Nhận xét về số chính phương:
>Khi phân tích thừa số nguyên tố của $x$, ta có:
>$$
>\begin{flalign}
>&x = {p_1}^{a_1} \cdot {p_2}^{a_2} \cdot {p_3}^{a_3} \cdots {p_k}^{a_k} \\
>\Leftrightarrow \; &x^2 = \left({p_1}^{a_1} \cdot {p_2}^{a_2} \cdot {p_3}^{a_3} \cdots {p_k}^{a_k} \right)^2 \\
>\Leftrightarrow \; &x^2 = {p_1}^{2 \cdot a_1} \cdot {p_2}^{2 \cdot a_2} \cdot {p_3}^{2 \cdot a_3} \cdots {p_k}^{2 \cdot a_k}
>\end{flalign}
>$$
> Với $p_i$ là thừa số nguyên tố thứ $i$ và $a_i$ là số mũ của thừa số nguyên tố thứ $i$.
>:::success
>Như vậy, số chính phương có ==số mũ của các thừa số nguyên tố== đều là ==số chẵn==.
>:::
Giả sử khi phân tích thừa số nguyên tố của $A_i$, thu được $b_1, b_2, b_3, \dots, b_m$ là các thừa số có số mũ lẻ. Để tích $A_i \cdot A_j$ là một số chính phương thì $A_j$ cũng phải có cùng các thừa số nguyên tố có số mũ lẻ là $b_1, b_2, b_3, \dots, b_m$.
> Vì khi nhân $A_i$ với $A_j$, số mũ của của các thừa số nguyên tố sẽ được cộng vào:
> $$
> b_i^x \cdot b_i^y = b_i^{x+y}
> $$
> Nếu một trong hai số $x$ và $y$ lẻ, số còn lại cũng phải lẻ để đảm bảo $x + y$ chẵn.
Nói cách khác, $A_i$ và $A_j$ có cùng tích của các thừa số nguyên tố mũ lẻ $\left(b_1 \cdot b_2 \cdot b_3 \cdots b_m \right)$.
> Có thể đơn giản hóa như vậy vì $b_i$ là số nguyên tố, nên mỗi bộ sẽ cho ra tích khác nhau
>[!Important]
>Bài toán bây giờ đã trở thành đếm số cặp $i \lt j$ sao cho $f \left(A_i \right) = f \left(A_j \right)$, với $f\left(x\right)$ là tích các thừa số nguyên tố mũ lẻ của $x$.
Để phân tích thừa số nguyên tố của $n$ số nguyên $A_i$ thành các thừa số nguyên tố, theo cách thông thường sẽ có độ phức tạp là $O\left( \sum_{i=1}^n \sqrt{A_i}\right)$, dẫn đến chạy quá thời gian.
Giả sử ta có $E_i$ là ước nguyên tố nhỏ nhất của $i$. Khi đó, để phân tích thừa số nguyên tố của $A_i$, ta liên tục chia $A_i$ cho $E_{A_i}$ đến khi $A_i = 1$.
>VD: Phân tích thừa số nguyên tố của $n = 40$
> - $E_{40} = 2 \rightarrow n = \frac{40}{2} = 20$
> - $E_{20} = 2 \rightarrow n = \frac{20}{2} = 10$
> - $E_{10} = 2 \rightarrow n = \frac{10}{2} = 5$
> - $E_{5} = 5 \rightarrow n = \frac{5}{5} = 1$
>
> Như vậy, $n = 40 = 2^2 \cdot 5^2$.
Vì trong mỗi thao tác, ta chia $A_i$ cho ít nhất là $2$ nên số thao tác tối đa là $\log_2 A_i$.
>[!Tip] Hướng dẫn
> Việc thực hiện tính trước mảng $E$ thường được gọi là ==sàng ước nguyên tố==. Đúng như tên gọi của nó, phương pháp này áp dụng tư tưởng của thuật toán ==sàng nguyên tố==.
> :::success
> Cụ thể, ta duyệt $i$ từ $2$ đến $n$ (với $n$ là giá trị lớn nhất cần sàng tới), nếu $i$ chưa tìm được ước nguyên tố $(E_i = 0)$, ==$i$ là số nguyên tố==, và ta duyệt qua tất cả các bội của $j$ $\left( i, i \cdot 2, i \cdot 3, \dots \right)$ và đánh dấu $E_j = i$.
> :::
> Vì với mỗi số, ta duyệt qua các bội của nó, do đó thuật toán trên có độ phức tạp:
> $$
> O \left( \frac n1 + \frac n2 + \frac n2 \dots \right) \approx O \left( n \log n \right)
> $$
> :::warning
> Tuy nhiên, trong thực tế, thuật toán trên chạy nhanh hơn rất nhiều vì **ta chỉ duyệt qua bội của những số nguyên tố**.
> :::
Như vậy, ta duyệt qua $A_i$ với $i$ từ $1$ đến $n$, lưu mảng đánh dấu $\operatorname{cnt}$ với $\operatorname{cnt}_x$ là số lượng $f\left(A_j\right) = x$ với $1 \le u \lt i$.
>Với $x = {p_1}^{a_1} \cdot {p_2}^{a_2} \cdot {p_3}^{a_3} \cdots {p_k}^{a_k}$ và $f\left(x\right) = b_1 \cdot b_2 \cdot b_3 \cdots b_m$, ta có:
>$$
>\begin{flalign}
>\{b_1, b_2, b_3, \dots, b_m\} &\subset \{{p_1}, {p_2}, {p_3}, \dots, {p_k} \} \\
>\Rightarrow b_1 \cdot b_2 \cdot b_3 \cdots b_m &\le {p_1} \cdot {p_2} \cdot {p_3} \cdots {p_k}\\
>\Leftrightarrow f\left(x\right) \le &\; x \le 10^6
>\end{flalign}
>$$
>Sử dụng mảng đếm là khả thi, không cần sử dụng **map**.
Đáp án khi đó là tổng $\operatorname{cnt}_{A_i}$ với $i$ từ $1$ đến $n$.
**Độ phức tạp thời gian:** $O\left( \log A_1 + \log A_2 + \dots + \log A_n \right) = O\left( \sum_{i=1}^n \log{A_i}\right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, E[N], A[N], cnt[N], pw[N+1];
long long res;
//Sàng ước nguyên tố
void sieve() {
E[1] = 1;
for (int p = 2; p <= 1e6; p++) {
if (E[p] == 0) {
for (int j = p; j <= 1e6; j += p) {
E[j] = p;
}
}
}
}
int main(){
sieve();
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
vector<int> prime_factors;
while (A[i] > 1) {
pw[E[A[i]]]++;
prime_factors.push_back(E[A[i]]);
A[i] /= E[A[i]];
}
sort(prime_factors.begin(), prime_factors.end());
prime_factors.erase(unique(prime_factors.begin(), prime_factors.end()), prime_factors.end());
int odd = 1;
for (auto i : prime_factors) {
if (pw[i] % 2 == 1) {
odd *= i;
}
pw[i] = 0;
}
res += cnt[odd];
cnt[odd]++;
}
cout << res;
return 0;
}
```
:::
## Bài 3: Giải mã (6 điểm)
### Tóm tắt đề bài:
Cho dãy số nguyên gồm $n$ phần tử $A_1, A_2, \dots, A_n$. Biết rằng **độ lệch** một của đoạn con liên tiếp là hiệu lớn nhất giữa hai phần tử bất kỳ trong đoạn.
**Yêu cầu:** Tính giá trị của mọi đoạn con liên tiếp trong dãy. Đáp án chia dư cho $10^9+7$.
**Subtask:**
- $30\%$ số điểm: $n \le 10^3$.
- $30\%$ số điểm: $n \le 10^5$ và $A_i$ phân biệt.
- $20\%$ số điểm: $A_1 \le p_2 \le \dots \le A_n$.
- $20\%$ số điểm: $n \le 2 \cdot 10^6$.
### Mô hình hóa bài toán
Cho dãy số nguyên gồm $n$ phần tử $A_1, A_2, \dots, A_n$. Tính:
$$
\sum_{i = 1}^n \sum_{j = i}^n \left[ \max \left( A_i, A_{i+1}, \dots, A_j\right) - \min \left( A_i, A_{i+1}, \dots, A_j\right) \right] \bmod \left( 10^9 + 7 \right)
$$
### Subtask 1:
Dùng hai vòng lặp để duyệt qua tất cả các đoạn con liên tiếp $[i, j]$, sau đó cộng độ lệch của tất cả các đoạn con lại để được đáp án.
**Độ phức tạp thời gian:** $O \left( n^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3, MOD = 1e9+7;
int n, A[N+5];
long long ans;
main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> A[i];
}
for (int i = 1; i <= n; i++) {
int tmin = INT_MAX, tmax = INT_MIN;
for (int j = i; j >= 1; j--) {
tmin = min(tmin, A[j]);
tmax = max(tmax, A[j]);
ans += (tmax - tmin);
ans %= MOD;
}
}
cout << ans;
}
```
:::
### Subtask 2
>[!Tip] Ý tưởng "chìa khóa"
> Có thể thấy, bài toán đang xoay quanh việc tính cho **tất cả các đoạn con**. Rõ ràng đây là một phạm vi rất lớn.
>
>Thay vào đó, ta hãy thử tách nhỏ biểu thức cần tính:
>$$
>\begin{flalign}
&\;\sum_{i = 1}^n \sum_{j = i}^n \left[ \max \left( A_i, A_{i+1}, \dots, A_j\right) - \min \left( A_i, A_{i+1}, \dots, A_j\right) \right] \\
&= \sum_{i = 1}^n \sum_{j = i}^n \max \left( A_i, A_{i+1}, \dots, A_j\right) - \sum_{i = 1}^n \sum_{j = i}^n \min \left( A_i, A_{i+1}, \dots, A_j\right)
\end{flalign}
>$$
>
>Nghĩa là, ta tính tổng các **số lớn nhất trong các đoạn** và trừ đi tổng các **số nhỏ nhất trong các đoạn**.
>
>:::success
>Nhưng ta vẫn có thể đơn giản hóa bài toán hơn nữa về việc tính biểu thức:
>$$
>\sum_{i=1}^n A_i \cdot \operatorname{mx}_{A_i} - \sum_{i=1}^n A_i \cdot \operatorname{mn}_{A_i}
>$$
>Trong đó:
>- $\operatorname{mx}_{A_i}$ là **số lượng đoạn con chứa $A_i$ nhận $A_i$ là giá trị lớn nhất**.
>- $\operatorname{mn}_{A_i}$ là **số lượng đoạn con chứa $A_i$ nhận $A_i$ là giá trị nhỏ nhất**.
Như vậy, với mỗi số $A_i$, ta cần tìm $\operatorname{mx}_{A_i}$ và $\operatorname{mn}_{A_i}$.
:::info
Sau đây, lời giải sẽ chỉ hướng dẫn việc tính $\operatorname{mx}_{A_i}$, vì tính $\operatorname{mn}_{A_i}$ cũng tương tự.
:::
Nói cách khác, $\operatorname{mx}_{A_i}$ là số lượng đoạn con $[l, r]$ mà $\max \left( A_l, A_{l+1}, \dots, A_r \right) = A_i$ với $l \le i \le r$. Nhận thấy ==$u \le l \le r \le v$== với:
- $u$ là chỉ số nhỏ nhất thỏa $u \lt i$ và $A_u, A_{u+1}, \dots, A_{i-1} \lt A_i$.
- $v$ là chỉ số lớn nhất thỏa $v \gt i$ và $A_{i+1}, A_{i+2}, \dots, A_{v} \lt A_i$
Có thể tìm được hai số $u$ và $v$ này bằng **tìm kiếm nhị phân** kết hợp với **tìm max trên đoạn bằng bảng thưa (sparse table)** hoặc **đi bộ trên cây segment tree**.
Khi này, số lượng đoạn con nhận $A_i$ làm $\max$ là:
$$
\left(i - u + 1 \right)\cdot\left(v-i+1\right)
$$
**Độ phức tạp thời gian:** $O \left( n \log n \right)$.
:::success
:::spoiler Code (Sparse table)
```cpp=1
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5, MOD = 1e9+7;
int n, a[N+5], lg2[N+5], tmin[20][N+5], tmax[20][N+5];
long long ans = 0;
void build() {
for (int i = 1; i <= n; i++) {
tmin[0][i] = tmax[0][i] = a[i];
}
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
tmin[j][i] = min(tmin[j-1][i], tmin[j-1][i + (1 << (j-1))]);
tmax[j][i] = max(tmax[j-1][i], tmax[j-1][i + (1 << (j-1))]);
}
}
for (int i = 2; i <= n; i++) {
lg2[i] = lg2[i >> 1] + 1;
}
}
int get_max(int l, int r) {
int j = lg2[r - l + 1];
return max(tmax[j][l], tmax[j][r - (1 << j) + 1]);
}
int get_min(int l, int r) {
int j = lg2[r - l + 1];
return min(tmin[j][l], tmin[j][r - (1 << j) + 1]);
}
int max_left(int i) {
int l = 1, r = i, res = i;
while (l <= r) {
int m = (l+r)/2;
if (get_max(m,i) > a[i]) {
l = m+1;
}
else {
res = m;
r = m-1;
}
}
return res;
}
int max_right(int i) {
int l = i, r = n, res = i;
while (l <= r) {
int m = (l+r)/2;
if (get_max(i,m) > a[i]) {
r = m-1;
}
else {
res = m;
l = m+1;
}
}
return res;
}
int min_left(int i) {
int l = 1, r = i, res = i;
while (l <= r) {
int m = (l+r)/2;
if (get_min(m,i) < a[i]) {
l = m+1;
}
else {
res = m;
r = m-1;
}
}
return res;
}
int min_right(int i) {
int l = i, r = n, res = i;
while (l <= r) {
int m = (l+r)/2;
if (get_min(i,m) < a[i]) {
r = m - 1;
}
else {
res = m;
l = m + 1;
}
}
return res;
}
main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
build();
for (int i = 1; i <= n; i++) {
ans += 1LL * (max_right(i) - i + 1) * (i - max_left(i) + 1) % MOD * a[i] % MOD;
ans -= 1LL * (min_right(i) - i + 1) * (i - min_left(i) + 1) % MOD * a[i] % MOD;
}
cout << (ans % MOD + MOD) % MOD;
}
```
:::
### Subtask 3
Kế thừa tư tưởng của subtask trước.
Vì dãy $A$ đã được sắp xếp tăng dần nên dễ dàng nhận thấy:
- $\max$ và $\min$ trong đoạn $[l, r]$ lần lượt là $A_r$ và $A_l$.
- $A_i$ là $\max$ trong các đoạn $[1,i],[2,i],\dots,[i,i]$ (có ==$i$== đoạn).
- $A_i$ là $\min$ trong các đoạn $[i,i],[i,i+1],\dots,[i,n]$ (có ==$n-i+1$== đoạn).
:::success
Như vậy, **đáp án** của bài toán là **biểu thức** sau:
$$
\sum_{i=1}^n A_i \cdot i - \sum_{i=1}^n A_i \cdot (n - i + 1)
$$
:::
**Độ 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 MOD = 1e9+7;
int n;
long long sum, ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
long long a;
cin >> a;
ans += (a * i - a * (n - i + 1));
((ans %= MOD) += MOD) %= MOD;
//Trong trường hợp ans bị âm cần + MOD
}
cout << ans % MOD;
}
```
:::
### Subtask 4
Kế thừa tư tưởng từ subtask 2. Tuy nhiên, có hai vấn đề cần xử lý:
- Giới hạn dữ liệu lớn.
- $A_i$ không phân biệt.
>[!Tip] Về giới hạn dữ liệu
> Cần thực hiện thao tác tìm $u$ và $v$ bằng kĩ thuật [**monotonic stack**](https://www.youtube.com/watch?v=Dq_ObZwTY_Q) để giảm độ phức của các thao tác từ $O(\log)$ xuống còn $O(1)$.
>[!Tip] Về các giá trị trùng lặp
> Nếu vẫn giữ nguyên thuật toán như trên, sẽ dẫn đến tình trạng ==đếm dư==.
>
> Cụ thể, khi tính $\text{mx}_i$ và $\text{mx}_j$ với ==$A_i = A_j$== cùng là số lớn nhất trong ==một đoạn $[l, r]$==, đoạn đó sẽ vô ý bị xét đến hai lần.
>
> :::success
> Cách xử lý đơn giản nhất ở đây là ==đặt cận== cho $u$ hoặc $v$. Ban đầu, ta có:
>- $u$ là chỉ số nhỏ nhất thỏa $u \lt i$ và $A_u, A_{u+1}, \dots, A_{i-1} \lt A_i$.
>- $v$ là chỉ số lớn nhất thỏa $v \gt i$ và $A_{i+1}, A_{i+2}, \dots, A_{v} \lt A_i$
>
> Ta có thể đặt cận cho $v$ ($v$ không được mở rộng ra đến giá trị bằng $A_i$):
> - $u$ là chỉ số nhỏ nhất thỏa $u \lt i$ và $A_u, A_{u+1}, \dots, A_{i-1} \le A_i$.
> - $v$ là chỉ số lớn nhất thỏa $v \gt i$ và $A_{i+1}, A_{i+2}, \dots, A_{v} \lt A_i$
>
> Hoặc đặt cận cho $u$ ($u$ không được mở rộng ra đến giá trị bằng $A_i$):
> - $u$ là chỉ số nhỏ nhất thỏa $u \lt i$ và $A_u, A_{u+1}, \dots, A_{i-1} \lt A_i$.
> - $v$ là chỉ số lớn nhất thỏa $v \gt i$ và $A_{i+1}, A_{i+2}, \dots, A_{v} \le A_i$
> :::
> Thực hiện ==hoàn toàn tương tự== để tính ==$\text{mn}_i$==.
**Độ phức tạp thời gian:** $O(n)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6, MX = 1e9;
const long long M = 1e9 + 7;
long long n, A[N+2];
long long NLE[N+1], PLS[N+1];
//Next less (or equal) - Previous less (strict)
long long NGE[N+1], PGS[N+1];
//Next greater (or equal) - Previous greater (strict)
stack<long long> sta;
void clear() {
while (!sta.empty()) {
sta.pop();
}
}
long long add(long long a, long long b) {
return (a + b) % M;
}
long long sub(long long a, long long b) {
return ((a - b) % M + M) % M;
}
long long mul(long long a, long long b) {
return (a * b) % M;
}
main() {
cin >> n;
for (long long i = 1; i <= n; i++)
cin >> A[i];
//Initialize PLS
clear();
A[0] = -(MX + 1);
sta.push(0);
for (int i = 1; i <= n; i++) {
while (A[sta.top()] >= A[i]) {
sta.pop();
}
PLS[i] = sta.top();
sta.push(i);
}
//Initialize NLE
clear();
A[n+1] = -(MX + 1);
sta.push(n+1);
for (int i = n; i >= 1; i--) {
while (A[sta.top()] > A[i]) {
sta.pop();
}
NLE[i] = sta.top();
sta.push(i);
}
//Initialize PGS
clear();
A[0] = MX + 1;
sta.push(0);
for (int i = 1; i <= n; i++) {
while (A[sta.top()] <= A[i]) {
sta.pop();
}
PGS[i] = sta.top();
sta.push(i);
}
//Initialize NGE
clear();
A[n+1] = MX + 1;
sta.push(n+1);
for (int i = n; i >= 1; i--) {
while (A[sta.top()] < A[i]) {
sta.pop();
}
NGE[i] = sta.top();
sta.push(i);
}
//Find total value of MIN OF SUBARRAY
int totalMn = 0;
for (int i = 1; i <= n; i++) {
totalMn = add(totalMn, mul(mul(A[i], (NLE[i] - i)), (i - PLS[i])));
}
//Find total value of MAX OF SUBARRAY
int totalMx = 0;
for (int i = 1; i <= n; i++) {
totalMx = add(totalMx, mul(mul(A[i], (NGE[i] - i)), (i - PGS[i])));
}
//Result
cout << sub(totalMx, totalMn);
return 0;
}
```
:::
## Bài 4: Trò chơi của John (5 điểm)
### Tóm tắt đề bài:
Cho một bảng trò chơi gồm $n$ hàng và $n$ cột. Ô giao giữa hàng $i$ và cột $j$ có số điểm là $A_{i, j}$.
Tại một ô, người chơi có thể di chuyển đến $4$ ô kề cạnh. Số điểm khi đi từ một ô đến ô khác là số điểm nhỏ nhất trong trong tất cả các ô mà người chơi đi qua.
Trò chơi gồm $q$ màn, mỗi màn chơi cho biết ô xuất phát và ô đích phải đi đến (đảm bảo hai phòng phân biệt).
**Dữ liệu đảm bảo:** $1 \le n \le 400$, $1 \le q \le 3 \cdot 10^5$ và $1 \le A_{i, j} \le 10^9$.
**Yêu cầu:** Hãy tính số điểm lớn nhất có thể nhận được ở mỗi màn chơi.
**Subtask:**
- $50\%$ số điểm: $1 \le n, q \le 100$.
- $50\%$ số điểm: Không có ràng buộc gì thêm.
### Mô hình hóa bài toán:
Cho một đồ thị $n \cdot n$ đỉnh được cho dưới dạng bảng, mỗi đỉnh có cạnh nối đến $4$ đỉnh kề nó trên bảng. Khi đi từ đỉnh này đến đỉnh khác, số điểm người chơi nhận được là số điểm của ô có số điểm nhỏ nhất trên đường đi.
Cho $q$ truy vấn, mỗi truy vấn cho đỉnh xuất phát và đỉnh kết thúc, tìm số điểm lớn nhất người chơi có thể đạt được.
### Subtask 1
>[!Tip] Đánh số lại đồ thị
>Để dễ thực hiện các thao tác trên đồ thị, ta tiến hành ==đánh số== các ô trên bảng:
$$
\text{ID}_{x, y} = (x - 1) \cdot n + y
$$
Áp dụng thuật toán [**Dijkstra**](https://viblo.asia/p/thuat-toan-dijkstra-va-ung-dung-aWj53zgQl6m) suy biến để tìm **đường đi có số điểm lớn nhất** như sau:
- Gọi $\text{max_score}_i$ là số điểm lớn nhất khi xuất phát từ đỉnh đã cho và đến được đỉnh $i$.
- Ưu tiên duyệt đồ thị từ những đỉnh $i$ với $\text{max_score}_i$ lớn nhất, nghĩa là lưu thông tin này trong một cấu trúc dữ liệu sắp xếp tăng dần (priority_queue).
**Độ phức tạp thời gian:** $O \left( n^2 \log n^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 102;
int n, q, a[N][N];
int dx[] = {-1, 1, 0, 0};
int dy[] = { 0, 0, 1,-1};
vector <int> ed[N*N];
int max_score[N*N], w[N*N];
int id(int x, int y) {
return (x-1)*n + y;
}
void add_edge(int u, int v){
ed[u].push_back(v);
ed[v].push_back(u);
}
int dijkstra(int s, int t) {
memset(max_score, 0, sizeof max_score);
priority_queue < pair<int, int> > prq;
max_score[s] = w[s];
prq.push({max_score[s], s});
while (prq.size() > 0) {
int u = prq.top().second, score_u = prq.top().first;
prq.pop();
if (score_u < max_score[u]) continue;
for (int v : ed[u]) {
if (max_score[v] < min(max_score[u], w[v])) {
max_score[v] = min(max_score[u], w[v]);
prq.push({max_score[v], v});
}
}
}
return max_score[t];
}
int main(){
cin >> n >> q;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 0; k < 4; k++) {
int u = i+dx[k], v = j+dy[k];
if (u < 1 || u > n || v < 1 || v > n) {
continue;
}
add_edge(id(i, j), id(u, v));
}
w[id(i, j)] = a[i][j];
}
}
for (int i = 1; i <= q; i++) {
int x, y, u, v;
cin >> x >> y >> u >> v;
cout << dijkstra(id(x, y), id(u, v)) << '\n';
}
return 0;
}
```
:::
### Subtask 2
>[!Tip] Ý tưởng "chìa khóa"
> Ta dựng một **cây khung lớn nhất đặc biệt**, trong bài toán này nghĩa là cây khung với ==cạnh có trọng số nhỏ nhất là lớn nhất== (khác với định nghĩa **cây khung lớn nhất**).
>
> Việc dựng cây khung không khó. Ta sử dụng thuật toán [**DSU - Disjoint Set Union**](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union), sắp xếp lại tất cả các cạnh của đồ thị giảm dần theo trọng số và ưu tiên dùng những cạnh đầu tiên để ghép vào cây khung.
> > Trọng số của cạnh $(u, v)$ trong bài này là $\min(A_u, A_v)$
Khi này, đồ thị đã liên thông, dễ thấy đường đi từ $u$ đến $v$ bất kỳ ==tối ưu== chính là đường đi từ $u$ đến $v$ trên cây khung mình vừa dựng.
Để tìm được **số điểm** của đường đi, ta áp dụng **nhảy nhị phân** giống trong tìm kiếm LCA - tổ tiên chung gần nhất), độ phức tạp của mỗi thao tác là $O(\log n^2)$.
**Độ phức tạp thời gian:** $O \left( n^2 \log n^2 \right)$.
:::success
:::spoiler Code
```cpp=1
#include <bits/stdc++.h>
using namespace std;
const int N = 500, M = 1e5, X = __lg(N*N) + 1;
int n, q, A[N+1][N+1], mn[N+1][N+1][X+1], h[N+1][N+1];
pii root, goal, par[N+1][N+1], p[N+1][N+1][X+1];
vector<pii> pos[M+1], adj[N+1][N+1];
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
pii acs(pii u) {
if (par[u.fi][u.se] == u) return u;
else return par[u.fi][u.se] = acs(par[u.fi][u.se]);
}
void join(pii u, pii v) {
pii x = acs(u), y = acs(v);
if (x < y) swap(x, y);
par[y.fi][y.se] = x;
}
bool in(pii u) {
return (u.fi >= 1 && u.fi <= n && u.se >= 1 && u.se <= n);
}
void dfs(pii u, pii parent) {
for (pii v : adj[u.fi][u.se])
if (v != parent) {
h[v.fi][v.se] = h[u.fi][u.se] + 1;
p[v.fi][v.se][0] = u;
mn[v.fi][v.se][0] = min(A[u.fi][u.se], A[u.fi][u.se]);
dfs(v, u);
}
}
void init() {
for (int k = 1; (1 << k) <= n*n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
pii parent = p[i][j][k-1];
p[i][j][k] = p[parent.fi][parent.se][k-1];
mn[i][j][k] = min(mn[i][j][k-1], mn[parent.fi][parent.se][k-1]);
}
}
int get(pii u, pii v) {
if (h[u.fi][u.se] < h[v.fi][v.se]) swap (u, v);
int ret = min(A[u.fi][u.se], A[v.fi][v.se]);
int k = h[u.fi][u.se] - h[v.fi][v.se];
for (int i = 0; (1 << i) <= k; i++)
if (k & (1 << i)) {
ret = min(ret, mn[u.fi][u.se][i]);
u = p[u.fi][u.se][i];
}
if (u == v) return ret;
k = __lg(h[u.fi][u.se]);
for (int i = k; i >= 0; i--)
if (p[u.fi][u.se][i] != p[v.fi][v.se][i]) {
ret = min(ret, mn[u.fi][u.se][i]);
ret = min(ret, mn[v.fi][v.se][i]);
u = p[u.fi][u.se][i];
v = p[v.fi][v.se][i];
}
return min({ret, mn[u.fi][u.se][0],mn[v.fi][v.se][0]});
}
int main() {
cin >> n >> q;
vector<pair<int, pair<pii, pii>>> E;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
cin >> A[i][j];
par[i][j] = {i, j};
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
pii u = {i,j};
for (int k = 0; k < 4; k++) {
pii v = {u.fi + dx[k], u.se + dy[k]};
if (in(v))
E.push_back({min(A[u.fi][u.se], A[v.fi][v.se]), {u, v}});
}
}
sort(E.begin(), E.end(), greater<pair<int, pair<pii, pii>>>());
for (auto pr : E) {
pii u = pr.se.fi, v = pr.se.se;
if (acs(u) == acs(v)) continue;
join(u, v);
adj[u.fi][u.se].push_back(v);
adj[v.fi][v.se].push_back(u);
}
dfs({1, 1}, {0, 0});
init();
while (q--) {
cin >> root.fi >> root.se >> goal.fi >> goal.se;
cout << get(root, goal) << "\n";
}
return 0;
}
```
:::