# HSG lớp 9 TPHCM năm 2024 - 2025 [toc] ## Bài 1: Sắp xếp (7 điểm) ### Subtask 1: Ở subtask này ta có $n \le 1000$, dùng thuật toán ==buble sort== như đề bài mô tả và đếm số thao tác đã thực hiện, làm như sau: - Duyệt qua mọi cặp phần tử kề nhau $a_i$ và $a_{i+1}$, đổi chỗ 2 phần tử này cho nhau và tăng số lần hoán đổi lên nếu $a_i \gt a_{i+1}$. - Lặp lại quá trình trên đến khi không còn thực hiện được nữa. **Độ phức tạp thời gian:** $O \left( n^2 \right)$. ### subtask 2: >[!Note]Nhận xét: >Chắc chắn tồn tại thao tác đổi chỗ hai phần tử $a_i$ và $a_j (i \lt j)$ nếu $a_i \gt a_j$. >Đáp án bài toán bây giờ chuyển thành đếm số cặp $i, j$ thỏa mãn $i < j$ và $a_i \gt a_j$. >[!Tip]thuật toán: >- Sử dụng cấu trúc dữ liệu ==segment tree== hoặc ==fenwick tree==. **Cách làm:** Xử lí bài toán offline, mỗi phần tử lưu dưới dạng $pair$ (lưu giá trị và chỉ số), $sort$ các phần tử lại theo thứ tự tăng dần của giá trị, nếu giá trị bằng nhau thì $sort$ tăng dần theo chỉ số. $\rightarrow$ như vậy, nếu duyệt các phần tử theo thứ tự từ $1$ đến $n$, muốn biết được phần tử $a_i$ lớn hơn bao nhiêu phần tử $a_j (i \le j)$, đáp án chính là số lượng phần tử có chỉ số lớn hơn chỉ số của $a_i$ mà đã duyệt qua trước đó (vì chắc chắn giá trị của $a_i$ là lớn nhất cho đến hiện tại). $\rightarrow$ ta có thể lưu và lấy giá trị đó bằng 1 trong 2 cấu trúc dữ liệu nói trên. **Độ phức tạp thời gian:** $O \left( n \log \right)$. :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> #define pii pair<int, int> #define fi first #define se second using namespace std; const int Mod = 1e9 + 7; const int N = 2e5 + 5; int n; pii a[N]; int t[N]; void up(int x){ while (x){ t[x]++; x -= x & -x; } } int get(int x){ int res = 0; while (x <= n){ res += t[x]; x += x & -x; } return res; } main(){ ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0); freopen("SAPXEP.inp","r",stdin); freopen("SAPXEP.out","w",stdout); cin >> n; for (int i = 1; i <= n; i++){ cin >> a[i].fi; a[i].se = i; } sort(a+1, a+1+n); long long res = 0; for (int i = 1; i <= n; i++){ res += get(a[i].se + 1); up(a[i].se); } cout << res; return 0; } ``` ::: ## Bài 2: Khu vực (6.5 điểm) ### subtask 1 và subtask 2: >[!Note]Nhận xét: >- được phép đổi 1 thẻ thành số bất kì, điều đó có nghĩa là số cần tìm phải là ước của ít nhất $n-1$ số trong dãy. >- Hay nói cách khác, đáp án là ==ước chung lớn nhất== của $n-1$ phần tử bất kì trong dãy. >[!Tip] Tính chất > $gcd(a, b, c) = gcd(gcd(a, b), c)$ Thứ cần tính bây giờ là $gcd$ của $n-1$ phần tử bất kì, nghĩa là sẽ có 1 phần tử không cần xét. Ta có thể duyệt qua mọi phần tử không cần xét đó, rồi lấy $gcd$ những phần tử còn lại. **Độ phức tạp thời gian** $O (n \log)$ >trong đó $\log$ là độ phức tạp của $\gcd$. ### subtask 3: >[!Tip]Cải tiến: >Sử dụng tư tưởng preffix và suffix, gọi $pre_i$ là $gcd(a_1, a_2, \cdots, a_i)$ và $suf_i$ là $gcd(a_i, a_{i+1}, \cdots, a_{n-1}, a_n)$. $\rightarrow$ nếu chọn không tính phần tử thứ $i$ thì gcd của $n-1$ phần tử còn lại là $gcd(pre_{i-1}, suf{i+1})$. **Độ phức tạp thời gian:** $O(n \log)$ :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> #define all(x) x.begin(), x.end() using namespace std; const int Mod = 1e9 + 7; const int N = 1e5 + 5; int n, a[N], pre[N], suf[N]; main(){ ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0); freopen("KHUVUC.inp","r",stdin); freopen("KHUVUC.out","w",stdout); cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) pre[i] = __gcd(pre[i-1], a[i]); for (int i = n; i >= 1; i--) suf[i] = __gcd(suf[i+1], a[i]); int res = 0; for (int i = 1; i <= n; i++) res = max(res, __gcd(pre[i-1], suf[i+1])); cout << res; return 0; } ``` ::: ## Bài 3: Giải đấu (6.5 điểm) ### subtask 1: với mỗi truy vấn $l, r$ ta duyệt qua các vị trí $i$ $(l \le i \le r)$, chia các phần tử từ $l$ đến $i$ cho đội thứ nhất, các phần tử từ $i+1$ đến $r$ cho đội thứ 2. Tính tổng ranking mỗi đội và lấy chệnh lệch nhỏ nhất. **Độ phức tạp thời gian:** $O(q \cdot n^2)$ ### subtask 2: thuật toán giống như subtask 1, cải tiến phần lấy tổng mỗi đội bằng ==preffix sum==. **Độ phức tạp thời gian:** $O(q \cdot n)$ ### subtask 3: >[!Note]Nhận xét: >Gọi $sum$ là tổng ranking các phần tử từ $l$ đến $r$. >Chênh lệch tổng của 2 đội là nhỏ nhất chỉ khi tổng của mỗi đội gần với $sum / 2$ nhất. Dùng ==tìm kiếm nhị phân== để tìm ra vị trí $pos$ tối ưu mà tại đó, ta chia các phần tử từ $l$ đến $pos$ cho đội thứ nhất và các phần từ tử $pos+1$ đến $r$ cho đội thứ hai. **Độ phức tạp thời gian:** $O(q \cdot \log n)$ :::success :::spoiler Code ```cpp = 1 #include <bits/stdc++.h> #define int long long using namespace std; const int Mod = 1e9 + 7; const int N = 1e6 + 5; int n, q, sum[N]; int getPos(int i, int l, int r, int sumRange){ int aim = sumRange / 2, pos; while (l <= r){ int m = l+r >> 1; if (sum[m] - sum[i-1] >= aim) pos = m, r = m-1; else l = m+1; } return pos; } main(){ ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0); freopen("GIAIDAU.inp","r",stdin); freopen("GIAIDAU.out","w",stdout); cin >> n >> q; for (int i = 1; i <= n; i++){ int x; cin >> x; sum[i] = sum[i-1] + x; } while (q--){ int l, r; cin >> l >> r; int sumRange = sum[r] - sum[l-1]; int pos = getPos(l, l, r, sumRange); int sumL = sum[pos] - sum[l-1], sumR = sum[r] - sum[pos]; int res = abs(sumL - sumR); pos--; sumL = sum[pos] - sum[l-1], sumR = sum[r] - sum[pos]; res = min(res, abs(sumL - sumR)); cout << res << '\n'; } return 0; } ``` :::