Hướng dẫn cho LQDOJ CUP 2022 - Round 1 - AILIME
Chép code từ bài hướng dẫn để nộp bài là hành vi có thể dẫn đến khóa tài khoản.
Authors:
Subtask \(1\) (\(40\%\) số điểm): \(n, q \leq 10^3\).
Tutorial
Một lớp học \(E\) chỉ thắng khi số lượng học sinh của lớp \(E\) chiếm hơn nửa số người tham gia.
Chứng minh trường hợp xấu nhất
Trong trường hợp xấu nhất, mọi lớp khác đều cùng hội đồng lớp \(E\), gọi lớp \(S\) là toàn bộ học sinh của các lớp \(E\) hợp lại.
Thế thì quá trình chơi sẽ chỉ có các trường hợp xảy ra như sau:
- Tới lượt của một bạn lớp \(S\) và bạn kế tiếp là lớp khác \(E\), thì sẽ bỏ lượt cho bạn tiếp theo cũng thuộc lớp \(S\)
- Tới lượt của một bạn lớp \(S\) và bạn kế tiếp là lớp \(E\), bạn đó sẽ loại một học sinh lớp \(E\) và lượt tiếp theo là của học sinh lớp \(E\)
- Tới lượt của một bạn lớp \(E\) và bạn kế tiếp là lớp khác \(S\), thì sẽ bỏ lượt cho bạn tiếp theo cũng thuộc lớp \(S\)
- Tới lượt của một bạn lớp \(E\) và bạn kế tiếp là lớp \(E\), bạn đó sẽ loại một học sinh lớp \(E\) và lượt tiếp theo là của học sinh lớp \(E\)
- Chỉ còn lại học sinh của lớp \(E\)
Và trong trường hợp xấu nhất này thì \(S\) cũng là người bắt đầu, và cứ mỗi \(2\) lượt thì \(S\) và \(E\) loại nhau \(1\) thành viên.
Mà ta có \(|E| > n - |E| = |S|\), nên sau \(2 \cdot |S|\) lượt thì \(|E| > |S| = 0\), hay lớp \(E\) là lớp duy nhất còn sót lại.
Một cách hiểu trực quan hơn là bài toán đưa một mảng \(n\) phần tử tối đa \(n\) phần tử khác nhau về thành dãy nhị phân, mỗi lần số \(0\) xoá số \(1\) thì số \(1\) xoá số \(0\) và ngược lại, nên kêt
Lưu ý
Đúng là có thể mỗi lớp có những chiến thuật khác nhau, tuy nhiên nếu nội bộ \(S\) không cùng thống nhất nhau mà lại có \(2\) lớp \(A, B \in S\) mà loại bỏ học sinh của nhau thì \(|S|\) sẽ giảm nhanh hơn nhiều nên \(E\) thắng nhanh hơn
Vì \(E\) đang chơi tối ưu nhất nên không dại gì phải bỏ lượt khi loại được một đối thủ bất kì, vì suy cho cùng dù có chọn loại học sinh của lớp nào trước thì lớp \(E\) vẫn bị các lớp còn lại hội đồng tấn công.
Trường hợp tốt nhất xảy ra khi mọi thành viên lớp \(S\) đều loại bỏ lẫn nhau và bỏ lượt khi gặp học sinh lớp \(E\).
Chứng minh trường hợp đặc biệt
-
Trường hợp 1: Khi mọi người tham gia đều là lớp \(E\) thì họ nghiễm nhiên đã chiến thắng theo định nghĩa
-
Trường hợp 2: Khi có lớp phá game bằng cách bỏ hết lượt chơi
Mặc dù bạn nghĩ nó là một lối chơi thú vị, khi mà ảnh hưởng tới lối chơi tối ưu của những team top đầu, bằng cách loại đi các team yếu không có khả năng chiến thắng.
Tuy nhiên, lớp \(E\) vẫn sẽ không bị ảnh hưởng bởi vì chiến thuật lớp \(E\) luôn loại đối thủ của mình bất chấp đối thủ có sử dụng chiến thuật nào đi nữa (lớp \(E\) không cần bỏ lượt vẫn tối ưu).
- Trường hợp 3: Không có lớp nào chiếm số áp đảo, thì không tồn tại một lớp luôn dành chiến thắng
Khi điều này xảy ra thì các lớp luôn có trường hợp thua hoặc có trường hợp lặp vô hạn do nếu loại bỏ đối thủ thì mình sẽ không tốt hơn top \(2\) được, ví dụ \(\{1, 2, 3\}\), nếu bạn loại \(1\) người thì người còn lại sẽ loại bạn để đạt top \(1\)
Và vì các lớp đều chơi tối ưu vì "Mọi người đều có tinh thần tham gia để đem về chiến thắng cho cả đội." nên không thể có trường hợp toàn bộ các lớp chung tay skip để cho một lớp duy nhất dành chiến thắng được.
Vậy nên với mỗi phần tử \(a_i\) trong đoạn \(a[l \ldots r]\) đang xét, ta cần phải tính hàm \(f(a_i) =\) số lần xuất hiện của \(a_i\) trong đoạn đó.
Nếu tồn tại \(a_i\) để \(f(a_i) > \left \lfloor \frac{r-l+1}{2} \right \rfloor\) là số thí sinh trong đoạn, thì lớp \(E = a_i\) chính là lớp duy nhất luôn dành chiến thắng, ngược lại không tồn tại lớp nào mà thắng trọn vẹn được cả, như đã chứng minh ở trên.
Có nhiều cách tính \(f(a_i)\) như sau:
-
Tạo mảng \(b[l \ldots r] = a[l \ldots r]\) rồi sắp xếp \(b[]\) trong \(O(|b| \log |b|)\), lúc này các phần tử bằng nhau sẽ kề nhau nên cần tìm đoạn dài nhất có các phần tử bằng nhau. Ví dụ trong C++ có hàm
std::sort -
Dùng các cấu trúc dữ liệu hỗ trợ việc sắp xếp hay duyệt các số theo thứ tự trong \(O(|b| \log |b|)\). Ví dụ trong C++ có
std::priority_queue,std::set,std::map -
Dùng các cấu trúc dữ liệu kiểu mảng băm để đếm số phần tử trùng nhau
std::hash,std::unordered_set,std::unordered_map -
Nén mảng \(a[1 \ldots n]\) (với từng số \(a_i\) lớn thứ \(p\) trong mảng sẽ gán lại thành \(p\)), và lưu vị trí xuất hiện của từng số \(a[i]\), khi truy vấn chỉ cần chặt nhị phân để đếm số phần tử trên đoạn \([l, r]\)
Solution
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 1005;
int n, numQuery;
long long a[MAX_N];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
while (numQuery--) {
int type;
cin >> type;
if (type == 1) {
int l, r, v;
cin >> l >> r >> v;
for (int i = l; i <= r; ++i) {
a[i] += v;
}
} else {
int left, right;
cin >> left >> right;
map<long long, int> cnt;
for (int i = left; i <= right; ++i) {
++cnt[a[i]];
}
bool noWinner = true;
for (pair<long long, int> e : cnt) {
long long value = e.first;
int occurence = e.second;
if (occurence > (right - left + 1) / 2) {
cout << value << "\n";
noWinner = false;
break;
}
}
if (noWinner) {
cout << "IMPOSSIBLE" << '\n';
}
}
}
return 0;
}
Subtask \(2\) (\(30\%\) số điểm): \(q \leq 5 \cdot 10^3\), chỉ bao gồm truy vấn loại \(2\).
Tutorial
Gọi số chiếm hơn nửa lần xuất hiện của tập là số áp đảo, ta có các nhận xét như sau
- Nếu số \(x\) là số áp đảo thì mọi bit của \(x\) cũng áp đảo tại đó.
- Nếu tồn tại một số là số áp đảo, thì xác xuất để một số bất kì là áp đảo là hơn \(50\) %
- Tồn tại cấu trúc dữ liệu để nếu tồn tại phần tử áp đảo, thì luôn tìm được giá trị đó.
- Tồn tại cấu trúc dữ liệu để kiểm tra xem nửa trái, hoặc nửa phải, có phần tử áp đảo, hay cả hai đều không có.
Vậy bài toán này mình có nhiều các khai thác để giải bài
Tutorial cách 1: Tách số về thành chuỗi bit
Ta có một nhận xét là, nếu một số xuất hiện hơn nửa số lần, và tồn tại số \(b = b_0\) để nó không áp đảo với bit thứ \(b_0\) của các số trong tập, thì số đó không phải là số áp đảo.
Vậy ta biến bài toán thành các bài toán độc lập nhau, mỗi bài toán xét trên số bit thứ \(b = 0 \rightarrow 60\) và tìm xem số áp đảo tại \(b\) là \(x_b = 0\) hay \(x_b = 1\). Khi này kết quả bài toán là \(\Sigma x_b \times 2^b\)
Giờ thì bài toán lại trở về các số \(0\) và \(1\), thế để kiểm tra xem số áp đảo là màu nào, thì ta có thể sử dụng quy hoạch động (không cập nhật) hoặc Fenwick Tree (có thể cập nhật) trong \(O(n \log n)\) cho đơn giản, ngắn và hằng số thấp.
Sau đó khi tính kết quả thực tế của bài toán, ta phải kiểm tra lại xem nếu nó xuất hiện quả nửa số phần tử trong mảng chưa, nếu đúng là thế thì nó là số áp đảo.
Để kiểm tra số lần xuất hiện của một số trên đoạn, ta có thể lưu các vị trí xuất hiện của số đó trong mảng và sử dụng chặt nhị phân để tìm chặn trên chặn dưới.
Cách này sẽ có độ phức tạp là \(O(n \log n \times \log_2 a)\) với \(a\) là giới hạn của các giá trị trong mảng và \(n\) là số phần tử trong mảng.
Ví dụ
- Số \(1\) là số áp đảo trong tập \(\{1\}\) vì nó là phần tử duy nhất
- Số \(2\) không phải là số áp đảo trong tập \(\{1, 2\}\) vì xét bit tại \(b = 0\) (cơ số \(2^0 = 1\)) ta có cả hai bit đều không phải là bit áp đảo trong dãy \(\{1, 0\}\)
- Số \(3\) không phải là số áp đảo trong tập \(\{1, 2, 3, 3\}\) vì nó xuất hiện không quá nửa lần.
- Số \(5\) không phải là số áp đảo trong tập \(\{1, 2, 3, 3, 5, 5, 5\}\) vì xét bit tại \(b = 2\) (cơ số \(2^2 = 4\)) ta có cả hai bit đều không phải là bit áp đảo trong dãy \(\{0, 0, 0, 0, 1, 1, 1\}\)
- Số \(8\) không phải là số áp đảo trong tập \(\{1, 2, 3, 3, 5, 5, 5, 8, 8, 8, 8, 8\}\) vì xét bit tại \(b = 0\) (cơ số \(2^0 = 1\)) ta có cả hai bit đều không phải là bit áp đảo trong dãy \(\{1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0\}\)
- Số \(7\) là số áp đảo trong tập \(\{1, 3, 3, 5, 5, 5, 7, 7, 7, 7, 7, 7, 7\}\) vì số \(7_{10} = 111_{2}\) áp đảo trên từng hệ nhị phân của nó, và nó xuất hiện hơn nửa số lần.
Solution
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 1000005;
int n, numQuery;
int f[MAX_N][32];
long long a[MAX_N];
map<long long, vector<int> > mp;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
memset(f[0], 0, sizeof(f));
for (int i = 1; i <= n; ++i) {
cin >> a[i];
mp[a[i]].push_back(i);
for (int b = 0; b <= 30; ++b) {
f[i][b] = f[i - 1][b] + (a[i] >> b & 1);
}
}
while (numQuery--) {
int type, left, right;
cin >> type >> left >> right;
long long v = 0;
for (int b = 0; b <= 30; ++b) {
int cnt_1 = f[right][b] - f[left - 1][b];
int cnt_0 = (right - left + 1) - cnt_1;
v |= (long long)(cnt_1 > cnt_0) << b;
if (cnt_1 == cnt_0) {
v = -1;
break;
}
}
if (mp.count(v) == false) {
cout << "IMPOSSIBLE\n";
continue;
}
int lower = lower_bound(mp[v].begin(), mp[v].end(), left) - mp[v].begin();
int upper = upper_bound(mp[v].begin(), mp[v].end(), right) - mp[v].begin();
if (upper - lower > (right - left + 1) / 2) {
cout << v << '\n';
} else {
cout << "IMPOSSIBLE\n";
}
}
return 0;
}
Tutorial cách 2: Xử lí bài toán theo xác xuất
Ta có nhận xét là nếu một số là số áp đảo, thì nó phải xuất hiện hơn một nửa số lần, nên nếu ta bốc ngẫu nhiên một số bất kì thì phải có hơn \(50\)% xác xuất là bốc trúng số cần tìm.
Ví dụ
- Số \(1\) là số áp đảo, và duy nhất xuất hiện trong tập \({1, 1}\) nên xác xuất xuất hiện là \(100\)% > \(50\)%
- Số \(27\) là số áp đảo, và xuất hiện \(5\) lần trong tập \(\{2, 7, 27, 27, 27, 27, 27, 69, 420\}\) nên xác xuất xuất hiện là \(\frac{5}{9} = 0.555555556 > 50\)%
- Số \(2\) không phải là số áp đảo vì nó không hề xuất hiện trong tập \({1, 3, 5, 7, 9}\) nên xác xuất xuất hiện là \(0\)% < \(50\)%
- Số \(7\) không phải là số áp đảo vì nó xuất hiện không quả nửa số lần trong tập \(\{2, 7\}\), xác xuất xuất hiện là \(50\)% nhưng chưa hơn nửa.
Vậy với mỗi \(i = 1 \rightarrow n\) ưu vị trí xuất hiện của \(a_i\), ta bốc thử \(k\) lần số \(a_i\) ngẫu nhiên trên \(a[l \ldots r]\) và đếm số lần xuất hiện của \(a_i\).
Gọi tập \(S_x = \{p_1, p_2, \ldots, p_q\}\) là tập các vị trị mà ở đó \(a[p_i] = x\), thế thì để đếm số \(a_i\) trong đoạn \(a[l, r]\) ta chỉ cần đếm số \(p_i\) trong đoạn \([l, r]\)
Ta có thể đếm bằng chặt nhị phân đơn giản
Nếu với mọi \(a_i\) ta có tập \(S_x\) được sắp xếp tăng dần, thì nó tồn tại hàm đơn điệu $f(n) = $ số phần tử \(p_i \leq n\) như sau
Ví dụ với mảng \(S_x = \{1, 2, 3, 5, 8\}\) ta có:
Vậy ta chỉ cần chặt nhị phân là tìm được đoạn \(l \leq p_{x} < p_{x+1} < \ldots < p_{y} \leq r\) thì số phần tử giá trị đó xuất hiện là \(y - x + 1\), ngược lại là \(0\).
Lưu ý cẩn thận vị trí \(p_i < 0, p_i = x, p_i = y, p_i > y\) và cẩn thận với cách bạn code là mảng bắt đầu từ \(0\) hay từ \(1\)
Trong C++ có hàm lower_bound() sẽ giúp bạn tìm được \(x\) và hàm upper_bound() giúp bạn tìm được \(y + 1\) đơn giản như sau
int countOccurence(const vector<long long> &S, int l, int r) { /// S = Tập p[i] độ dài q
return (l > r) ? 0 : (upper_bound(S.begin(), S.end(), r) - lower_bound(S.begin(), S.end(), l));
}
Cách này sẽ có độ phức tạp là \(O(k \times \log n)\) với \(k\) chính là số lần chọn ngẫu nhiên \(a_i\), nếu xác xuất mỗi số bạn bốc trúng là đều nhau thì xác xuất để thuật đưa ra kết quả sai mỗi lần chọn \(a_i\) là \(\epsilon = \frac{1}{2^k}\)
Ghi chú
Nén mảng \(a[1 \ldots n]\) về thành mảng \(a'[1 \ldots n]\) sao cho \(a[i]\) chính là số lớn thứ \(a'[i]\) trong \(a[1 \ldots n]\). Bằng cách này thì độ phức tạp nhanh hơn nhưng có thể không đáng kể.
Ngoài ra cũng có thể tối ưu độ phức tạp lên \(O(k \times \log \sqrt{n})\) nhưng không đáng kể, vì hằng số bù lại vào cũng như nhau.
Solution
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 1000005;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
int n, numQuery;
long long a[MAX_N];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
map<long long, vector<int>> mp;
for (int i = 1; i <= n; ++i){
mp[a[i]].push_back(i);}
while (numQuery--) {
int opt, left, right;
cin >> opt >> left >> right;
bool answer = false;
for (int it = 1; it <= 27; ++it) {
long long v = a[uniform_int_distribution<int>(left, right)(rng)];
int lower = lower_bound(mp[v].begin(), mp[v].end(), left) - mp[v].begin();
int upper = upper_bound(mp[v].begin(), mp[v].end(), right) - mp[v].begin();
if (upper - lower > (right - left + 1) / 2) {
answer = true;
cout << v << '\n';
break;
}
}
if (!answer) {
cout << "IMPOSSIBLE\n";
}
}
return 0;
}
Tutorial cách 3: Dùng cấu trúc dữ liệu và kiểm tra lại
Mình dùng Segment Tree cơ bản, mỗi nút sẽ coi như đại diện cho một đội tốt nhất hiện tại, chỉ chứa hai thông tin là <mã màu áo> và <số thành viên>
Ý tưởng của mình là, nếu \(2\) bạn có cùng màu áo thì hợp \(2\) team với nhau, ngược lại thì từng cặp thành viên của hai nhóm sẽ bị loại dần tới khi chỉ còn một bên.
struct Node<int color, int count>
Node operator + (const Node &A, const Node &B) {
if (A.color == B.color) return Node(color, A.cnt + B.cnt);
if (A.cnt > B.cnt) return Node(A.color, A.cnt - B.cnt);
if (A.cnt < B.cnt) return Node(B.color, B.cnt - A.cnt);
return Node(any color, cnt = 0);
}
Nếu bạn duyệt từng team một từ \(a[L], a[L + 1], \ldots\) duyệt sang dần tới \(\ldots, a[R - 1], a[R]\) thì có \(2\) trường hợp có thể xảy ra
- \(a_i = color\) là số áp đảo
- Không có số áp đảo trên đoạn này
Chứng minh trực tiếp
Trên đoạn thì có nhiều màu áo khác nhau, người thứ \(i\) có màu áo \(a_i\)
Hãy giả sử là giờ chúng ta solo theo kiểu một đấu một với nhau.
Thế thì rõ ràng là khi hai nhóm cùng màu áo thì hợp lại với nhau để trở nên mạnh mẽ hơn
if (A.color == B.color) return Node(color, A.cnt + B.cnt);
Ngược lại nếu khác màu áo thì team nào đông thì thắng
if (A.cnt > B.cnt) return Node(A.color, A.cnt - B.cnt);
if (A.cnt < B.cnt) return Node(B.color, B.cnt - A.cnt);
return Node(any color, cnt = 0);
Nhưng như đã nhắc tới ở subtask \(1\), nếu màu áo \(E\) nào đó mà áp đảo, thì cho dù trong trường hợp xấu nhất là bị chơi hội đồng, và các lớp liên minh với nhau thành màu áo \(S\) nào đó, thì ta vẫn có \(|E| > n - |E| = |S|\)
Nên sau mỗi lần đấu đá nhau do khác màu áo, thì \(E\) mất \(x\) người cũng làm \(S\) mất \(x\) người. Nên sau khi toàn bộ các nhóm đấu nhau, thì \(S\) sẽ bị bay màu trước, bởi vì tại \(x = |S|\), có \(|E| - x > |S| = 0\)
Tuy nhiên, nếu không có màu áo áp đảo, thì kết quả trả về sẽ là một \(a_i\) ngẫu nhiên nào đó trên đoạn \(a[l \ldots r]\) hoặc là có giá trị <count> $ = 0$, nên vẫn cần phải kiểm tra lại bằng cách chặt nhị phân số vị trí của \(a_i\) trong đoạn \([l, r]\)
Tuy nhiên, bài toán ta đang xét có phẩn tổng quát hơn, đó là chúng ta sẽ sử dụng cấu trúc dữ liệu cây để giải bài nhanh hơn.
Chứng minh trên vẫn đúng, xong phải cẩn thận kẻo suy xét những vấn đề không cần thiết làm trực giác ta cảm giác nó không đúng lắm.
Ví dụ: Sau một quá trình đấu đá nhau trên cây nhị phân, còn lại \(2\) đỉnh là \(\text{<color }= E\text{, count }= X\text{>}\) với \(\text{<color }= S\text{, count }= Y\text{>}\) làm sao để chứng minh là khi biết \(E\) là phần tử áp đảo thì chắc chắn hệ quả \(X > Y\) phải thoả.
Chứng minh trên cây
Trong trường hợp xấu nhất mà ta xét, thì các lớp luôn liên minh với nhau để hội đồng nhóm áp đảo.
Nhưng trên cái cây của mình, mình không thể biết được ai là áp đảo cho tới khi đấu với nhau, nên sẽ có những lúc đấu đá nội bộ.
Thế nên cái cây chỉ là một trong các cách ghép cặp các team đấu đá với nhau, nên dù đảo đi đảo lại hay thế nào đi nữa, thì trong trường hợp xấu nhất (ví dụ xen kẽ \(E S E S E \ldots E S E\)) thì \(E\) sẽ luôn tồn tại cuối cùng nếu nó thực sự áp đảo.
Và tất nhiên nếu có giá trị khác may mắn chui qua được, thì vẫn phải qua bước kiểm tra số phần tử của nó ở trên đoạn \([l, r]\) nên nó vẫn đúng.
Còn về tại sao \(X > Y\) luôn đúng khi \(E\) áp đảo thì, trong trường hợp xấu nhất (tức là thay đổi cây tính toán một cách tuỳ ý sao cho \(E\) bị thiệt hại nhiều nhất), ta vẫn có \(X \geq X_{min} > Y_{max} \geq Y\).
Chứng minh phản chứng
Gọi \(K[x] = y\) nghĩa là \(x\) bị loại bởi \(y\) trong quá trình hợp cây lên.
Ban đầu thì có thể khởi tạo \(K[x] = x\) hoặc số nào đó khác đoạn \([L \ldots R]\)
struct Emilia {
int color;
vector<int> member;
};
thế thì cái hàm hợp \(2\) nút của chúng ta sẽ được viết lại thành như sau
Emilia Merge(Emilia L, Emilia R) {
if (L.color == R.color) {
R.emplace_back(L.cbegin() , L.cend());
return L;
}
if (L.member.size() == R.member.size()) {
return trash_value;
}
if (L.member.size() < R.member.size()) {
swap(L, R);
}
for (; R.size(); L.pop_back(), R.pop_back()) {
K[L.back()] = R.back();
K[R.back()] = L.back();
}
return L;
}
Giả sử \(Emilia\) là tập áp đảo, có mã màu là \(E\), và \(e_i\) là các thành viên của lớp \(E\) trên đoạn đang xét.
Thế thì ta phản chứng bằng cách giả sử \(K[e_i] \neq E\ \forall\ e_i \in {Emilia}\), nghĩa là hàm phải trả về giá trị khác \(E\) \(\dagger\)
Mà vì ta đấu đá lẫn nhau kiểu qua lại nên \(K_A = B \Leftrightarrow K_B = A\), tức là không thể tồn tại \(K_X = Y \rightarrow K_Y \neq X\) \(\ddagger\)
Gọi \(K_{ed}\) là tập các bạn \(e_i\) đã bị loại và \(K_{er}\) là tập các bạn đã loại \(e_i\).
Vì \(\dagger\) và \(\ddagger\) nên \(K_{ed} \cap K_{er} = \emptyset\)
Mà ta cũng có \(K[K_A] = A\) nên \(|K_{ed}| = |K_{er}|\)
Mà vì như ta đã giả sử từ \(\dagger\) là mọi \(e_i\) đều sẽ bị loại, nghĩa là \(|K_{ed}| = |E|\)
Vậy thì ta có \(|K_{ed} \cup K_{er}| = |K_{ed}| + |K_{er}| - |K_{ed} \cap K_{er}| = |E| + |E| - |\emptyset| = 2 \cdot |E|\)
Mà ban đầu ta đã định nghĩa \(E\) là phần tử áp đảo nên \(|E| > (r - l + 1) - |E| \Leftrightarrow 2 \cdot |E| > (r - l + 1)\) (mâu thuẫn)
Suy ra điều phải chứng minh.
Solution
#include <bits/stdc++.h>
using namespace std;
const long long INF = 0x3f3f3f3f3f3f3f3f;
const int MAX_N = 1000005;
struct Node {
long long col;
int cnt;
Node(long long col = INF, int cnt = 0) : col(col), cnt(cnt) {}
};
Node operator+(const Node &left, const Node &right) {
if (left.col == +INF) {
return right;
}
if (right.col == +INF) {
return left;
}
if (left.col == right.col) {
return Node(left.col, left.cnt + right.cnt);
}
if (left.cnt >= right.cnt) {
return Node(left.col, left.cnt - right.cnt);
}
return Node(right.col, right.cnt - left.cnt);
}
long long a[MAX_N];
struct SegmentTree {
private:
int treeSize;
vector<Node> nodes;
vector<long long> lazy;
void initTree(int id, int low, int high) {
if (low == high) {
nodes[id] = Node(a[low], 1);
return;
}
int mid = (low + high) / 2;
initTree(id * 2, low, mid);
initTree(id * 2 + 1, mid + 1, high);
nodes[id] = nodes[id * 2] + nodes[id * 2 + 1];
}
void doLazy(int id) {
if (lazy[id] != 0) {
nodes[2 * id].col += lazy[id];
lazy[2 * id] += lazy[id];
nodes[2 * id + 1].col += lazy[id];
lazy[2 * id + 1] += lazy[id];
lazy[id] = 0;
}
}
void update(int id, int low, int high, int left, int right, int val) {
if (low > right || high < left) {
return;
}
if (low >= left && high <= right) {
lazy[id] += val;
return;
}
int mid = (low + high) / 2;
doLazy(id);
update(id * 2, low, mid, left, right, val);
update(id * 2 + 1, mid + 1, high, left, right, val);
nodes[id] = nodes[id * 2] + nodes[id * 2 + 1];
}
Node get(int id, int low, int high, int left, int right) {
if (low > right || high < left) {
return Node();
}
if (low >= left && high <= right) {
return nodes[id];
}
int mid = (low + high) / 2;
Node leftVal = get(id * 2, low, mid, left, right);
Node rightVal = get(id * 2 + 1, mid + 1, high, left, right);
return leftVal + rightVal;
}
public:
void init(int treeSize) {
this->treeSize = treeSize;
int tmp = 1;
while (tmp < treeSize) {
tmp *= 2;
}
nodes.resize(tmp * 2);
lazy.resize(tmp * 2);
initTree(1, 1, treeSize);
}
void update(int left, int right, int val) {
update(1, 1, treeSize, left, right, val);
}
Node get(int left, int right) {
return get(1, 1, treeSize, left, right);
}
};
int n, numQuery;
map<long long, vector<int>> mp;
SegmentTree segmentTree;
/// SPyofgame Emilia Trick Segment Tree
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++i) {
mp[a[i]].push_back(i);
}
segmentTree.init(n + 1);
while (numQuery--) {
int type;
cin >> type;
assert(type == 2);
int left, right;
cin >> left >> right;
Node res = segmentTree.get(left, right);
if (res.cnt == 0) {
cout << "IMPOSSIBLE\n";
continue;
}
int upper = upper_bound(mp[res.col].begin(), mp[res.col].end(), right) - mp[res.col].begin();
int lower = lower_bound(mp[res.col].begin(), mp[res.col].end(), left) - mp[res.col].begin();
int total = upper - lower;
if (total > (right - left + 1) / 2) {
cout << res.col << '\n';
} else {
cout << "IMPOSSIBLE\n";
}
}
return 0;
}
Tutorial cách 4: Chỉ dùng mỗi cấu trúc dữ liệu
Giống như cách trên, nhưng thay vì kiểm tra lại số lần xuất hiện của một phần tử mà sống sót cuối cùng (để xem nó áp đảo thật sự hay không), thì mình có thể sử dụng Persistent Segment Tree để lưu lại lịch sử của cây để đếm số lần xuất hiện trên đoạn.
Ta có thể tạo một cây gồm các dữ liệu như sau
long long top; // tổng số phần tử trên cây
long long lhs[LIM << 5]; // lhs[id] = nhánh trái của nút
long long rhs[LIM << 5]; // rhs[id] = nhánh phải của nút
long long ver[LIM << 5]; // ver[id] = phiên bản hiện tại của nút
long long sum[LIM << 5]; // sum[ver[r]] - sum[ver[l-1]] = đếm số lần xuất hiện trên đoạn phiên bản [l, r] (sau lần update cuối là l-1)
Thế thì tại mỗi bước, mình kiểm tra xem nhánh trái, hay nhánh phải, có tồn tại một phần tử mà số lần xuất hiện của nó là hơn \(k = \left \lfloor \frac{r - l + 1}{2} \right \rfloor\) hay không, nếu có thì đi vô nhánh đó.
Nhưng cững có thể là cả \(2\) nhánh đều không có phần tử áp đảo, lúc đó mình sẽ dừng đi sâu hơn vào cây và trả về kết quả là một giá trị đặc biệt.
int query(int lt, int rt, int l, int r, int k)
{
if (l == r) return l; /// Đã tìm được phần tử đó
int m = (l + r) >> 1; /// Cây nhị phân thì chặt giữa thôi :3
if (k < sum[lhs[rt]] - sum[lhs[lt]]) return query(lhs[lt], lhs[rt], l, m+0, k); /// Kiểm tra nếu nhánh trái có phần tử áp đảo trong đoạn đệ quy tiếp theo [l \ldots r] = [lhs[l] \ldots lhs[r]]
if (k < sum[rhs[rt]] - sum[rhs[lt]]) return query(rhs[lt], rhs[rt], m+1, r, k); /// Kiểm tra nếu nhánh phải có phần tử áp đảo trong đoạn đệ quy tiếp theo [l \ldots r] = [rhs[l] \ldots rhs[r]]
return 0; /// Không có phần tử áp đảo, trả về giá trị đặc biệt, giả sử mọi a[i] > 0
}
Mình chỉ tạo đỉnh mới khi cập nhật điểm
void update(long long pre, long long &cur, int l, int r, int p)
{
/// Tạo đỉnh mới
cur = ++top;
lhs[cur] = lhs[pre];
rhs[cur] = rhs[pre];
sum[cur] = sum[pre] + 1;
if (l == r) {
return;
}
int m = (l + r) >> 1;
if (p <= m) {
update(lhs[pre], lhs[cur], l, m+0, p); /// Nhánh trái
} else {
update(rhs[pre], rhs[cur], m+1, r, p); /// Nhánh phải
}
}
Còn để xử lí dữ liệu thì có nhiều cách, bao gồm cả cách nén giá trị lại, \(b_p\) là số lớn thứ \(p\) trong \(a[1 \ldots n]\)
cin >> n >> q;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
b[i] = ++a[i]; /// Shift to 1-based
}
sort(b + 1, b + n + 1);
int m = unique(b + 1, b + n + 1) - (b + 1);
for (int i = 1; i <= n; ++i) {
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
update(ver[i-1], ver[i], 1, m, a[i]);
}
Solution
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 5e4 + 54;
int n, numQuery;
long long a[MAX_N];
long long b[MAX_N];
long long top;
long long lhs[MAX_N << 5];
long long rhs[MAX_N << 5];
long long sum[MAX_N << 5];
long long ver[MAX_N << 5];
void update(long long pre, long long &cur, int low, int high, int pos) {
cur = ++top;
lhs[cur] = lhs[pre];
rhs[cur] = rhs[pre];
sum[cur] = sum[pre] + 1;
if (low == high) {
return;
}
int mid = (low + high) >> 1;
if (pos <= mid) {
update(lhs[pre], lhs[cur], low, mid , pos);
} else {
update(rhs[pre], rhs[cur], mid + 1, high, pos);
}
}
int query(int left, int right, int low, int high, int k) {
if (low == high) {
return low;
}
int mid = (low + high) >> 1;
if (k < sum[lhs[right]] - sum[lhs[left]]) {
return query(lhs[left], lhs[right], low, mid, k);
}
if (k < sum[rhs[right]] - sum[rhs[left]]) {
return query(rhs[left], rhs[right], mid + 1, high, k);
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
b[i] = ++a[i];
}
sort(b + 1, b + n + 1);
int m = unique(b + 1, b + n + 1) - (b + 1);
for (int i = 1; i <= n; ++i) {
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
update(ver[i - 1], ver[i], 1, m, a[i]);
}
while (numQuery--) {
int type, left, right;
cin >> type >> left >> right;
int half = (right - left + 1) / 2;
int pos = query(ver[left - 1], ver[right], 1, m, half);
int answer = b[pos] - 1;
if (answer != -1) {
cout << answer << '\n';
} else {
cout << "IMPOSSIBLE\n";
}
}
return 0;
}
Subtask \(3\) (\(30\%\) số điểm): Không có ràng buộc gì thêm.
Nhận xét
Để giải subtask \(3\), thì ngoài cách sử dụng Persistent Lazy Segment Tree, bạn có thể dùng thuật toán chia căn để giải quyết, còn cập nhật và tính toán kết quả của một khối to có thể sử dụng một trong \(4\) ý tưởng của subtask \(2\) là được.
Solution
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 50005;
const int BLOCK = 230;
const long long INF = 0x3f3f3f3f3f3f3f3f;
int n, numQuery;
long long a[MAX_N], b[MAX_N];
int blockId[MAX_N], start[BLOCK], en[BLOCK];
long long lazy[BLOCK];
pair<long long, long long> maxBlock[BLOCK];
pair<long long, long long> merge(const pair<long long, long long> &a, const pair<long long, long long> &b) {
if (a.first == b.first) {
return make_pair(a.first, b.second + a.second);
} else if (a.second > b.second) {
return make_pair(a.first, a.second - b.second);
} else if (a.second < b.second) {
return make_pair(b.first, b.second - a.second);
}
return make_pair(-INF, 0);
}
void update(int block, int left, int right, long long value) {
maxBlock[block] = make_pair(-INF, 0);
for (int i = start[block]; i <= en[block]; i++) {
a[i] += lazy[block] + (i >= left && i <= right ? value : 0);
b[i] = a[i];
maxBlock[block] = merge(maxBlock[block], make_pair(a[i], 1));
}
lazy[block] = 0;
sort(b + start[block], b + en[block] + 1);
}
void update(int left, int right, int value) {
update(blockId[left], left, right, value);
for (int i = blockId[left] + 1; i < blockId[right]; i++) {
lazy[i] += value;
maxBlock[i].first += value;
}
if (blockId[left] != blockId[right]) {
update(blockId[right], left, right, value);
}
}
long long get(int left, int right) {
pair<long long, long long> result = make_pair(-INF, 0LL);
for (int i = left; i <= right && i <= en[blockId[left]]; i++) {
result = merge(result, make_pair(a[i] + lazy[blockId[left]], 1));
}
for (int i = blockId[left] + 1; i < blockId[right]; i++) {
result = merge(result, maxBlock[i]);
}
if (blockId[left] != blockId[right]) {
for (int i = start[blockId[right]]; i <= right; i++) {
result = merge(result, make_pair(a[i] + lazy[blockId[right]], 1));
}
}
return result.first;
}
int countAppear(int left, int right, long long value) {
int result = 0;
for (int i = left; i <= right && i <= en[blockId[left]]; i++) {
if (a[i] + lazy[blockId[left]] == value) {
++result;
}
}
for (int i = blockId[left] + 1; i < blockId[right]; i++) {
result += upper_bound(b + start[i], b + en[i] + 1, value - lazy[i]) - lower_bound(b + start[i], b + en[i] + 1, value - lazy[i]);
}
if (blockId[left] != blockId[right]) {
for (int i = start[blockId[right]]; i <= right; i++) {
if (a[i] + lazy[blockId[right]] == value) {
result++;
}
}
}
return result;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> numQuery;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
blockId[i] = (i + BLOCK - 1) / BLOCK;
if (!start[blockId[i]]) {
start[blockId[i]] = i;
}
en[blockId[i]] = i;
}
for (int i = 1; i <= blockId[n]; i++) {
update(i, 1, n, 0);
}
while (numQuery--) {
int type, left, right;
cin >> type >> left >> right;
if (type == 1) {
int value;
cin >> value;
update(left, right, value);
} else {
long long result = get(left, right);
if (result != -INF && countAppear(left, right, result) * 2 > (right - left + 1)) {
cout << result << "\n";
} else {
cout << "IMPOSSIBLE\n";
}
}
}
return 0;
}
Bình luận