Hoàng Minh Nguyễn
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # Coding Challenge #2 ## Đề Bài :::spoiler [Đề bài gốc](https://oj.vnoi.info/problem/coding_challenge_dauthau) có thể được đọc trên hệ thống [VNOJ](https://oj.vnoi.info/contest/coding_challenge_2) # **Coding Challenge #2 - Đấu Thầu** Nước VNOI gồm $n$ thành phố và đang có kế hoạch liên kết chúng bằng $m$ con đường cao tốc *hai chiều*. Các con đường được đánh số từ 1 tới $m$, con đường thứ $i$ liên kết thành phố $u_i$ với $v_i$, và có *lợi nhuận* là $w_i$ (các giá trị $w_i$ phân biệt). Các con đường được thiết kế để tất cả các cặp thành phố đều có thể trực tiếp hoặc gián tiếp đi tới nhau. Để xây dựng các con đường đó, chính quyền VNOI quyết định giao việc này cho $k$ nhà thầu. Sau những vòng bỏ phiếu căng thẳng, các nhà thầu này được xếp ở các vị trí từ $1$ tới $k$ thể hiện sự ưu tiên trong việc chọn tuyến đường thi công. Mỗi nhà thầu sẽ được chọn một tập hợp các con đường trong $m$ đường cao tốc có sẵn trong kế hoạch, sao cho chúng *không tạo ra bất kì chu trình nào* và con đường không bị nhà thầu nào trước đó chọn. Giả sử, nếu nhà thầu chọn con đường $\{1 \to 2; 2 \to 3; 3 \to 1; 4 \to 5\}$ là không thoả mãn, bởi vì có chu trình $1 \to 2 \to 3 \to 1$. Lưu ý rằng ở ví dụ trên mũi tên được đánh hướng cho mục đích biểu diễn, còn trên thực tế thì các con đường là hai chiều. Đầu tiên, nhà thầu thứ $1$ sẽ được chọn trước, sau đó tới nhà thầu thứ $2$, ... tới nhà thầu thứ $k$. Tất nhiên với cách này, một số con đường sẽ không được xây dựng, nhưng ở giai đoạn đầu của dự án thì đây là chuyện bình thường. Tất nhiên, mỗi nhà thầu đều muốn tối đa hoá lợi ích của mình, vậy nên khi được chọn các con đường, họ sẽ chọn tập con đường mang tới **tổng lợi nhuận** lớn nhất có thể. Nhiệm vụ của bạn là tính toán trước cho chính quyền VNOI xem mỗi nhà thầu từ $1$ tới $k$ sẽ có lợi nhuận là bao nhiêu khi tất cả đều chọn tập con đường một cách tối ưu. ## Input - Dòng đầu tiên gồm 3 số nguyên $n, m, k$ miêu tả số thành phố, số con đường cần xây dựng và số nhà thầu; - $m$ dòng tiếp theo, dòng thứ $i$ gồm $3$ số nguyên $u_i, v_i, w_i$ miêu tả con đường thứ $i$ trong kế hoạch, kết nối thành phố $u_i$ với $v_i$ ## Output - Gồm $k$ dòng, dòng thứ $i$ chứa giá trị là tổng lợi nhuận của nhà thầu thứ $i$ trong cách chia tối ưu. ## Subtask - Trong tất cả các test $w_i \le 10^9$ - Có $20$% test tương ứng $20$% số điểm có $n \le 10^5, m \le 5 \times 10^5; k = 1$. - Có $25$% test tương ứng $25$% số điểm có $n \le 10^3, m \le 10^4; k \le 1000$. - Có $35$% test tương ứng $35$% số điểm có $n \le 10^3, m \le 5 \times 10^5; k \le 10^4$. - $20$% số test còn lại tương ứng với $20$ số điểm có $n \le 10^5, m \le 5 \times 10^5; k \le 10^4$. ## Sample ### Sample 1 #### Input ``` 4 5 1 1 2 5 2 3 4 3 4 3 4 1 2 1 3 1 ``` #### Output ``` 12 ``` ### Sample 2 #### Input ``` 5 8 3 1 2 9 2 3 8 3 4 7 2 5 4 1 3 3 2 4 2 4 5 6 1 5 5 ``` #### Output ``` 30 14 0 ``` ::: ### Tóm Tắt Đề Bài - Cho một đồ thị vô hướng liên thông $G = (V, E)$ có $n$ đỉnh và $m$ cạnh mang trọng số dương. - Thực hiện thao tác sau $k$ lần: - Tìm tập cạnh $E' \subseteq E$ sao cho: - $E'$ không chứa chu trình; - $E'$ có tổng trọng số lớn nhất có thể. - Xóa các cạnh trong $E'$ khỏi $E$ ($E \leftarrow E \setminus E'$). - Cho biết tổng trọng số của $E'$ sau mỗi lần thực hiện thao tác. | Subtask | Điểm | Giới hạn | | :--- | :--- | :--- | | $1$ | $20$% | $n \le 10^5, m \le 5 \times 10^5; k = 1$ | | $2$ | $25$% | $n \le 10^3, m \le 10^4; k \le 1000$ | | $3$ | $35$% | $n \le 10^3, m \le 5 \times 10^5; k \le 10^4$ | | $4$ | $20$% | $n \le 10^5, m \le 5 \times 10^5; k \le 10^4$ | ### Minh Họa Sample :::spoiler Sample 1 - Minh hoạ đồ thị sau mỗi lần thực hiện thao tác ![sample_01](https://hackmd.io/_uploads/ryW-FJe0el.gif) - Minh họa tập cạnh được chọn bởi các thao tác ![sample_01_summary](https://hackmd.io/_uploads/r1tW21xAel.png) ::: :::spoiler Sample 2 - Minh họa đồ thị sau mỗi lần thực hiện thao tác ![sample_02](https://hackmd.io/_uploads/S1SWKyxCeg.gif) - Minh họa tập cạnh được chọn bởi các thao tác ![sample_02_summary](https://hackmd.io/_uploads/BJweiJeCgg.png) ::: ## Lời Giải ### [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1) **Giới hạn:** $n \le 10^5, m \le 5 \times 10^5; k = 1$ #### Ý Tưởng - Vì số thao tác cần phải thực hiện $k$ là $1$, bài toán có thể được quy về tìm cây khung lớn nhất (ngược với [bài toán tìm cây khung nhỏ nhất](https://wiki.vnoi.info/algo/graph-theory/minimum-spanning-tree.md)) trong đồ thị đã cho bởi: - Vì tập cạnh được chọn, tạm gọi là $E'$, không được chứa chu trình, $E'$ phải là một [rừng](https://en.wikipedia.org/wiki/Tree_(graph_theory)). - Hơn nữa, vì đồ thị ban đầu liên thông, để tổng trọng số là lớn nhất, $E'$ phải là một cây khung (rừng liên thông gồm $n - 1$ cạnh). Nếu $E'$ không liên thông, ta có thể thêm một cạnh mới mà không tạo ra chu trình, làm tăng tổng trọng số. Điều này mâu thuẫn với giả thiết tối ưu. - Để tìm cây khung lớn nhất, ta có thể sử dụng [thuật toán Kruskal](https://cp-algorithms.com/graph/mst_kruskal.html) với một thay đổi nhỏ: Thay vì sắp xếp các cạnh theo thứ tự tăng dần trọng số, ta sắp xếp giảm dần. :::spoiler Chứng minh ##### Cách chứng minh thứ nhất (ngắn gọn) - Bài toán tìm cây khung lớn nhất trên đồ thị $G = (V, E)$ có thể được quy về bài toán tìm cây khung nhỏ nhất trên đồ thị $G' = (V, E)$ với cùng tập đỉnh và cạnh, nhưng với trọng số mới được đảo dấu ($w'_e = -w_e$ cho mọi cạnh $e \in E$). Khi đó, cây khung nhỏ nhất của $G'$ cũng chính là cây khung lớn nhất của $G$. - [Thuật toán Kruskal](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm) có thể được dùng để tìm cây khung nhỏ nhất với các cạnh được duyệt theo thứ tự trọng số tăng dần. Khi áp dụng thuật toán trên đồ thị $G'$, thứ tự này tương đương với việc duyệt các cạnh của $G$ theo thứ tự trọng số giảm dần. ##### Cách chứng minh thứ hai (chi tiết) >[!Note] Lưu ý: >Cách chứng minh này phần lớn giống với cách minh tính đúng đắn của [thuật toán Kruskal](https://cp-algorithms.com/graph/mst_kruskal.html). - Ta có thể chứng minh tính đúng đắn của ý tưởng trên bằng cách chứng minh quy nạp theo số lượng cạnh đã chọn với mệnh đề sau: *"Với tập cạnh $M$ được chọn bởi thuật toán tại mọi thời điểm, đồ thị luôn chứa một cây khung lớn nhất với tập cạnh $T$ chứa mọi cạnh của $M$"*. - **Trường hợp cơ sở**: Mệnh đề trên hiển nhiên đúng với trường hợp ban đầu, khi $M$ không chứa bất kì cạnh nào bởi tập rỗng luôn là tập con của bất kì cây khung lớn nhất nào. - **Bước quy nạp:** Khi ta xem xét có nên thêm cạnh $e$ vào $M$ đang thỏa mệnh đề trên không, ta có ba trường hợp sau: 1. $M \cup \{e\}$ chứa chu trình (thêm cạnh $e$ vào $M$ tạo nên một chu trình): Thuật toán sẽ không thêm $e$ vào $M$. Vì vậy, mệnh đề sẽ vẫn thỏa bởi $T$ cũng không thể chứa $e$ ($T$ là một cây khung nên không chứa chu trình). 2. $T$ chứa $e$: Mệnh đề trên tiếp tục thỏa. 3. $T$ không chứa $e$: Khi thêm cạnh $e$ vào $T$, ta có $T \cup {e}$ chứa một chu trình duy nhất có chứa cạnh $e$. Chu trình này phải chứa ít nhất một cạnh $f$ không ở trong $M$. Nếu không, việc thêm $e$ vào $M$ đã tạo chu trình, tức rơi vào trường hợp đầu tiên. Vì cạnh $f$ đã không được chọn bởi thuật toán trước đó, trọng số của cạnh $f$ không thể lớn hơn trọng số cạnh $e$ ($w_f \leq w_e$). Khi này, ta nhận thấy rằng cây khung với tập cạnh $T' = T \cup \{e\} \setminus \{f\}$ cũng là cây khung lớn nhất bởi tổng trọng số của $T'$ không nhỏ hơn tổng trọng số của $T$. Mệnh đề vì thế vẫn tiếp tục được thỏa bởi $M \cup \{e\}$ là tập con của tập cạnh $T'$ của một cây khung lớn nhất. ::: #### Độ Phức Tạp - Ý tưởng trên có thể được cài đặt với độ phức tạp thời gian $O(m \log m + n)$ bằng cách sử dụng cấu trúc dữ liệu [Disjoint Set Union](https://cp-algorithms.com/graph/mst_kruskal_with_dsu.html) để quản lý các thành phần liên thông như trong bài toán tìm [cây khung nhỏ nhất](https://en.wikipedia.org/wiki/Minimum_spanning_tree). :::spoiler Giải thích - Bước khởi tạo cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) có thể có độ phức tạp $O(n)$. - Bước sắp xếp cạnh có độ phức tạp $O(m \log m)$. - Với mỗi lần xem xét một cạnh $e = (u, v)$, ta cần kiểm tra hai đỉnh của cạnh $e$ có cùng thuộc một thành phần liên thông không và nếu không thuộc thì ta có gộp hai thành phần liên thông chứa $u$ và $v$ lại. Nếu ta cài đặt cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) với cả hai phương pháp gộp theo kính cỡ và nén đường đi, một lần xem xét thêm cạnh như trên có độ phức tạp $O(\alpha(n))$ với $\alpha$ là hàm [Ackermann](https://en.wikipedia.org/wiki/Ackermann_function) nghịch đảo. Vì ta duyệt $m$ cạnh, bước xém xét hết tất cả các cạnh sẽ có độ phức tạp $O(m \cdot \alpha(n))$. Độ phức tạp tổng lúc này sẽ là $O(m \log m) + O(m \cdot \alpha(n)) + O(n) = O(m \log m + n)$. >[!Warning] Lưu ý: >Nếu ta chỉ dùng một trong phương pháp nêu trên khi cài đặt cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union), một lần xem xét thêm cạnh như trên có độ phức tạp $O(\log(n))$. Khi này, độ phức tạp tổng sẽ là $O(m \cdot (\log(m) + \log(n)) + n)$. ::: - Với cách cài đặt này, độ phức tạp bộ nhớ là $O(n + m)$. :::spoiler Giải thích - Lưu danh sách cạnh chiếm cần $O(m)$ bộ nhớ. - Cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) cần $O(n)$ bộ nhớ. ::: #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; class DisjointSetUnion { private: mutable vector<int> roots; public: DisjointSetUnion() {}; DisjointSetUnion(const int n): roots(n, -1) {}; int getRoot(const int x) const { return (roots[x] < 0) ? x : (roots[x] = getRoot(roots[x])); } bool checkConnected(const int x, const int y) const { return getRoot(x) == getRoot(y); } bool unite(int x, int y) { x = getRoot(x); y = getRoot(y); if (x == y) return false; if (roots[x] > roots[y]) swap(x, y); roots[x] += roots[y]; roots[y] = x; return true; } }; constexpr int MAX_N = 1E5 + 5, MAX_M = 1E5 + 5, MAX_K = 1E4 + 4; int n, m, k; vector<tuple<int, int, int> > edges; void input() { cin >> n >> m >> k; edges.resize(m); for (auto &[w, u, v] : edges) cin >> u >> v >> w; } void solve() { DisjointSetUnion dsu(n + 1); long long result = 0; sort(edges.rbegin(), edges.rend()); for (const auto &[w, u, v] : edges) if (dsu.unite(u, v)) result += w; cout << result << '\n'; } signed main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); solve(); return 0; } ``` ::: ### [Subtask 2](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-2) **Giới hạn:** $n \le 10^3, m \le 10^4; k \le 1000$ #### Ý Tưởng - Giới hạn của [Subtask 2](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-2) cho phép chúng ta thực hiện ý tưởng đề cập trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1) $k$ lần mà vẫn đảm bảo được độ phức tạp thời gian cho phép. Cụ thể hơn, với mỗi thao tác, ta có thể thực hiện lại ý tưởng trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1) rồi xóa tập cạnh được chọn bởi thao tác khỏi đồ thị để ta có thể xử lý các thao tác sau. >[!Note] Lưu ý: >Vì sau khi xóa cạnh khỏi đồ thị, đồ thị có thể không liên thông nữa, tập cạnh mà chúng ta cần tìm lúc này có thể là một [rừng](https://en.wikipedia.org/wiki/Tree_(graph_theory)) thay vì chỉ một cây khung: với mỗi thành phần liên thông trong đồ thị hiện tại, ta cần tìm cây khung lớn nhất trong thành phần liên thông đó; tập cạnh chúng ta cần tìm sẽ là tập cạnh được tạo bởi các cạnh của các khung đó. #### Độ Phức Tạp - Ý tưởng trên có thể được cài đặt với độ phức tạp $O(m \log m + k \cdot (n + m \cdot \alpha(n)))$. - Độ phức tạp bộ nhớ sẽ tương tự như trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1), tức $O(n + m)$. #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; class DisjointSetUnion { private: mutable vector<int> roots; public: DisjointSetUnion(const int n): roots(n, -1) {}; void reload() { fill(roots.begin(), roots.end(), -1); } int getRoot(const int x) const { return (roots[x] < 0) ? x : (roots[x] = getRoot(roots[x])); } bool checkConnected(const int x, const int y) const { return getRoot(x) == getRoot(y); } bool unite(int x, int y) { x = getRoot(x); y = getRoot(y); if (x == y) return false; if (roots[x] > roots[y]) swap(x, y); roots[x] += roots[y]; roots[y] = x; return true; } }; constexpr int MAX_N = 1E5 + 5, MAX_M = 1E5 + 5, MAX_K = 1E4 + 4; int n, m, k; vector<tuple<int, int, int> > edges; void input() { cin >> n >> m >> k; edges.resize(m); for (auto &[w, u, v] : edges) cin >> u >> v >> w; } void solve() { DisjointSetUnion dsu(n + 1); vector<bool> removed(m); long long result = 0; sort(edges.rbegin(), edges.rend()); for (int t = 0; t < k; ++t) { result = 0; dsu.reload(); for (int i = 0; i < m; ++i) { if (removed[i]) continue; const auto &[w, u, v] = edges[i]; if (dsu.unite(u, v)) { removed[i] = true; result += w; } } cout << result << '\n'; } } signed main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); solve(); return 0; } ``` ::: ### [Subtask 3](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-3) **Giới hạn:** $n \le 10^3, m \le 5 \times 10^5; k \le 10^4$ #### Ý Tưởng - Đặt $S_{i, j}$ (với $1 \leq i \leq k$ và $0 \leq j \leq m$) là tập cạnh được chọn trong thao tác thứ $i$ sau khi đã xét $j$ cạnh trong danh sách các cạnh được sắp xếp theo trọng số giảm dần. :::spoiler Chi tiết và ví dụ >[!Warning] Lưu ý: >Phần này trình bày chi tiết cách xác định tập cạnh $S_{i, j}$ theo định nghĩa trên. Nội dung này có thể không bắt buộc phải đọc để hiểu ý tưởng chính, mà chỉ nhằm làm rõ hơn định nghĩa $S_{i, j}$. **Chi Tiết** - Với $j = 0$, ta có $S_{i,j} = \emptyset$ vì chưa có cạnh nào được xét ở thời điểm bắt đầu thao tác thứ $i$. - Với $1 \leq j \leq m$, đặt $e$ là cạnh thứ $j$. Khi đó, ta có thể xác định $S_{i, j}$ như sau: - $S_{i, j} = S_{i, j - 1}$ nếu một trong hai điều kiện sau thỏa: - Cạnh $e$ đã được chọn trước đó: $\exists z ((1 \leq z < i) \land (e \in S_{z,j}))$ - Việc thêm cạnh $e$ sẽ tạo thành một chu trình trong tập hiện tại: $S_{i, j - 1} \cup \{e\}$ chứa ít nhất một chu trình. - $S_{i, j} = S_{i, j - 1} \cup \{e\}$ trong trường hợp còn lại. **Ví dụ** - Với [test ví dụ thứ hai](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Sample-2), các cạnh sau khi được sắp xếp giảm dần theo trọng số có thứ tự như sau: $$ \begin{array}{|c|*{8}{c|}} \hline \textbf{i} & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 \\ \hline e_i & (1,2) & (2,3) & (3,4) & (4,5) & (1,5) & (2,5) & (1,3) & (2,4) \\ \hline w_i & 9 & 8 & 7 & 6 & 5 & 4 & 3 & 2 \\ \hline \end{array} $$ - Giá trị $S_{i, j}$ được tính như bảng sau: $$ \begin{array}{|c|*{3}{l|}} \hline \textbf{$j \backslash i$} & \textbf{1} & \textbf{2} & \textbf{3} \\ \hline 0 & \emptyset & \emptyset & \emptyset \\ \hline 1 & \{(1,2)\} & \emptyset & \emptyset \\ \hline 2 & \{(1,2), (2,3)\} & \emptyset & \emptyset \\ \hline 3 & \{(1,2), (2,3), (3,4)\} & \emptyset & \emptyset \\ \hline 4 & \{(1,2), (2,3), (3,4), (4,5)\} & \emptyset & \emptyset \\ \hline 5 & \{(1,2), (2,3), (3,4), (4,5)\} & \{(1, 5)\} & \emptyset \\ \hline 6 & \{(1,2), (2,3), (3,4), (4,5)\} & \{(1, 5), (2, 5)\} & \emptyset \\ \hline 7 & \{(1,2), (2,3), (3,4), (4,5)\} & \{(1, 5), (2, 5), (1, 3)\} & \emptyset \\ \hline 8 & \{(1,2), (2,3), (3,4), (4,5)\} & \{(1, 5), (2, 5), (1, 3), (2, 4)\} & \emptyset \\ \hline \end{array} $$ ::: - Ta nhận thấy rằng nếu cạnh thứ $j$, gọi là $e_j$, được chọn trong bất kì một thao tác $i$ nào thì $i$ chính là giá trị nhỏ nhất sao cho với mọi thao tác $z$ trước $i$ ($1 \leq z < i$), việc thêm $e_j$ vào $S_{z, j - 1}$ sẽ tạo thành chu trình. Nếu không, cạnh $e_j$ đã nên được chọn trước thao tác $i$. Ta có: $$i = \min\{x \mid \forall z ((1 \leq z < x) \rightarrow C(S_{z, j - 1} \cup \{e_j\})) \}$$ với $C$ là mệnh đề *"đồ thị con tạo bởi tập cạnh $A$ chứa ít nhất một chu trình"*. - Dựa vào nhận xét trên, ta có thể duyệt qua từng cạnh và dùng [kỹ thuật chặt nhị phân](https://wiki.vnoi.info/algo/basic/Binary-Search) để xác định thao tác sớm nhất có thể chứa cạnh đó mà không tạo chu trình. Để kiểm tra việc một chu trình có thể xuất hiện hay không khi thêm một cạnh nào đó, với mỗi thao tác, ta sử dụng một cấu trúc dữ liệu để quản lý thành phần liên thông trong suốt quá trình duyệt qua các cạnh. **Ý tưởng cài đặt**: Ta có thể sử dụng $k$ cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) để quản lý thành phần liên thông giữa $n$ đỉnh cho $k$ thao tác. #### Độ Phức Tạp - Ý tưởng trên có thể được cài đặt với độ phức tạp $O(m \log m + k \cdot n + m \log k \cdot \alpha(n))$. :::spoiler Giải thích - Bước sắp xếp cạnh có độ phức tạp $O(m \log m)$ (tương tự như trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1)). - Việc khởi tạo $k$ cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) để quản lý thành phần liên thông trên $n$ đỉnh có độ phức tạp $O(k \cdot n)$. - Bước xử lý các cạnh có độ phức tạp $O(m \log k \cdot \alpha(n))$. - Với mỗi lần xét cạnh, chúng ta tìm được thao tác (nếu tồn tại) mà chứa được cạnh đang xét sau tối đa $O(\log k)$ bước chặt nhị phân. - Với mỗi lần chặt nhị phân, việc kiểm tra việc hai đỉnh của cạnh đang xét có đang thuộc cùng một thành phần liên thông có độ phức tạp $O(\alpha(n))$ (tương tự như trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1)). - Chúng ta phải xét qua $O(m)$ cạnh. - Ngoài ra, chúng ta có thể phải cập nhật thành phần liên thông cuối mỗi lần xét cạnh với độ phức tạp $O(\alpha(n))$. Độ phức tạp tổng vì thế là $O(m \cdot (\log k \cdot \alpha(n) + \alpha(n))) = O(m \log k \cdot \alpha(n))$ ::: - Với cách cài đặt này, độ phức tạp bộ nhớ là $O(m + k \cdot n)$. :::spoiler Giải thích - Lưu danh sách cạnh chiếm cần $O(m)$ bộ nhớ (tương tự như trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1)). - Lưu $k$ cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) cần $O(k \cdot n)$ bộ nhớ. ::: #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; class DisjointSetUnion { private: mutable vector<int> roots; public: DisjointSetUnion() {}; DisjointSetUnion(const int n): roots(n, -1) {}; int getRoot(const int x) const { return (roots[x] < 0) ? x : (roots[x] = getRoot(roots[x])); } bool checkConnected(const int x, const int y) const { return getRoot(x) == getRoot(y); } bool unite(int x, int y) { x = getRoot(x); y = getRoot(y); if (x == y) return false; if (roots[x] > roots[y]) swap(x, y); roots[x] += roots[y]; roots[y] = x; return true; } }; constexpr int MAX_N = 1E5 + 5, MAX_M = 1E5 + 5, MAX_K = 1E4 + 4; int n, m, k; long long result[MAX_K]; vector<DisjointSetUnion> graphs; vector<tuple<int, int, int> > edges; void input() { cin >> n >> m >> k; edges.resize(m); for (auto &[w, u, v] : edges) cin >> u >> v >> w; } void solve() { int low = 0, middle = 0, high = 0; graphs.resize(k + 1, DisjointSetUnion(n + 1)); sort(edges.rbegin(), edges.rend()); for (const auto &[w, u, v] : edges) { low = 0; high = k + 1; while (low + 1 < high) { middle = low + high >> 1; if (graphs[middle].checkConnected(u, v)) low = middle; else high = middle; } if (high <= k) { graphs[high].unite(u, v); result[high] += w; } } for (int i = 1; i <= k; ++i) cout << result[i] << '\n'; } signed main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); solve(); return 0; } ``` ::: ### [Subtask 4](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-4) **Giới hạn:** $n \le 10^5, m \le 5 \times 10^5; k \le 10^4$ #### Ý Tưởng >[!Note] Lưu ý: >Ý tưởng chính sử dụng trong [Subtask](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-4) này cũng giống như trong [Subtask 3](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-3). Điểm khác biệt chính nằm ở hướng cài đặt ý tưởng. - :::spoiler Nhận xét về độ phức tạp của cách cài đặt cho Subtask 3 Với [độ phức tạp](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#%C4%90%E1%BB%99-Ph%E1%BB%A9c-T%E1%BA%A1p26) của [cách cài đặt](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#%C3%9D-T%C6%B0%E1%BB%9Fng25) cho [Subtask 3](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-3), ta nhận thấy rằng việc quản lý các thành phần liên thông cho từng thao tác, là phần có chi phí tính toán nặng trong trường hợp tệ nhất (với độ phức tạp $O(k \cdot n)$). Mặt khác, các bước xử lý còn lại có chi phí tính toán chấp nhận được. Vì vậy, nếu ta muốn cải thiện ý tưởng đó, ta cần phải tối ưu những bước xử lý liên quan tới quản lý thành phần liên thông. ::: - Ta có một số nhận xét như sau: - Với cách cài đặt đề cập trong [Subtask 3](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-3), ta cần phải quan lý tổng cộng $k \cdot n$ đỉnh (mỗi cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) quản lý $n$ đỉnh). Tuy nhiên, trong suốt quá trình xử lý $m$ cạnh, chỉ có tối đa $2 \cdot m$ đỉnh thuộc các thành phần liên thông có kích thước lớn hơn $1$. :::spoiler Giải thích Mỗi cạnh được xét nếu được thêm vào chỉ có thể làm tăng số lượng đỉnh nằm trong một thành phần liên thông không đơn lẻ thêm tối đa $2$ đỉnh ::: - Khi kiểm tra hai đỉnh $u$ và $v$ khác nhau có thuộc cùng một thành phần liên thông hay không, ta nhận thấy rằng: *nếu một trong hai đỉnh thuộc một thành phần có kích thước bằng $1$ thì $u$ và $v$ chắc chắn không cùng thành phần liên thông*. $\Rightarrow$ Dựa trên những nhận xét trên, ta chỉ cần quản lý các thành phần liên thông có kích thước ít nhất là $2$ đỉnh, và xử lý riêng các trường hợp thành phần liên thông chỉ có một đỉnh. Về **mặt cài đặt**, ta chỉ cần thêm đỉnh vào cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) khi ta cần gộp thành phần liên thông của đỉnh đó với đỉnh khác. #### Độ Phức Tạp - Ý tưởng trên có thể được cài đặt với độ phức tạp $O(m \log m + k + m \log k \cdot \alpha(n))$. :::spoiler Giải thích - Việc quản lý các thành phần liên thông giờ đây chỉ có độ phức tạp $O(k)$ để khởi tạo và $O(m)$ để thêm đỉnh khi xử lý tất cả $m$ cạnh. Vì thế độ phức tạp tổng của cách cài đặt là $O(m \log m) + O(k) + O(m) + O(m \log k \cdot \alpha(n)) = O(m \log m + k + m \log k \cdot \alpha(n))$ ::: - Với cách cài đặt này, độ phức tạp bộ nhớ là $O(m + k)$. :::spoiler Giải thích - Lưu danh sách cạnh chiếm cần $O(m)$ bộ nhớ như trong [Subtask 1](https://hackmd.io/gh_66Q-xSKqyFfNKiMSmPQ#Subtask-1). - Lưu $k$ cấu trúc dữ liệu [Disjoint Set Union](https://wiki.vnoi.info/algo/data-structures/disjoint-set-union) cần $O(k)$ bộ nhớ và cần thêm $O(m)$ bộ nhớ do chúng ta có thể thêm tối đa $2 \cdot m$ đỉnh. $\Rightarrow$ Độ phức tạp tổng của cách cài đặt là $O(m) + O(m) + O(k) = O(m + k)$. ::: #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; class DisjointSetUnion { private: mutable vector<int> roots; public: DisjointSetUnion() {}; int getRoot(const int x) const { return (roots[x] < 0) ? x : (roots[x] = getRoot(roots[x])); } bool checkConnected(const int x, const int y) const { return getRoot(x) == getRoot(y); } int addVertex() { const int result = roots.size(); roots.push_back(-1); return result; } bool unite(int x, int y) { x = getRoot(x); y = getRoot(y); if (x == y) return false; if (roots[x] > roots[y]) swap(x, y); roots[x] += roots[y]; roots[y] = x; return true; } }; class UndirectedGraph { private: unordered_map<int, int> ids; DisjointSetUnion dsu; public: bool checkConnected(const int x, const int y) const { const auto a = ids.find(x); if (a == ids.end()) return false; const auto b = ids.find(y); if (b == ids.end()) return false; return dsu.checkConnected(a -> second, b -> second); } int getID(const int x) { const auto a = ids.find(x); if (a == ids.end()) return ids[x] = dsu.addVertex(); return a -> second; } bool unite(const int x, const int y) { return dsu.unite(getID(x), getID(y)); } }; constexpr int MAX_N = 1E5 + 5, MAX_M = 1E5 + 5, MAX_K = 1E4 + 4; int n, m, k; long long result[MAX_K]; vector<UndirectedGraph> graphs; vector<tuple<int, int, int> > edges; void input() { cin >> n >> m >> k; edges.resize(m); for (auto &[w, u, v] : edges) cin >> u >> v >> w; } void solve() { int low = 0, middle = 0, high = 0; graphs.resize(k + 1); sort(edges.rbegin(), edges.rend()); for (const auto &[w, u, v] : edges) { low = 0; high = k + 1; while (low + 1 < high) { middle = low + high >> 1; if (graphs[middle].checkConnected(u, v)) low = middle; else high = middle; } if (high <= k) { graphs[high].unite(u, v); result[high] += w; } } for (int i = 1; i <= k; ++i) cout << result[i] << '\n'; } signed main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); solve(); return 0; } ``` :::

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully