# Hướng dẫn giải đề thi đầu vào THPT Chuyên Lam Sơn - Thanh Hóa ## Bài 1 : Tính tổng ### Cách làm: #### Subtask 1 : $L \le R \le 10 ^ 4$ Ở subtask này, ta chỉ cần duyệt for và tính tổng các số chẵn. **Code:** ```c++ #include<bits/stdc++.h> using namespace std; int main(){ int L , R; cin >> L >> R; int res = 0; for(int i = L ; i <= R ; i++) if(i % 2 == 0) res += i; cout << res << endl; return 0; } ``` #### Subtask 2 : $L \le R \le 2 * 10 ^ 9$ Ở subtask này thì ta cần tính tổng này bằng 1 cách nhanh hơn. Ta sẽ sử dụng công thức tính tổng từ $1$ đến $n$ là $\frac{n * (n + 1)}{2}$. Ta có ví dụ như sau: $$2 + 4 + 6 + 8 + ... + n$$ $$= 2 * (1 + 2 + 3 + 4 + .... + n / 2)$$ Tức ta sẽ biến đổi bài toán thành tính tổng từ $\lceil \frac{L}{2} \rceil$ đến $\lfloor \frac{R}{2} \rfloor$ và nhân $2$ vào để thành tổng các số chẵn từ $L$ đến $R$. **Lưu ý:** Ta sẽ làm tròn lên và làm tròn xuống. **Code :** ```c++ #include<bits/stdc++.h> using namespace std; long long sum(int x){ return x * (x + 1) / 2; } int main(){ int L , R; cin >> L >> R; int res = 0; int X = (L + 1) / 2; int Y = R / 2; cout << 2 * (sum(Y) - sum(X - 1)); return 0; } ``` ## Bài 2: Nguyên tố ### Cách làm: #### Subtask 1: $a \le b \le 200$ , $T \le 10^3$ Ở subtask đầu tiên, ta sẽ làm với cách duyệt trâu mọi thứ. Bao gồm duyệt ước trong $O(x)$ và kiểm tra số nguyên tố trong $O(x)$. Tổng quát lại thì độ phức tạp của ta sẽ là $\approx O(B^2 * T)$ ```c++ #include<bits/stdc++.h> using namespace std; bool isPrime(int x){ if(x <= 1) return false; for(int i = 1 ; i <= x ; i++) if(x % i == 0) return false; return true; } int countDiv(int x){ int cnt = 0; for(int i = 1 ; i <= x; i++) if(x % i == 0){ cnt++; } return cnt; } int main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); int numTest; cin >> numTest; while(numTest--){ int a , b; cin >> a >> b; int cnt = 0; for(int i = 1 ; i <= n ; i++) if(isPrime(countDiv)) cnt++; cout << cnt << "\n"; } } ``` #### Subtask 2: $a \le b \le 2000$ , $T \le 10^3$ Ở subtask này, ta sẽ cải tiến bằng 2 bước như sau: * Đếm số lượng ước số của $x$ trong $O(\sqrt{x})$. * Kiểm tra số nguyên tố trong $O(\sqrt{x})$. Có 2 bước cải tiến này, độ phức tạp của ta sẽ giảm xuống còn $O(B * \sqrt{B} * T)$, đủ để pass subtask thứ $2$. **Code:** ```c++ #include<bits/stdc++.h> using namespace std; bool isPrime(int x){ if(x <= 1) return false; for(int i = 1 ; i * i <= x ; i++) if(x % i == 0) return false; return true; } int countDiv(int x){ int cnt = 0; for(int i = 1 ; i * i <= x; i++) if(x % i == 0){ cnt += 2; if(cnt == cnt / i) cnt--; } return cnt; } int main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); int numTest; cin >> numTest; while(numTest--){ int a , b; cin >> a >> b; int cnt = 0; for(int i = 1 ; i <= n ; i++) if(isPrime(countDiv)) cnt++; cout << cnt << "\n"; } } ``` #### Subtask 3: $a \le b \le 10^6$ , $T \le 10^5$ Ở subtask này, các giới hạn được đẩy lên rất lớn nên ta sẽ có các bước tối ưu như sau: * Ta sẽ đếm ước cho tất cả số $x$ với $1 \le x \le 10^6$ trong $O(n * \log{n})$ thay vì duyệt từng số và đếm số lượng ước. * Sau khi đếm số lượng ước cho mỗi số, ta sẽ chuẩn bị một mảng cộng dồn $Pref$ với $Pref_i$ là số lượng số có số ước là một số nguyên tố trong khoảng từ $1$ đến $i$. ```c++ #include<bits/stdc++.h> using namespace std; const int maxv = 1e6; int cntDiv[maxv + 1]; // cntDiv[i] là số lượng ước số của i int Pref[maxv + 1]; void prep(){ for(int i = 1 ; i <= maxv ; i++){ for(int j = i ; j <= maxv ; j += i) cntDiv[j]++; } Pref[1] = 0; for(int i = 2 ; i <= maxv ; i++){ int t = cntDiv[i]; // t là số lượng ước của i if(cntDiv[t] == 2) // kiểm tra t có phải là số nguyên tố hay không Pref[i] = Pref[i - 1] + 1; else Pref[i] = Pref[i - 1]; } } int main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); prep(); int numTest; cin >> numTest; while(numTest--){ int a , b; cin >> a >> b; cout << Pref[b] - Pref[a - 1] << "\n"; } } ``` ## Bài 3: Tìm số ### Cách làm: #### Subtask 1: $T = 1 , a = b , N \le 2 * 10^9$ Ở subtask này, vì $a = b$ nên dãy số sinh ra sẽ chỉ là các bội số của $a$. Tức là dãy số sinh ra sẽ là: $$a , 2*a , 3*a , 4*a, .... , i*a , .... , n*a$$ Tức lúc này, số ta cần tìm là $N * a$. Tuy nhiên, quan sát giới hạn, ta thấy $N \le 2 * 10^9 , a \le 10^5$ nên giá trị tối đa có thể nhận là $N * a = 2 * 10^{14}$ nên ta sẽ sử dụng **long long** thay vì int. **Code:** ```c++ #include<bits/stdc++.h> using namespace std; int main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); int numTest; cin >> numTest; long long a , b , N; cin >> a >> b >> N; cout << N * a; } ``` #### Subtask 2: $T = 1 , a \neq b , N \le 10^4$ Ở subtask này, ta sẽ liệt kê các số theo thứ tự tăng dần bằng cách làm việc với **Bội chung nhỏ nhất - LCM**. Ta biết, các bội số của $a$ là $i * a$ còn các bội số của $b$ là $j * b$. Tại vị trí $(i , j)$ mà $i * a = j * b$ thì đó là bội chung của $a$ và $b$. Cứ mỗi bội chung như vậy sẽ tạo ra một vòng lặp và ta có thể xác định được giá trị dựa vào vòng lặp tìm được. **Code:** ```c++ #include<bits/stdc++.h> using namespace std; #define int long long int __lcm(int a , int b){ return a / __gcd(a , b) * b; } int32_t main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); int numTest; cin >> numTest; long long a , b , N; cin >> a >> b >> N; vector<int> loop; int LIM = __lcm(a , b); int t = a; while(t < LIM){ loop.push_back(t); t += a; } t = b; while(t < LIM){ loop.push_back(t); t += b; } loop.push_back(LIM); int K = N / loop.size(); N %= loop.size(); if(N == 0) cout << LIM * K << endl; else cout << LIM * K + loop[N - 1] << endl; } ``` #### Subtask 3: Không có ràng buộc gì thêm - Ở subtask này, ta sẽ sử dụng kĩ thuật **tìm kiếm nhị phân**. Vậy thì làm sao để sử dụng tknp? Đầu tiên, ta sẽ có công thức để tìm số lượng số chia hết cho $X$ mà bé hơn hoặc bằng $Y$ là $\frac{X}{Y}$. - Gọi $x$ là giá trị ta tìm nhị phân, ta có thể đếm được số lượng giá trị đã được liệt kê (gọi là $S_x$) bằng công thức: $$S_x =\frac{x}{a} + \frac{x}{b} - \frac{x}{LCM(a , b)}$$ - Công thức trên được lý giải bằng bao hàm loại trừ. - Có được công thức trên, việc của ta là tìm $x$ **nhỏ nhất** sao cho $S_x = N$. **Code:** ```c++ #include<bits/stdc++.h> using namespace std; #define int long long int __lcm(int a , int b){ return a / __gcd(a , b) * b; } int count(int x , int a , int b){ return x/a + x/b - x/lcm(a , b); } int32_t main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); int numTest; cin >> numTest; while(numTest--){ int a , b , N; cin >> a >> b >> N; int l = 0 , r = 1e18 , res = -1; while(l <= r){ int mid = (l + r) / 2; if(count(mid , a , b) >= N){ r = mid - 1; res = mid; }else l = mid + 1; } cout << res << endl; } } ``` ## Bài 4: Đảo xâu ### Cách làm #### Subtask 1: $|S| \le 10^3$ và $m \le 10^3$. Ở subtask này, ta chỉ cần làm trâu. **Code :** ```c++ #include<bits/stdc++.h> using namespace std; string s; int32_t main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin >> s; int n = s.size(); s = '*' + s; int mid = n / 2 + 1; int m; cin >> m; while(m--){ int x; cin >> x; for(int i = mid ; i <= x ; i++) swap(s[i] , s[n - i + 1]); } for(int i = 1 ; i <= n ; i++) cout << s[i]; cout << endl; } ``` #### Subtask 2: Không có ràng buộc gì thêm - Ở subtask này, ta sẽ sử dụng **Prefix Sum - Mảng cộng dồn** để AC. - Gọi $f_i$ là số lần mà vị trí $i$ phải thực hiện đổi chỗ. Nếu với mỗi truy vấn ta thực hiện việc tăng $f_i$ thêm $1$ với mọi vị trí $i$ trong khoảng $i = mid \rightarrow x$. Tức, thay vì mỗi lần ta thực hiện swap với mỗi vị trí $i$ thì ta chỉ tăng $f_i$, rồi sau khi thực hiện tất cả truy vấn thì ta mới thực hiện hoán đổi nếu $f_i$ lẻ - tức là những vị trí bị hoán đổi chẵn lần không thay đổi. Lúc này, quá trình của ta như sau: ```c++ int m; cin >> m; while(m--){ int x; cin >> x; for(int i = mid ; i <= x ; i++) f[i]++; } for(int i = mid ; i <= n ; i++) if(f[i] % 2 == 1) swap(s[i] , s[n - i + 1]); for(int i = 1 ; i <= n ; i++) cout << s[i]; cout << endl; ``` Như ta thấy đoạn code: ```c++ for(int i = mid ; i <= x ; i++) f[i]++; ``` Có thể được tối ưu bằng **Prefix sum - mảng cộng dồn**. Nên lúc này, code của ta sẽ như sau: ```c++ #include<bits/stdc++.h> using namespace std; string s; int f[2000005]; int32_t main(){ ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin >> s; int n = s.size(); s = '*' + s; int mid = n / 2 + 1; int m; cin >> m; while(m--){ int x; cin >> x; f[x + 1]--; f[mid]++; } for(int i = mid ; i <= n ; i++){ f[i] = f[i - 1] + f[i]; if(f[i] % 2 == 1) swap(s[i] , s[n - i + 1]); } for(int i = 1 ; i <= n ; i++) cout << s[i]; cout << endl; } ```