alo Vũ ơi Vũ
đã đăng vào 25, Tháng 3, 2026, 8:39nà ná na na anh Phùng Thanh Độ anh Độ mixi, Phùng Thanh Độ anh Độ mixi, anh Độ cảm ơn anh Độ
nà ná na na anh Phùng Thanh Độ anh Độ mixi, Phùng Thanh Độ anh Độ mixi, anh Độ cảm ơn anh Độ
Xin chào các bạn,
Thời điểm năm hết Tết đến cũng là lúc mà các contest thi thử HSG9 của câu lạc bộ SQRT quay trở lại. Với xuân Bính Ngọ năm nay, chúng mình mời các bạn tham gia chuỗi 4 contest của chúng mình để có thể nhận được những phong bao lì xì cực kỳ hấp dẫn nhân dịp đầu năm mới nhé.
Các bạn hãy cùng chúng mình điểm qua những thông tin chính của chuỗi contest năm nay nào :D
Chuỗi contest sẽ kéo dài trong 4 ngày, từ ngày 10 đến 13 tháng 2 năm 2026. Contest của mỗi ngày sẽ bắt đầu vào 20 giờ và kết thúc vào 22 giờ 30.
Kết quả sẽ được công bố vào tối ngày 14 tháng 2 năm 2026 trên server Discord SQRT Community.
(Một thí sinh được tính là dự thi nếu tham gia ít nhất một trong bốn contest và có điểm chung cuộc lớn hơn 0. Với các giải Lucky AC và Lucky Solution, nếu thí sinh nộp bài nhiều lần cho một bài tập thì chỉ tính lần nộp cuối cùng)
Dự đoán số lượng thí sinh dự thi: Các bạn comment dưới bài viết này tổng số thí sinh dự thi theo dự đoán của các bạn. Bạn có kết quả tốt nhất (chênh lệch so với số lượng thực tế thấp nhất) sẽ nhận được giải thưởng ~20.260~ VNĐ.

