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 New
    • Engagement control
    • Make a copy
    • 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 Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy 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
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Coding Challenge #4 ## Đề Bài :::spoiler [Đề bài gốc](https://oj.vnoi.info/problem/coding_challenge_coin) có thể được đọc trên hệ thống [VNOJ](https://oj.vnoi.info/contest/coding_challenge_4) # **Coding Challenge #4 - Cân xu** Bổn Sâm có $n$ đồng xu **khác nhau về trọng lượng** và một cái cân hai bên. Mỗi khi anh ta đặt đồng xu $x$ và $y$ lên cân, trọng lượng tương đối giữa chúng sẽ được hiển thị - tức là anh ta biết được giữa $x$ và $y$, đồng nào nặng hơn. Gọi **cấp bậc** của đồng xu $x$ là số lượng đồng xu *không nặng hơn* đồng xu $x$ (bao gồm cả chính nó). Do đó, đồng xu nhẹ nhất có cấp bậc là $1$, đồng nhẹ nhì là $2$, ..., và đồng nặng nhất có cấp bậc là $n$. Sẽ có $m$ lần cân. Với mỗi đồng xu, hãy xác định sau **lần cân thứ** $m$ thì có thể chắc chắn biết cấp bậc của đồng xu đó. ## Input Dòng đầu tiên nhập hai số nguyên $n$, $m$. Tiếp theo là $m$ dòng, dòng thứ $i$ nhập hai số nguyên $x$, $y$, cho biết kết quả lần cân thứ $i$ là **đồng xu $x$ nặng hơn đồng xu $y$**. Đảm bảo rằng các kết quả cân **không mâu thuẫn** với nhau. ## Output In ra một dòng gồm $n$ số nguyên. Số thứ $i$ là **lần cân sớm nhất** sau đó có thể xác định chắc chắn **cấp bậc của đồng xu $i$**. Nếu **không thể xác định**, in `–1` tại vị trí đó. ## Scoring ### Giới hạn dữ liệu - $2 \leq n \leq 200,000$ - $1 \leq m \leq 800,000$ - $1 \leq x, y \leq n$, $x \neq y$ ### Subtask - Subtask $1$ ($6$ điểm): $n \leq 7$, $m \leq 20$ - Subtask $2$ ($16$ điểm): $n \leq 100$, $m \leq 400$ - Subtask $3$ ($10$ điểm): $n \leq 1000$, $m \leq 4000$ - Subtask $4$ ($68$ điểm): không có ràng buộc gì thêm ### Lưu ý Trong mỗi subtask, nếu bạn **chỉ xác định đúng rằng cấp bậc của từng đồng xu có thể hay không thể được xác định**, nhưng **không xác định đúng là được xác định sau lần cân thứ mấy**, bạn vẫn sẽ nhận được **$50$% số điểm** của subtask đó. ## Sample ### Sample Input 1 ``` 4 4 2 4 3 1 4 1 2 3 ``` ### Sample Output 1 ``` 3 4 -1 -1 ``` ### Sample Input 2 ``` 6 8 1 5 5 4 6 2 2 5 4 3 6 1 6 5 2 1 ``` ### Sample Output 2 ``` 8 8 5 5 5 6 ``` ### Notes #### Giải thích ví dụ $1$ Ta có thể xác định rằng sau $3$ lần cân đầu tiên, đồng xu $1$ là đồng xu nặng nhất, nhưng không thể biết điều đó chỉ sau $2$ lần cân. Do đó, số nguyên đầu tiên trong kết quả đúng là $3$. Tương tự, ta có thể xác định rằng đồng xu $2$ là đồng xu nhẹ nhất sau $4$ lần cân, nhưng không phải sau $3$ lần cân. Vì vậy, số nguyên thứ hai trong kết quả đúng là $4$. Lưu ý rằng cả hai thứ tự $2, 4, 3, 1$ và $2, 3, 4, 1$ đều là các cách sắp xếp hợp lệ của trọng lượng các đồng xu. Do đó, đồng xu $3$ có thể có cấp bậc $2$ hoặc $3$, đều phù hợp với tất cả các kết quả cân, vì vậy cấp bậc của đồng xu $3$ không bao giờ được xác định chắc chắn. Tương tự, đồng xu $4$ cũng không bao giờ có cấp bậc xác định. Nếu kết quả của bạn là `1 1 -1 -1`, bạn sẽ đạt được $50\%$ số điểm cho test nhỏ này. ::: ## Lời Giải ### [Subtask 1](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-1) **Giới hạn:** $n \leq 7$; $m \leq 20$ #### Ý Tưởng - Đặt $P$ là thứ tự của các đồng xu được sắp xếp tăng dần theo cấp bậc của chúng. Với định nghĩa này, ta có thể biểu diễn $P$ bằng một hoán vị của $n$ số tự nhiên đầu tiên. :::spoiler Ví dụ Với $n = 3$, $P = (2, 3, 1)$ có thể được hiểu như: - Đồng xu $2$ có cấp bậc là $1$; - Đồng xu $3$ có cấp bậc là $2$; - Đồng xu $1$ có cấp bậc là $3$. ::: - Đặt $e_i = (x_i, y_i)$ là cặp đồng xu được cân ở lần cân thứ $i$ ($1 \leq i \leq m$; đồng xu $x_i$ nặng hơn đồng xu $y_i$). Đặt $E_j = \{e_i \vert 1 \leq i \leq j \}$ là tập hợp cặp đồng xu của $i$ lần cân đầu tiên. - Ta gọi một thứ tự $P$ là hợp lệ với một tập hợp lần cân $E$ nếu không tồn tại một cặp đồng xu nào mà thứ tự của chúng không thỏa kết quả của một lần cân nào đó trong $E$. Nói cách khác, nếu $(x, y) \in E$ thì vị trí của đồng xu $y$ phải nằm phía trước vị trí của đồng xu $x$ trong $P$ (đồng xu $x$ có cấp bậc cao hơn đồng xu $y$). Vì dữ liệu đầu vào đảm bảo rằng các kết quả cân không mâu thuẫn với nhau, ít nhất một thứ tự hợp lệ phải tồn tại với tập hợp $m$ lần cân ($E_m$). - Cấp bậc của đồng xu $z$ là có thể chắc chắn xác định được với tập hợp lần cân $E$ **nếu trong mọi thứ tự $P$ hợp lệ so với $E$, cấp bậc của $z$ không đổi** (tạm gọi đây là điều kiện $(1)$). Vì vậy, nếu ta chỉ kiểm tra rằng cấp bậc của từng đồng xu có thể hay không thể được xác định (và đạt $50$% số điểm của [subtask 1](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-1)), ta có thể: - Duyệt qua tất cả $n!$ hoán vị để tìm các thứ tự hợp lệ; và - Với mỗi đồng xu, ta kiểm tra liệu có tồn tại hai thứ tự hợp lệ khác nhau mà cấp bậc của đồng xu trong hai thứ tự đó là khác nhau. - Với mỗi đồng xu $z$, việc tìm được thời điểm sớm nhất mà có thể chắc chắn xác định cấp bậc của đồng xu đó sẽ tương đương với việc tìm ra tập hợp gồm các lần cân đầu tiên với kích thước nhỏ nhất sao cho điều kiện $(1)$ vẫn thỏa. Nói cách khác, ta cần tìm $i$ nhỏ nhất sao cho với $E_i$, cấp bậc $z$ có thể chắc chắn xác định. Để thực hiện điều này, ta có thể duyệt qua một tập hợp lần cân và thực hiện kiểm tra như trên. #### Độ Phức Tạp - Ý tưởng nêu trên có thể được cài đặt với độ phức tạp thời gian $O(m \cdot n! \cdot (m + n))$. - Ta cần duyệt qua $O(m)$ tập hợp lần cân. - Với mỗi tập hợp, ta cần duyệt qua $O(n!)$ hoán vị để tìm các thứ tự hợp lệ. - Với mỗi thứ tự, ta thực hiện kiểm tra tính hợp lệ bằng cách duyệt qua $O(m)$ lần cân. Nếu thứ tự hợp lệ, chúng ta cần phải kiểm tra cấp bậc của $O(n)$ đồng xu có khác so với thứ tự hợp lệ khác không. #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; int n, m, x[MAX_M], y[MAX_M]; void input() { cin >> n >> m; for (int i = 0; i < m; ++i) { cin >> x[i] >> y[i]; --x[i]; --y[i]; } } vector<int> findPositions(const vector<int> &permutation) { const int n = permutation.size(); vector<int> positions(n); for (int i = 0; i < n; ++i) positions[permutation[i]] = i; return positions; } bool checkValid(const vector<int> &order, const int e) { const vector<int> &positions(findPositions(order)); for (int i = 0; i < e; ++i) if (positions[x[i]] > positions[y[i]]) return false; return true; } bool checkInvalid(const vector<int> &order, const int e) { return !checkValid(order, e); } vector<int> findLevels(const int e) { vector<int> levels(n, n), permutation(n); /** level = -1 : The level cannot be determined level = n : The level has not been determined yet (as there is no contradiction, there should no final result with this value) 0 <= level < n : The level is currently determined **/ iota(permutation.begin(), permutation.end(), 0); do { if (checkInvalid(permutation, e)) continue; for (int i = 0; i < n; ++i) { const auto &value = permutation[i]; auto &level = levels[value]; if (level < 0) continue; if (level == n) { level = i; continue; } if (level != i) level = -1; } } while (next_permutation(permutation.begin(), permutation.end())); return levels; } vector<int> findWhen() { vector<int> result(n, -1); for (int e = 1, i = 0; e <= m; ++e) { const auto &levels(findLevels(e)); for (i = 0; i < n; ++i) { if (result[i] >= 0 || levels[i] < 0) continue; result[i] = e; } } return result; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); for (const int &e : findWhen()) cout << e << ' '; cout << '\n'; return 0; } ``` ::: ### [Subtask 2](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-2) **Giới hạn:** $n \leq 100$; $m \leq 400$ #### Ý Tưởng - Ta có thể biểu diễn mối quan hệ trọng lượng tương đối bằng cách dựng một đồ thị. Cụ thể hơn, đặt $G = (V, E)$ với: - $V$ là tập hợp các đồng xu - $E$ là tập hợp các lần cân: $(x, y) \in E$ (tồn tại một cung trực tiếp từ $x$ tới $y$) nếu như tồn tại một lần cân xác định được rằng đồng xu $x$ nặng hơn đồng xu $y$. - Ta nhận thấy rằng: đồng xu $x$ có thể được xác định nặng hơn đồng xu $y$ khi đồ thị $G$ phải chứa ít nhất một đường đi từ $x$ tới $y$. :::spoiler Giải thích - Giả sử đường đi từ $x$ tới $y$ là một dãy các đỉnh $U_1, ..., U_k$ (với $k \geq 2$, $U_1 = x$, $U_k = y$) thì theo cách ta dựng đồ thị, đồng xu $U_i$ phải nặng hơn đồng xu $U_{i + 1}$ (với $1 \leq i < k$). Vì thế, dựa vào tích chất bắc cầu, ta có đồng xu $U_1$ (hay $x$) phải nặng hơn đồng xu $U_k$ (hay $y$). ::: - Tập hợp các đỉnh mà có thể **đến được từ đỉnh $x$** cũng đồng thời là tập hợp các đỉnh đại diện cho đồng xu có thể xác định **nhẹ** hơn đồng xu $x$. - Ta có thể xác định được tập hợp này bằng cách sử dụng [các thuật toán duyệt đồ thị](https://en.wikipedia.org/wiki/Graph_traversal) như [BFS](https://en.wikipedia.org/wiki/Breadth-first_search) hoặc [DFS](https://en.wikipedia.org/wiki/Depth-first_search) trên đồ thị $G$. - Tương tự, tập hợp các đỉnh mà có thể **đi tới được đỉnh $x$** cũng đồng thời là tập hợp các đỉnh đại diện cho đồng xu nhẹ có thể xác định **nặng** đồng xu $x$. - Để xác định được tập hợp này, ta cũng có thể sử dụng [các thuật toán duyệt đồ thị](https://usaco.guide/silver/graph-traversal?lang=cpp) như trên nhưng trên [một đồ thị có chiều các cạnh được đảo so với $G$](https://en.wikipedia.org/wiki/Transpose_graph#:~:text=In%20the%20mathematical%20and%20algorithmic,the%20corresponding%20edges%20in%20G.). - Cụ thể hơn, ta có thể đặt $G^T = (V, E^T)$ với $E^T = \{(y, x) \vert (x, y) \in E\}$ :::spoiler Minh họa đồ thị được dựng từ sample - Đồ thị được dựng từ [sample $1$](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ?both#Sample-Input-1) ![Slide1](https://hackmd.io/_uploads/rykf6JPxWg.png) - Đồ thị được dựng từ [sample $2$](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ?both#Sample-Input-2) ![Slide2](https://hackmd.io/_uploads/BkUGTkveZl.png) ::: - Vì vậy, nếu ta chỉ muốn kiểm tra rằng cấp bậc của một đồng xu $z$ có thể hay không thể được xác định (và đạt $50$% số điểm của [subtask 2](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-2)), ta có thể thực hiện bằng cách kiểm tra **phép hợp của hai tập hợp đến được từ $z$ và có thể đi đến được $z$ có chứa mọi đỉnh của đồ thị đã cho không** (cấp bậc của đồng xu $z$ chỉ có thể xác định khi trong lượng tương đối giữa nó và các đồng xu khác đều có thể xác định). Tạm gọi điều kiện của việc kiểm tra này là điều kiện $(2)$. - Tương tự như [subtask 1](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-1), với mỗi đồng xu $z$, việc tìm được thời điểm sớm nhất mà có thể chắc chắn xác định cấp bậc của đồng xu đó sẽ tương đương với việc tìm ra tập hợp gồm các lần cân đầu tiên với kích thước nhỏ nhất sao cho điều kiện $(2)$ vẫn thỏa. #### Độ Phức Tạp - Ý tưởng nêu trên có thể được cài đặt với độ phức tạp thời gian $O(n \cdot m \cdot (n + m))$. - Ta cần duyệt qua $O(n)$ đỉnh đại diện cho các đồng xu. - Với mỗi đỉnh, ta cần duyệt qua $O(m)$ tập hợp lần cân. - Với mỗi tập hợp lần cân, việc duyệt đồ thị có thể được thể được hiện tại với thuật toán [BFS](html) hoặc [DFS](https://cp-algorithms.com/graph/depth-first-search.html) với độ phức tạp thời gian $O(n + m)$. #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; vector<pair<int, int> > graph[MAX_N], reversedGraph[MAX_N]; int n, m, x[MAX_M], y[MAX_M]; void input() { cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> x[i] >> y[i]; graph[x[i]].emplace_back(i, y[i]); reversedGraph[y[i]].emplace_back(i, x[i]); } } namespace ReachabilityChecker { bool visited[MAX_N]; int limit; void runFirstDFS(const int x) { visited[x] = true; for (const auto &[e, y] : graph[x]) { if (visited[y] || e > limit) continue; runFirstDFS(y); } } void runSecondDFS(const int x) { visited[x] = true; for (const auto &[e, y] : reversedGraph[x]) { if (visited[y] || e > limit) continue; runSecondDFS(y); } } int check(const int source) { for (int i = 1; i <= n; ++i) visited[i] = false; runFirstDFS(source); runSecondDFS(source); for (int i = 1; i <= n; ++i) if (!visited[i]) return false; return true; } } void determineWhen() { vector<int> result(n + 1, -1); for (int e = 1, x = 0; e <= m; ++e) { ReachabilityChecker::limit = e; for (x = 1; x <= n; ++x) { if (result[x] > 0) continue; if (ReachabilityChecker::check(x)) result[x] = e; } } for (int i = 1; i <= n; ++i) cout << result[i] << ' '; cout << '\n'; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); determineWhen(); return 0; } ``` ::: ### [Subtask 3](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-3) **Giới hạn:** $n \leq 1000$; $m \leq 4000$ #### Ý Tưởng - [Subtask 3](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-3) có thể được giải quyết theo nhiều cách khác nhau. ##### Cách Thứ Nhất - Ta có nhận xét rằng, với đồng xu $z$, nếu cấp bậc của $z$ có thể được xác định với tập hợp $E_i$, tập hợp của $i$ lần cân đầu tiên, thì cũng có thể được xác định với $E_j$ (với $i < j \leq m$) vì $E_i \subset E_j$. Vì vậy, thay vì duyệt tuần tự từng tập hợp lần cân (mỗi lần xét một thêm kết quả cân) như trong [subtask 2](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ?both#Subtask-2), ta có thể sử dụng [thuật toán chặt nhị phân](https://wiki.vnoi.info/algo/basic/Binary-Search) để tìm được thời điểm sớm nhất. ##### Cách Thứ Hai - Ý tưởng của cách thứ hai gần giống với [thuật toán Prim](https://cp-algorithms.com/graph/mst_prim.html) (sử dụng trong cho bài toán tìm [cây khung nhỏ nhất](https://en.wikipedia.org/wiki/Minimum_spanning_tree)). Với mỗi cạnh $e_i$ trong đồ thị $G$ được định nghĩa trước đó, ta đặt trọng số của cạnh $e_i$ là $i$ (thời điểm cặp đồng xu mà $e_i$ đại diện được cân). - Với một đỉnh $z$, ta đặt $V^{+}_{z}$ là tập hợp các đỉnh có thể đến được từ $z$ trong $G$ (bao gồm cả $z$). Ta nhận thấy rằng việc tìm thời điểm sớm nhất mà tập hợp $V^{+}_{z}$ cũng sẽ tương đương với việc tối thiểu hóa trọng số lớn nhất trong cây chứa các cạnh $V^{+}_{z}$. - Tương tự, ta đặt $V^{-}_{z}$ là tập hợp các đỉnh có thể đến được từ $z$ trong $G^{T}$ (bao gồm cả $z$). Ta nhận thấy rằng việc tìm thời điểm sớm nhất mà tập hợp $V^{-}_{z}$ cũng sẽ tương đương với việc tối thiểu hóa trọng số lớn nhất trong cây chứa các cạnh $V^{-}_{z}$. - Để tối thiểu hóa trong cả hai trường hợp, ta đều có thể thực hiện bằng cách: - Khởi đầu với cây gồm một đỉnh $z$ duy nhất; - Lần lượt chọn cạnh với trọng số nhỏ nhất nối từ một đỉnh đã thuộc cây và một đỉnh chưa thuộc cây; Thêm cạnh và đỉnh của cạnh đó và cây; Thực hiện nhiều lần cho đến khi ta không thể mở rộng được cây nữa. - Nếu $V^{+}_{z} \cup V^{-}_{z} \neq V$ thì cấp bậc của $z$ không thể xác định. Ngược lại, cấp bậc của $z$ sẽ là giá trị lớn nhất trong hai trường hợp. #### Độ Phức Tạp - Ý tưởng trong cách thứ nhất có thể được cài đặt với độ phức tạp thời gian $O(n \cdot \log m \cdot (n + m))$. - Độ phức tạp có thể được phân tích gần như tương tự như trong [Subtask 2](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ?both#%C4%90%E1%BB%99-Ph%E1%BB%A9c-T%E1%BA%A1p23) nhưng khi này, ta chỉ xét $O(\log m)$ thay vì $O(m)$ tập hợp lần cân. - Ý tưởng trong cách thứ hai có thể được cài đặt với độ phức tạp thời gian $O(n \cdot (n + m \log m))$. - Ta phải xét qua $O(n)$ đỉnh. - Với mỗi đỉnh, việc xử lý thông tin các đỉnh có độ phức tạp $O(n)$. Nếu chúng ta sử dụng [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)) để xác định cạnh với trọng số nhỏ nhất, bước xử lý sẽ có độ phức tạp $O(m \log m)$ bởi chúng ta thêm $O(m)$ cạnh vào [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)) với mỗi thao có độ phức tạp $O(\log m)$. - Ngoài ra, ý tưởng trong cách thứ hai cũng có thể được cài đặt với độ phức tạp thời gian $O(n \cdot (n + m))$ nếu ta sử dụng một mảng danh sách đánh số theo chỉ số cạnh thay vì dùng [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)) bởi trọng số cạnh chỉ nằm trong đoạn từ $1$ tới $m$. #### Code Minh Họa :::spoiler Code C++ minh họa cho cách thứ nhất ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; vector<pair<int, int> > graph[MAX_N], reversedGraph[MAX_N]; int n, m, x[MAX_M], y[MAX_M]; void input() { cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> x[i] >> y[i]; graph[x[i]].emplace_back(i, y[i]); reversedGraph[y[i]].emplace_back(i, x[i]); } } namespace ReachabilityChecker { bool visited[MAX_N]; int limit; void runFirstDFS(const int x) { visited[x] = true; for (const auto &[e, y] : graph[x]) { if (visited[y] || e > limit) continue; runFirstDFS(y); } } void runSecondDFS(const int x) { visited[x] = true; for (const auto &[e, y] : reversedGraph[x]) { if (visited[y] || e > limit) continue; runSecondDFS(y); } } int check(const int source) { for (int i = 1; i <= n; ++i) visited[i] = false; runFirstDFS(source); runSecondDFS(source); for (int i = 1; i <= n; ++i) if (!visited[i]) return false; return true; } } void determineWhen() { for (int x = 1, low = 0, middle = 0, high = m; x <= n; ++x) { ReachabilityChecker::limit = high = m; if (!ReachabilityChecker::check(x)) { cout << -1 << ' '; continue; } low = 0; while (low + 1 < high) { middle = low + high >> 1; ReachabilityChecker::limit = middle; if (ReachabilityChecker::check(x)) high = middle; else low = middle; } cout << high << ' '; } cout << '\n'; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); determineWhen(); return 0; } ``` ::: :::spoiler Code C++ minh họa thứ nhất cho cách thứ hai ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; vector<pair<int, int> > graph[MAX_N], reversedGraph[MAX_N]; int n, m, x[MAX_M], y[MAX_M]; bool visited[MAX_N]; void input() { cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> x[i] >> y[i]; graph[x[i]].emplace_back(i, y[i]); reversedGraph[y[i]].emplace_back(i, x[i]); } } int findEarliestTime(const int source, vector<pair<int, int> > graph[]) { priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > heap; int maximum = 0; heap.emplace(0, source); visited[source] = false; for (; !heap.empty();) { const auto [t, x] = heap.top(); heap.pop(); if (visited[x]) continue; maximize(maximum, t); visited[x] = true; for (const auto &[t, y] : graph[x]) { if (visited[y]) continue; heap.emplace(t, y); } } return maximum; } int findEarliestTime(const int source) { for (int i = 1; i <= n; ++i) visited[i] = false; const int maximum = max(findEarliestTime(source, graph), findEarliestTime(source, reversedGraph)); for (int i = 1; i <= n; ++i) if (!visited[i]) return -1; return maximum; } void determineWhen() { for (int x = 1; x <= n; ++x) cout << findEarliestTime(x) << ' '; cout << '\n'; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); determineWhen(); return 0; } ``` ::: :::spoiler Code C++ minh họa thứ hai cho cách thứ hai ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; vector<pair<int, int> > graph[MAX_N], reversedGraph[MAX_N]; int n, m, x[MAX_M], y[MAX_M]; bool visited[MAX_N]; void input() { cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> x[i] >> y[i]; graph[x[i]].emplace_back(i, y[i]); reversedGraph[y[i]].emplace_back(i, x[i]); } } int findEarliestTime(const int source, vector<pair<int, int> > graph[]) { static queue<int> queues[MAX_M]; int maximum = 0; visited[source] = false; queues[0].push(source); for (int t = 0; t <= m; ++t) for (; !queues[t].empty(); queues[t].pop()) { const auto &x = queues[t].front(); if (visited[x]) continue; maximize(maximum, t); visited[x] = true; for (const auto &[e, y] : graph[x]) { if (visited[y]) continue; ((e <= t) ? queues[t] : queues[e]).push(y); } } return maximum; } int findEarliestTime(const int source) { for (int i = 1; i <= n; ++i) visited[i] = false; const int maximum = max(findEarliestTime(source, graph), findEarliestTime(source, reversedGraph)); for (int i = 1; i <= n; ++i) if (!visited[i]) return -1; return maximum; } void determineWhen() { for (int x = 1; x <= n; ++x) cout << findEarliestTime(x) << ' '; cout << '\n'; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); determineWhen(); return 0; } ``` ::: ### [Subtask 4](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-4) **Giới hạn:** $n \leq 200,000$; $m \leq 800,000$ #### Ý Tưởng - Vì dữ liệu đầu vào đảm bảo rằng các kết quả cân không mâu thuẫn với nhau, đồ thị $G$ được dựng trước đó sẽ không chứa [chu trình](https://en.wikipedia.org/wiki/Cycle_(graph_theory)). Điều này đồng nghĩa với việc $G$ là một [DAG (Directed acyclic graph)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). - Ta đặt $S$ là một [thứ tự Tô-pô](https://wiki.vnoi.info/algo/graph-theory/topological-sort.md) của $G$ và đặt $T_x$ là vị trí của $x$ trong thứ tự đó ($T_x$ có thể được xem như chỉ số của $x$ sau khi ta thực hiện [sắp xếp Tô-pô](https://cp-algorithms.com/graph/topological-sort.html)). - Để miêu tả phần còn lại của ý tưởng dễ hơn, ta sẽ đánh số đỉnh đồ thị lại theo [thứ tự Tô-pô](https://wiki.vnoi.info/algo/graph-theory/topological-sort.md) (với mọi cung $(x, y)$ trong $G$ sau khi đã đánh số lại, ta có $x < y$). - Ta đặt $R_x$ là chỉ số đỉnh nhỏ nhất mà có cung từ trực tiếp từ $x$ đến đỉnh đó (có thể xem như $R_x = +\infty$ nếu như $x$ không có cung đi ra). Ta có nhận xét rằng: **với mọi đỉnh $z < x$, nếu như $R_u \leq x$ với mọi $z \leq u \leq x$ thì phải tồn tại ít nhất một đường đi từ $z$ tới $x$**. :::spoiler Chứng minh Nhận xét trên có thể được chứng minh bằng cách quy nạp: - Bước cơ sở: với $z = x - 1$ - Vì $z < R_z$, ta có $x - 1 < R_z \leq x$. Nói cách khác, $R_z = x$ hay từ $z$ đến $x$ có một cung trực tiếp. - Bước quy nạp: giả định nhận xét trên thỏa với mọi $z < u \leq x$, ta cần chứng minh nhận xét trên cũng thỏa với $z$. - Vì $z$ có cung trực tiếp từ nó đến $R_z$ (theo như định nghĩa của $R_z$) và $R_z$ có đường đi từ nó tới $x$ (theo như giả định và $z < R_z \leq x$), $z$ có đường đi từ nó tới $x$. ::: - Tương tự, ta đặt $L_x$ là chỉ số đỉnh lớn nhất mà có cung từ trực tiếp từ đỉnh đó đến $x$ (có thể xem như $L_x = -\infty$ nếu như không có cung đi đến $x$). Ta có nhận xét rằng: **với mọi đỉnh $x< z$, nếu như $x \leq L_u$ với mọi $x \leq u \leq z$ thì phải tồn tại ít nhất một đường đi từ $x$ tới $z$**. :::spoiler Chứng minh Nhận xét trên có thể được chứng minh bằng cách quy nạp như nhận xét trước đó: - Bước cơ sở: với $z = x + 1$ - Vì $L_z < z$, ta có $x \leq L_z < x + 1$. Nói cách khác, $L_z = x$ hay từ $x$ đến $z$ có một cung trực tiếp. - Bước quy nạp: giả định nhận xét trên thỏa với mọi $x \leq u < z$, ta cần chứng minh nhận xét trên cũng thỏa với $z$. - Vì $L_z$ có cung trực tiếp từ nó đến $z$ (theo như định nghĩa của $L_z$) và $L_z$ có đường đi từ $x$ tới nó (theo như giả định và $x \leq L_z < z$), $z$ có đường đi từ $x$ tới nó. ::: - Dựa trên hai nhận xét này, nếu ta chỉ muốn kiểm tra rằng cấp bậc của một đồng xu $z$ (sau khi đã đánh số lại) có thể hay không thể được xác định (và đạt $50$% số điểm của [subtask 4](https://hackmd.io/_V4EhjBxT8aZfBSCMuNYmQ#Subtask-4)), ta cần kiểm tra cả hai điều kiện sau có thỏa không: - Với mọi đỉnh $u$ có chỉ số nhỏ hơn $z$, $R_u \leq z$. - Với mọi đỉnh $u$ có chỉ số lớn hơn $z$, $z \leq L_u$. - Để xác định chính xác thời điểm sớm nhất cấp bậc của một đỉnh có thể được xác định, nếu ta thực hiện kiểm tra nêu trên nhiều lần với các tập hợp lần cân khác nhau, chi phí tính sẽ quá lớn. - Ta đặt $C_z$ là số lượng đỉnh $u$ thỏa mãn điều kiện nếu ta đang xét đỉnh $z$ ($R_u \leq z$ nếu $u < z$ hoặc $z \leq L_u$ nếu $z < u$). Cấp bậc của đỉnh $z$ có thể xác định được ở thời điểm $C_z = n - 1$ sớm nhất. - Ta nhận thấy rằng, nếu ta bắt đầu với một đồ thị chưa có cạnh nào và thêm dần các cạnh theo thứ tự đề bài, giả sử cạnh đang xét là $(x, y)$ thì ta sẽ chứng kiến những thay đổi này sau đây: - $R_x$ có thể giảm thành $y$: Khi này, giá trị $C_u$ tăng thêm một đơn vị với $u$ trong đoạn từ $y$ tới $R_x - 1$. - $L_y$ có thể tăng thành $x$: Khi này, giá trị $C_u$ tăng thêm một đơn vị với $u$ trong đoạn từ $L_x + 1$ tới $y$. - Sau khi cập nhật các giá trị $C_u$ sau mỗi lần thêm cạnh, giá trị $C_u$ mà ta cần để ý cũng chính là giá trị $C_u$ lớn nhất (bởi $n - 1$ cũng là giá trị lớn nhất có thể của $C_u$). - Dựa vào những nhận xét này, ta nhận thấy ta có thể sử dụng [cấu trúc dữ liệu Segment Tree với kỹ thuật Lazy Propogation](https://wiki.vnoi.info/algo/data-structures/segment-tree-extend) để quản lý thông tin bởi: - Ta cần thực hiện update (tăng giá trị) các đoạn khác nhau - Ta cần tìm giá trị lớn nhất trên đoạn. #### Độ Phức Tạp - Ý tưởng nêu trên có thể được cài đặt với độ phức tạp thời gian $O((n + m) \log n)$. - Việc tìm [thứ tự Tô-pô](https://en.wikipedia.org/wiki/Topological_sorting) có thể được thực hiện trong $O(n + m)$. - Vì ta cần phải duyệt qua $O(m)$ cạnh và với mỗi cạnh, ta có thể update đoạn với độ phức tạp $O(\log n)$. - Ngoài ra, mỗi khi cấp bậc của một đỉnh được xác định, ta phải "xóa" đỉnh đó khỏi cấu trúc dữ liệu để có thể tìm được những đỉnh khác. Ta sẽ có $O(n)$ thao tác "xóa" như vậy và mỗi thao tác có thể có độ phức tạp $O(\log n)$. #### Code Minh Họa :::spoiler Code C++ minh họa ```c++ #include <bits/stdc++.h> using namespace std; template<class A, class B> bool maximize(A &a, const B &b) { return (a < b) ? (a = b, true) : false; } template<class A, class B> bool minimize(A &a, const B &b) { return (a > b) ? (a = b, true) : false; } constexpr int MAX_N = 200'005, MAX_M = 800'005, INF = 0X3F3F3F3F, NINF = 0XC0C0C0C0; class SegmentTree { private: vector<pair<int, int> > maximum; vector<int> lazyValue; int length; void pushDown(const int i) { if (lazyValue[i]) { int j = i << 1; maximum[j].first += lazyValue[i]; lazyValue[j] += lazyValue[i]; j |= 1; maximum[j].first += lazyValue[i]; lazyValue[j] += lazyValue[i]; lazyValue[i] = 0; } } void modify(const int q, const int value, const int i, const int l, const int r) { if (q < l || r < q) return; if (l == r) { maximum[i] = make_pair(value, r); return; } const int m = l + r >> 1; pushDown(i); modify(q, value, i << 1, l, m); modify(q, value, i << 1 | 1, m + 1, r); maximum[i] = max(maximum[i << 1], maximum[i << 1 | 1]); } void update(const int ql, const int qr, const int value, const int i, const int l, const int r) { if (qr < l || r < ql) return; if (ql <= l && r <= qr) { maximum[i].first += value; lazyValue[i] += value; return; } const int m = l + r >> 1; pushDown(i); update(ql, qr, value, i << 1, l, m); update(ql, qr, value, i << 1 | 1, m + 1, r); maximum[i] = max(maximum[i << 1], maximum[i << 1 | 1]); } void build(const int i, const int l, const int r) { if (l == r) { maximum[i].second = l; return; } const int m = l + r >> 1; build(i << 1, l, m); build(i << 1 | 1, m + 1, r); } public: SegmentTree(int length, const int value): length(length) { maximum.resize(length + 1 << 2, make_pair(value, -1)); lazyValue.resize(maximum.size()); build(1, 1, length); }; void modify(const int q, const int value) { modify(q, value, 1, 1, length); } void update(const int ql, const int qr, const int value) { update(ql, qr, value, 1, 1, length); } pair<int, int> getTop() const { return maximum[1]; } }; signed position[MAX_N], degree[MAX_N], order[MAX_N]; int n, m, x[MAX_M], y[MAX_M]; vector<int> graph[MAX_N]; void input() { cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> x[i] >> y[i]; graph[x[i]].push_back(y[i]); ++degree[y[i]]; } } void determineTopologicalOrder() { queue<int> q; int t = 0; for (int u = 1; u <= n; ++u) if (degree[u] == 0) q.push(u); for (; !q.empty(); q.pop()) { const int &u = q.front(); order[position[u] = ++t] = u; for (const auto &v : graph[u]) if ((--degree[v]) == 0) q.push(v); } } void determineTime() { SegmentTree segmentTree(n, 0); vector<int> result(n + 1, -1), maximum(n + 1, NINF), minimum(n + 1, INF); for (int i = 1, u = 0, v = 0; i <= m; ++i) { u = position[x[i]]; v = position[y[i]]; if (v <= minimum[u]) { segmentTree.update(v, minimum[u] - 1, 1); minimum[u] = v; } if (maximum[v] <= u) { segmentTree.update(maximum[v] + 1, u, 1); maximum[v] = u; } for (;;) { const auto &[f, z] = segmentTree.getTop(); if (f < n - 1) break; result[order[z]] = i; segmentTree.modify(z, NINF); } } for (int i = 1; i <= n; ++i) cout << result[i] << ' '; cout << '\n'; } int main() { #ifdef LOCAL freopen("input.INP", "r", stdin); #endif // LOCAL cin.tie(0) -> sync_with_stdio(0); cout.tie(0); input(); determineTopologicalOrder(); determineTime(); 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