tris ko gay dung khong
Xin chào các bạn,
Nếu các bạn từng theo dõi TLEOJ và câu lạc bộ TLE, chắc hẳn các bạn đã biết về TLEOJ Cup, giải đấu thường niên được tổ chức bởi Câu lạc bộ TLE. Năm nay, chúng mình, câu lạc bộ SQRT, cùng với sự hỗ trợ của câu lạc bộ Tin học trường THPT Chuyên Quốc học Huế mang đến cho các bạn một giải đấu mới mang tên QHHOJ x SQRT Cup 2025, hứa hẹn sẽ là một sân chơi chất lượng và bổ ích cho các bạn học sinh đam mê Lập trình thi đấu.
QHHOJ x SQRT Cup 2025 bao gồm 3 vòng thi:
Link đăng ký:
Thể lệ cuộc thi: https://drive.google.com/file/d/1UWPIX6Rii7aiJNLIqTvFr913j33Ydj1R/view?usp=sharing
Ngoài ra, Ban tổ chức cũng sẽ đăng các tài liệu / nội dung khác lên đây: https://drive.google.com/drive/folders/1nNQziKwG8OH6DWzAZTRd82SVLd6P8K4L?usp=drive_link
Xin chào các bạn, chúng mình là câu lạc bộ SQRT.
SQRT được thành lập vào tháng 8 năm 2024 bởi một số bạn học sinh trên cả nước, với tiền thân là câu lạc bộ TLE. Đây là một câu lạc bộ phi lợi nhuận, được lập ra với mục đích chia sẻ kiến thức và phát triển cộng đồng Tin học Việt Nam. Chắc hẳn những bạn biết đến câu lạc bộ SQRT đều biết đến hệ thống chấm bài online của chúng mình – SQRT Online Judge (https://sqrtoj.edu.vn/), dự án chính của câu lạc bộ SQRT. Tuy nhiên, chúng mình vẫn có những nội dung khác mà các bạn có thể tìm thấy tại:
Nếu các bạn là giáo viên dạy Tin học và muốn có một nền tảng chấm bài của riêng mình, các bạn có thể tham khảo SQRT Shop (https://sqrtoj.edu.vn/shop/). Chúng mình có cung cấp các dịch vụ như sử dụng tính năng tổ chức và cài đặt – duy trì một trang chấm bài riêng với giá cả phải chăng. Nếu các bạn muốn ủng hộ câu lạc bộ, hãy đón chờ những sản phẩm như áo, áo khoác SQRT trong tương lai. Nếu các bạn có ý kiến đóng góp cho câu lạc bộ, hãy comment dưới bài viết này, inbox fanpage hoặc nhắn trong server Discord và ping @sqrt.sunlight để được giải đáp.
Chúng mình xin chân thành cảm ơn sự ủng hộ của các bạn trong gần một năm qua. Hy vọng các bạn có thể tiếp tục đi cùng chúng mình trong thời gian tới.

Xin chào các bạn,
Hiện nay rất nhiều tỉnh, thành phố, trường THPT chuyên đã tổ chức các kỳ thi tuyển sinh lớp 10, trong đó bao gồm cả môn chuyên Tin. Để giúp các bạn có thể luyện tập, câu lạc bộ SQRT đã đăng một số đề thi tuyển sinh lớp 10 chuyên Tin mới thi lên hệ thống, bao gồm các đề của các tỉnh / thành phố / trường sau:
Post này sẽ được cập nhật liên tục khi có đề thi mới.
Chúc các bạn luyện tập vui vẻ.
Vậy là contest thi thử giao lưu Tin học trẻ của CLB SQRT chúng mình đã diễn ra.
Tiện đây, mình xin gửi lời chúc mừng đến các bạn sau đã đạt top ~20~ của contest ở cả hai bảng và nhận được các phần thưởng giảm giá khi thi chính thức từ chúng mình:
Các bạn có thể đăng ký thi chính thức tại đây.
Bảng THCS:
| Thứ hạng | Handle | Tổng điểm |
|---|---|---|
| 2 | 400 | |
| 2 | 400 | |
| 3 | 360 | |
| 4 | 339 | |
| 5 | 313 | |
| 7 | 306 | |
| 7 | 306 | |
| 9 | 260 | |
| 9 | 260 | |
| 10 | 201 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 | |
| 20 | 200 |
Bảng THPT:
| Thứ hạng | Handle | Tổng điểm |
|---|---|---|
| 1 | 400 | |
| 4 | 360 | |
| 4 | 360 | |
| 4 | 360 | |
| 6 | 340 | |
| 6 | 340 | |
| 7 | 330 | |
| 8 | 328 | |
| 9 | 312 | |
| 10 | 300 | |
| 11 | 294 | |
| 12 | 206 | |
| 15 | 200 | |
| 15 | 200 | |
| 15 | 200 | |
| 16 | 172 | |
| 19 | 160 | |
| 19 | 160 | |
| 19 | 160 | |
| 20 | 140 |
Lời giải của bài tập đã được public, các bạn có thể luyện tập tại đây.
Lời cuối, cảm ơn các bạn đã tham gia contest của chúng mình!
Xìn chào tất cả các bạn, vẫn là mình - đây!
Kỳ thi Giao Lưu Tin Học Trẻ 2025 chính thức được khởi động, mang đến sân chơi bổ ích để các bạn trau dồi kỹ năng, chuẩn bị hành trang vững vàng cho kỳ thi chính thức. Dù bạn là học sinh Tiểu học, THCS hay THPT, đây đều là cơ hội để thể hiện tài năng và giao lưu học hỏi!
Chúng tôi mong muốn tạo ra một môi trường thi đấu công bằng, lành mạnh và bổ ích cho tất cả thí sinh. Mọi thắc mắc hoặc cần hỗ trợ, vui lòng liên hệ qua:
Hãy đăng ký ngay để không bỏ lỡ cơ hội thể hiện bản thân và rinh về những phần thưởng giá trị! 🌟
Bài toán được phát biểu chi tiết tại VNOI - sqrt2_a. Tóm tắt như sau:
Cho một đồ thị vô hướng gồm ~n~ đỉnh và ~m~ cạnh ban đầu. Sau đó có ~k~ truy vấn, mỗi truy vấn thuộc một trong hai loại:
Nhiệm vụ: sau mỗi truy vấn (và cả trước khi xử lý truy vấn nào), hãy in ra số thành phần liên thông của đồ thị.
Ràng buộc: ~(2 \le n \le 10^5), (1 \le m, k \le 10^5)~
Tổng số thao tác ~q = m + k~ cũng có thể lên tới ~2 \times 10^5~.
Đây là bài toán đồ thị động (dynamic graph) với cả hai thao tác thêm và xóa cạnh. Do kích thước lớn, cần những phương pháp tối ưu.
Ý tưởng:
Mỗi khi có thay đổi (thêm hoặc xóa cạnh), ta duyệt lại toàn bộ đồ thị bằng BFS hoặc DFS để đếm số thành phần liên thông.
Độ phức tạp:
Mỗi lần duyệt mất ~O(n + m')~ với ~m'~ là số cạnh hiện tại. Có ~q~ truy vấn, tổng độ phức tạp ~O(q \cdot (n + q))~ trong trường hợp xấu nhất.
Với ~n, q \le 2\times 10^5~, cách này quá chậm, không khả thi.
Nếu chỉ có thêm cạnh (đồ thị chỉ tăng dần), ta dùng Disjoint Set Union (DSU) rất hiệu quả:
Mỗi lần thêm cạnh, nếu hai đỉnh chưa cùng thành phần, ta hợp nhất và giảm số thành phần đi 1.
Độ phức tạp gần như ~O(1)~ cho mỗi thao tác.
Nhưng khi có xóa, DSU cơ bản không hỗ trợ tách rời các thành phần. Điều này đòi hỏi các kỹ thuật nâng cao hơn.
Nguồn tham khảo: Editorial chính thức của đề bài
Phương pháp này dựa trên ý tưởng phân hoạch dãy thao tác thành các khối (block) có kích thước xấp xỉ ~\sqrt{q}~. Mỗi khối được xử lý độc lập, trong đó tận dụng tính chất: số lượng cạnh thay đổi trạng thái (thêm/xóa) trong một khối là nhỏ, từ đó giảm chi phí xử lý mỗi truy vấn.
Chia ~q~ thao tác (gồm ~m~ cạnh ban đầu và ~k~ truy vấn) thành các khối, mỗi khối có ~B = \lceil \sqrt{q} \rceil~ thao tác (có thể điều chỉnh ~B~ để tối ưu thực tế). Khi xét một khối cụ thể, ta phân các cạnh của đồ thị thành hai loại:
Xử lý một khối gồm ba bước chính:
Giả sử khối hiện tại bao gồm các thao tác có chỉ số từ ~L~ đến ~R~ (đánh số 1..~q~).
Bước 1: Xác định cạnh tĩnh và cạnh động
Bước 2: Xây dựng DSU cơ sở cho khối
Bước 3: Xử lý tuần tự từng truy vấn trong khối
Với mỗi truy vấn thứ ~i~ từ ~L~ đến ~R~:
Chú ý: Các cạnh động có thể có trạng thái thay đổi theo thời gian trong khối. Việc duyệt lại toàn bộ cạnh động mỗi lần truy vấn (thay vì chỉ cập nhật một cạnh) tuy tốn ~O(B)~ nhưng là chấp nhận được vì ~B~ nhỏ.
Để thực hiện rollback, DSU không được dùng path compression. Thay vào đó, ta chỉ dùng union by size (hoặc rank). Mỗi lần hợp nhất hai thành phần thành công, ta lưu lại các thông tin thay đổi vào một ngăn xếp (stack), ví dụ: đỉnh con bị đổi cha, kích thước cũ của gốc mới, và số thành phần liên thông trước khi hợp nhất. Khi cần rollback, ta lấy thông tin từ stack và khôi phục lại trạng thái cũ. Mỗi thao tác hợp nhất và rollback đều có chi phí ~O(1)~ (không tính chi phí tìm gốc – nhưng tìm gốc vẫn là ~O(\log n)~ do không nén).
Ngoài ra, để tăng tốc phần xử lý cạnh tĩnh, có thể tách riêng một DSU khác không cần rollback, sử dụng path compression. Khi đó trong mỗi truy vấn, ta sẽ kết hợp kết quả từ DSU tĩnh và các cạnh động tạm thời. Tuy nhiên, việc kết hợp này đòi hỏi phải ánh xạ các đỉnh về đại diện của DSU tĩnh, khá phức tạp. Trong khuôn khổ bài viết, ta giữ cách dùng chung một DSU rollback cho toàn bộ, vì độ phức tạp vẫn đạt yêu cầu.
Với ~q \le 2 \times 10^5~, ~\sqrt{q} \approx 447~, ~q \sqrt{q} \approx 9 \times 10^7~, nhân với ~\log n \approx 18~ cho khoảng 1,6 tỷ phép toán – lớn nhưng nếu cài đặt tối ưu (C++, dùng mảng tĩnh, tránh cấp phát động) vẫn có thể chạy trong 2-3 giây. Thực tế, hằng số nhỏ hơn do nhiều cạnh không cần xét ở mỗi khối. Nếu muốn an toàn hơn, có thể tăng kích thước khối lên một chút (ví dụ ~B = 700~) để giảm số khối, nhưng khi đó chi phí trong khối tăng lên. Cần cân nhắc.
active[edge_id] để biết cạnh đó có đang tồn tại trong đồ thị tại thời điểm đang xét hay không. Mảng này được cập nhật liên tục qua các khối và qua các truy vấn trong khối.#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
void setupIO() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
}
// Cấu trúc đại diện cho một cạnh (luôn đảm bảo u < v để dễ so sánh)
struct Edge {
int u, v;
bool operator<(const Edge& other) const {
if (u != other.u) return u < other.u;
return v < other.v;
}
bool operator==(const Edge& other) const {
return u == other.u && v == other.v;
}
};
struct Query {
int t, id;
};
// Cấu trúc DSU hỗ trợ Rollback
struct DSU {
int components;
vector<int> parent;
vector<int> sz;
struct State {
int u, v;
int sz_u, sz_v;
int components;
};
vector<State> history;
DSU(int n) : components(n), parent(n + 1), sz(n + 1, 1) {
iota(parent.begin(), parent.end(), 0);
}
void reset(int n_verts) {
components = n_verts;
for(int i = 1; i <= n_verts; ++i) {
parent[i] = i;
sz[i] = 1;
}
history.clear();
}
// Gộp tĩnh: Có Path Compression
int find_pc(int i) {
if (parent[i] == i) return i;
return parent[i] = find_pc(parent[i]);
}
void unite_static(int i, int j) {
int root_i = find_pc(i);
int root_j = find_pc(j);
if (root_i != root_j) {
if (sz[root_i] < sz[root_j]) swap(root_i, root_j);
parent[root_j] = root_i;
sz[root_i] += sz[root_j];
components--;
}
}
// Gộp động: KHÔNG Path Compression để có thể Rollback
int find_no_pc(int i) {
while (parent[i] != i) i = parent[i];
return i;
}
void unite_dynamic(int i, int j) {
int root_i = find_no_pc(i);
int root_j = find_no_pc(j);
if (root_i == root_j) {
history.push_back({-1, -1, -1, -1, components});
return;
}
if (sz[root_i] < sz[root_j]) swap(root_i, root_j);
history.push_back({root_i, root_j, sz[root_i], sz[root_j], components});
parent[root_j] = root_i;
sz[root_i] += sz[root_j];
components--;
}
void rollback() {
if (history.empty()) return;
State st = history.back();
history.pop_back();
if (st.u != -1) {
parent[st.v] = st.v;
sz[st.u] = st.sz_u;
sz[st.v] = st.sz_v;
components = st.components;
}
}
};
int main() {
setupIO();
int n, m, k;
if (!(cin >> n >> m >> k)) return 0; // Đọc 3 số nguyên n, m, k
vector<Edge> all_edges;
vector<Edge> initial_edges(m);
// Đọc m cạnh ban đầu của đồ thị
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
if (u > v) swap(u, v);
initial_edges[i] = {u, v};
all_edges.push_back({u, v});
}
vector<Query> queries(k);
vector<Edge> query_edges(k);
// Đọc k truy vấn tiếp theo (t=1: thêm cạnh, t=2: xóa cạnh)
for (int i = 0; i < k; ++i) {
int t, u, v;
cin >> t >> u >> v;
if (u > v) swap(u, v);
queries[i] = {t, 0};
query_edges[i] = {u, v};
all_edges.push_back({u, v});
}
// Nén tọa độ cạnh
sort(all_edges.begin(), all_edges.end());
all_edges.erase(unique(all_edges.begin(), all_edges.end()), all_edges.end());
int E = all_edges.size();
vector<int> U(E), V(E);
for (int i = 0; i < E; ++i) {
U[i] = all_edges[i].u;
V[i] = all_edges[i].v;
}
vector<bool> in_graph(E, false);
// Đánh dấu các cạnh ban đầu đã tồn tại trong đồ thị
for (int i = 0; i < m; ++i) {
int id = lower_bound(all_edges.begin(), all_edges.end(), initial_edges[i]) - all_edges.begin();
in_graph[id] = true;
}
for (int i = 0; i < k; ++i) {
int id = lower_bound(all_edges.begin(), all_edges.end(), query_edges[i]) - all_edges.begin();
queries[i].id = id;
}
DSU dsu(n);
// 1. In ra số thành phần liên thông trước khi các thao tác diễn ra
for (int i = 0; i < E; ++i) {
if (in_graph[i]) dsu.unite_static(U[i], V[i]);
}
cout << dsu.components << " ";
// Bắt đầu xử lý truy vấn theo kỹ thuật Chia căn
int B = 400; // Kích thước Block (căn bậc hai của 10^5)
vector<bool> is_dynamic(E, false);
for (int i = 0; i < k; i += B) {
int L = i;
int R = min(k - 1, i + B - 1);
vector<int> dyn_edges;
for (int j = L; j <= R; ++j) {
int id = queries[j].id;
if (!is_dynamic[id]) {
is_dynamic[id] = true;
dyn_edges.push_back(id);
}
}
dsu.reset(n);
// Xây dựng cây dựa trên các cạnh tĩnh
for (int j = 0; j < E; ++j) {
if (in_graph[j] && !is_dynamic[j]) {
dsu.unite_static(U[j], V[j]);
}
}
// Xử lý từng truy vấn trong block hiện tại
for (int j = L; j <= R; ++j) {
int id = queries[j].id;
int type = queries[j].t;
// Cập nhật trạng thái cạnh (thêm vào đồ thị nếu t=1, xóa khỏi đồ thị nếu t=2) [cite: 13]
if (type == 1) in_graph[id] = true;
else if (type == 2) in_graph[id] = false;
int unions_done = 0;
for (int d_id : dyn_edges) {
if (in_graph[d_id]) {
dsu.unite_dynamic(U[d_id], V[d_id]);
unions_done++;
}
}
// 2. In ra số thành phần liên thông sau thao tác biến đổi
cout << dsu.components << (j == k - 1 ? "" : " ");
for (int c = 0; c < unions_done; ++c) {
dsu.rollback();
}
}
for (int id : dyn_edges) {
is_dynamic[id] = false;
}
}
cout << "\n";
return 0;
}
Nguồn tham khảo:
Đây là phương pháp tối ưu và phổ biến nhất cho bài toán đồ thị động với cả thêm và xóa cạnh. Ý tưởng cốt lõi là chuyển bài toán có cả thêm và xóa thành bài toán chỉ có thêm bằng cách xác định khoảng thời gian mà mỗi cạnh tồn tại, sau đó dùng một cấu trúc dữ liệu hỗ trợ thêm và quay lui (rollback) kết hợp với segment tree.
Giả sử ta có ~q~ thao tác (gồm ~m~ cạnh ban đầu và ~k~ truy vấn thêm/xóa). Ta đánh số các thao tác từ ~1~ đến ~q~. Với mỗi cạnh, ta xác định thời điểm nó được thêm vào (lần đầu tiên hoặc sau khi bị xóa) và thời điểm nó bị xóa (nếu có). Như vậy, mỗi cạnh sẽ tồn tại liên tục trong một hoặc nhiều đoạn thời gian. Do mỗi thao tác thêm/xóa đều tạo ra một cặp “thêm – xóa”, ta có thể biểu diễn mỗi cạnh bằng một đoạn ~[l, r)~ với ~l~ là thời điểm thêm, ~r~ là thời điểm xóa (hoặc ~q+1~ nếu không bị xóa). Nếu có nhiều lần thêm/xóa xen kẽ, mỗi lần thêm tạo một đoạn riêng.
Ví dụ:
Việc xác định các đoạn này có thể thực hiện bằng cách duyệt qua các thao tác, dùng một map lưu thời điểm thêm gần nhất của mỗi cạnh, khi gặp thao tác xóa thì lấy thời điểm thêm tương ứng và tạo đoạn.
Ta xây dựng một segment tree (cây phân đoạn) trên khoảng thời gian ~[1, q+1)~. Mỗi node của cây đại diện cho một đoạn thời gian. Với mỗi cạnh có đoạn tồn tại ~[l, r)~, ta chèn cạnh đó vào các node của segment tree sao cho đoạn ~[l, r)~ được phủ bởi các node con mà không bị dư thừa. Điều này tương tự như thao tác cập nhật đoạn trên segment tree: ta gọi hàm đệ quy, nếu đoạn của node nằm gọn trong ~[l, r)~ thì thêm cạnh vào node đó; ngược lại, tiếp tục xuống các node con. Nhờ vậy, mỗi cạnh được lưu trong ~O(\log q)~ node.
Vì chúng ta sẽ duyệt cây phân đoạn và thực hiện các thao tác thêm cạnh trên từng node, sau đó cần quay lui để xử lý các nhánh khác, nên DSU thông thường không đáp ứng được. Thay vào đó, ta sử dụng DSU có rollback (còn gọi là DSU với khả năng quay lui). Các đặc điểm của DSU này:
(a, parent[a] cũ) và (b, size[b] cũ). Sau đó thực hiện gắn và cập nhật size.rollback() sẽ pop hai phần tử khỏi stack và khôi phục lại trạng thái trước khi hợp nhất.snapshot (độ dài stack hiện tại) và rollback về snapshot đó.Độ phức tạp của mỗi thao tác unite/rollback là ~O(\log n)~ (do chỉ dùng union by size và không nén).
Sau khi đã chèn tất cả các cạnh vào segment tree, ta thực hiện duyệt cây theo chiều sâu (DFS) từ node gốc, với một DSU rollback toàn cục.
void dfs(node, l, r) {
snapshot = dsu.snapshot();
for (cạnh trong node) {
dsu.unite(u, v); // thêm các cạnh của node này
}
if (l+1 == r) { // node lá, tương ứng thời điểm l
ans[l] = dsu.components; // ghi nhận số thành phần liên thông
} else {
int mid = (l+r)/2;
dfs(node*2, l, mid);
dfs(node*2+1, mid, r);
}
dsu.rollback_to(snapshot); // quay lui để trả về trạng thái trước khi vào node
}
Giải thích:
Kết thúc quá trình, mảng ans chứa số thành phần liên thông sau mỗi thao tác (tại các thời điểm từ 1 đến q). Bài toán yêu cầu in trạng thái ban đầu (sau m cạnh ban đầu) và sau mỗi truy vấn tiếp theo, ta chỉ cần lấy các giá trị tương ứng.
unite trong quá trình duyệt DFS là tổng số lần cạnh xuất hiện trong các node, tức ~O((m+k) \log q)~. unite có chi phí ~O(\log n)~ do tìm gốc (không nén) và thao tác rollback cũng tốn ~O(\log n)~. u < v) để việc lưu trong map được nhất quán. unite thành công lưu đúng hai thay đổi, và khi rollback phải lấy đúng hai phần tử. Có thể dùng cấu trúc struct chứa loại thay đổi (parent hay size) hoặc lưu chung trong một mảng cặp. #include <bits/stdc++.h>
using namespace std;
// Cấu trúc DSU hỗ trợ rollback (không dùng path compression)
struct DSUWithRollback {
vector<int> parent, sz; // parent và size (union by size)
int comps; // số thành phần liên thông hiện tại
// Lưu lịch sử thay đổi để rollback
struct Change {
int v; // đỉnh bị thay đổi
int old; // giá trị cũ (parent cũ hoặc size cũ)
bool is_parent; // true nếu thay đổi parent, false nếu thay đổi size
};
stack<Change> history;
DSUWithRollback(int n) : parent(n+1), sz(n+1, 1), comps(n) {
for (int i = 1; i <= n; ++i) parent[i] = i;
}
// Tìm đại diện (không nén)
int find(int x) {
while (parent[x] != x) x = parent[x];
return x;
}
// Hợp nhất hai đỉnh, trả về true nếu có sự thay đổi
bool unite(int a, int b) {
a = find(a); b = find(b);
if (a == b) return false;
// Đảm bảo a có size nhỏ hơn để gắn a vào b
if (sz[a] > sz[b]) swap(a, b);
// Lưu thay đổi (push 2 change)
history.push({a, parent[a], true}); // parent của a
history.push({b, sz[b], false}); // size của b
parent[a] = b;
sz[b] += sz[a];
--comps;
return true;
}
// Rollback một lần hợp nhất (pop 2 change và tăng comps)
void rollback() {
Change sz_ch = history.top(); history.pop(); // size change
Change p_ch = history.top(); history.pop(); // parent change
parent[p_ch.v] = p_ch.old;
sz[sz_ch.v] = sz_ch.old;
++comps;
}
// Lấy trạng thái hiện tại (số lượng change đã lưu)
int snapshot() const {
return history.size();
}
// Rollback về trạng thái snapshot
void rollback_to(int snap) {
while ((int)history.size() > snap) rollback();
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
int Q = m + k; // tổng số thao tác (1-indexed)
// Đọc tất cả thao tác: (type, u, v)
vector<tuple<int,int,int>> ops(Q+1);
for (int i = 1; i <= m; ++i) {
int u, v; cin >> u >> v;
ops[i] = {1, u, v};
}
for (int i = 1; i <= k; ++i) {
int t, u, v; cin >> t >> u >> v;
ops[m+i] = {t, u, v};
}
// Xác định khoảng thời gian sống của mỗi cạnh (u,v)
map<pair<int,int>, int> last_add; // lần thêm gần nhất
vector<tuple<int,int,int,int>> edges; // u, v, l, r (tồn tại trong [l, r))
for (int i = 1; i <= Q; ++i) {
auto [type, u, v] = ops[i];
if (u > v) swap(u, v); // chuẩn hóa cạnh vô hướng
if (type == 1) { // thêm cạnh
last_add[{u, v}] = i;
} else { // xóa cạnh
auto it = last_add.find({u, v});
if (it != last_add.end()) {
int l = it->second;
int r = i;
edges.emplace_back(u, v, l, r);
last_add.erase(it);
}
}
}
// Các cạnh còn lại không bị xóa, tồn tại đến hết
for (auto &[uv, l] : last_add) {
auto [u, v] = uv;
edges.emplace_back(u, v, l, Q+1);
}
// Xây dựng segment tree trên các thời điểm [1, Q+1)
int seg_sz = Q + 2; // đủ để bao [1, Q+1)
vector<vector<pair<int,int>>> seg(4 * seg_sz); // mỗi node lưu danh sách cạnh (u,v)
// Thêm cạnh vào segment tree
function<void(int,int,int,int,int,int,int)> add_edge = [&](int node, int l, int r, int ql, int qr, int u, int v) {
if (ql <= l && r <= qr) {
seg[node].emplace_back(u, v);
return;
}
int mid = (l + r) / 2;
if (ql < mid) add_edge(node*2, l, mid, ql, qr, u, v);
if (qr > mid) add_edge(node*2+1, mid, r, ql, qr, u, v);
};
for (auto &[u, v, l, r] : edges) {
if (l < r) {
add_edge(1, 1, seg_sz, l, r, u, v);
}
}
DSUWithRollback dsu(n);
vector<int> ans(Q+2, 0); // ans[i] = số thành phần liên thông sau thao tác i
// Duyệt segment tree theo DFS
function<void(int,int,int)> dfs = [&](int node, int l, int r) {
int snap = dsu.snapshot();
// Thêm tất cả cạnh tại node này
for (auto &[u, v] : seg[node]) {
dsu.unite(u, v);
}
if (r - l == 1) { // node lá → ứng với thời điểm l
ans[l] = dsu.comps;
} else {
int mid = (l + r) / 2;
dfs(node*2, l, mid);
dfs(node*2+1, mid, r);
}
dsu.rollback_to(snap); // khôi phục trạng thái trước khi vào node
};
dfs(1, 1, seg_sz);
// In kết quả: sau m thao tác đầu và sau mỗi truy vấn
cout << ans[m];
for (int i = 1; i <= k; ++i) {
cout << ' ' << ans[m + i];
}
cout << '\n';
return 0;
}
vậy là contest SQRT Contest #04 đã chính thức khép lại với những khoảnh khắc thi đấu đầy kịch tính và các thành tích đáng tự hào của cộng đồng lập trình viên trẻ! Hãy cùng nhìn lại những điểm nhất nổi bật của cuộc thi lần này nhé:
Cuộc thi lần này tiếp tục khẳng định sức hút của mình với sự tham gia đông đảo từ cộng đồng:
Những con số này không chỉ là thống kê mà còn là câu chuyện về nỗ lực và đam mê của từng thí sinh.
Như thường lệ, CLB SQRT tiếp tục trao tặng 20 giải thưởng hấp dẫn dành cho những thí sinh xuất sắc nhất (với điều kiện có ít nhất ~5~ bài tập có điểm lớn hơn ~0~). Đây là những món quà nhỏ nhưng chứa đựng sự trân trọng lớn lao của chúng tớ:
| Số lượng | Giải thưởng | Tiền thưởng (VNĐ) | Giảm giá khi mua áo SQRT (VNĐ) |
|---|---|---|---|
| ~01~ | Vô địch | ~80.000~ | ~120.000~ |
| ~02~ | Nhất | ~60.000~ | ~100.000~ |
| ~02~ | Nhì | ~50.000~ | ~70.000~ |
| ~05~ | Ba | ~30.000~ | ~50.000~ |
| ~05~ | Tư | ~20.000~ | ~20.000~ |
| ~05~ | Khuyến khích | ~10.000~ | ~10.000~ |
Những phần thưởng này không chỉ là vật chất mà còn là động lực để các bạn tiếp tục chinh phúc những thử thách mới phía trước!
Ngoài giải thưởng, các thí sinh xuất sắc còn có cơ hội tăng rating trên hệ thống SQRTOJ. Đây là bước đệm để các bạn tiến gần hơn đến những mục tiêu lớn, như đổi màu handle – giấc mơ của mọi coder! Hãy tận dụng cơ hội này để khẳng định tên tuổi của mình nhé!
Chúng tôi xin gửi lời cảm ơn chân thành đến tất cả các bạn đã tham gia SQRT Contest #04. Dù bạn đứng ở vị trí nào trên bảng xếp hạng, mỗi dòng code, mỗi lần submit đều là minh chứng cho sự nỗ lực và đam mê của bạn. Chính các bạn đã làm nên một cuộc thi đầy ý nghĩa và đáng nhớ!
Chúc các bạn luôn giữ được ngọn lửa đam mê, tiếp tục học hỏi và bứt phá trong những sân chơi tiếp theo. CLB SQRT sẽ luôn đồng hành cùng các bạn trên chặng đường chinh phục lập trình!
👇 Tham gia cộng đồng chúng tớ tại:
SQRTOJ: SQRT Online Judge
SQRT Online Judge: SQRTOJ
SQRTOJ Community: Discord
SQRTOJ Fanpage: Facebook
SQRTOJ Group: Facebook