--- tags: VNOJ, Template, Data Structure, CQTshadow, SPyofgame title: 🌱 Dynamic Median author: Editorial-Slayers Team license: Public domain --- <style> .markdown-body { max-width: 2048px; } h1 { color: blue; font-family: verdana; font-size: 500%; } h2 { color: red; font-family: verdana; font-size: 400%; } h3 { color: gray; font-size: 300%; font-family : verdana; } h4 { font-family : verdana; font-size: 200%; } </style> $\Huge \text{🌱 Dynamic Median}$ ----- ###### 🔗 Link: [https://oj.vnoi.info/problem/medquery](https://oj.vnoi.info/problem/medquery) ###### 📌 Tags: `Template`, `Data Structure` ###### 👤 Writter: @CQTshadow ###### 👥 Contributor: @SPyofgame ###### 📋 Content: [TOC] ----- # Nhận xét - Bài toán tưởng chừng khá quen thuộc, nhưng nếu code không cẩn thận thì sẽ vẫn bị $TLE$ vì test khá mạnh. - Có rất nhiều cách để giải quyết bài toán này, ở đây tôi sẽ cung cấp cho các bạn cách làm sử dụng $2$ tập hợp. # Hướng dẫn ## I. Giải quyết bài toán ### 1. Bản chất - Bản chất chính, ta sẽ chia tập hợp đề bài của ta thành $2$ và duy trì $2$ tập hợp đó sao cho phần tử lớn nhất của tập hợp nhỏ hơn là $median$ của chúng ta : - Giả sử tập hợp của chúng ta là $S = \{s_1, s_2, ..., s_k\}$ - Tập hợp $Small$ : Lưu trữ các phần tử có vị trí thuộc khoảng $\left[1, \left \lfloor\frac{k + 1}{2}\right \rfloor\right]$ - Tập hợp $Big$ : Lưu trữ các phần tử có vị trí thuộc khoảng $\left[\left \lfloor\frac{k + 1}{2}\right \rfloor + 1, k\right]$ $\Longrightarrow$ Để duy trì như yêu cầu, ta chỉ cần đảm bảo $0 \leq Small_{size} - Big_{size} \leq 1$ ### 2. Cài đặt - Ta có thể dùng **CTDL** $std::set$ hoặc bất kỳ **CTDL** nào phù hợp với cách cài đặt. #### a. Thao Tác Cân Bằng - Như đã nói, ta cần duy trì sự cân bằng của $2$ tập hợp trên, do đó chỉ có $2$ trường hợp sai cần phải thay đổi : - **Trường hợp 1** : $Small_{size} > Big_{size} + 1$ $\Longrightarrow$ Khi đó ta lấy **phần tử lớn nhất** ở tập hợp $Small$ thêm vào tập hợp $Big$ - **Trường hợp 2** : $Small_{size} < Big_{size}$ $\Longrightarrow$ Khi đó ta lấy **phần tử bé nhất** ở tập hợp $Big$ thêm vào tập hợp $Small$ - Ta sẽ thêm thao tác này sau mọi thao tác thay đổi khác. :::success :::spoiler Code ```cpp = void Balance() { if(sizSmall > sizBig + 1) { Big.push(Small.top()), sizBig++; Small.pop(), sizSmall--; } else if(sizSmall < sizBig) { Small.push(Big.top()), sizSmall++; Big.pop(), sizBig--; } } ``` ::: #### b. Thao Tác Thêm - Ta cần xác định sẽ thêm phần tử $x$ mới vào tập hợp nào. - Gọi $median$ là trung vị hiện tại của chúng ta, như đã nói nó sẽ là phần tử lớn nhất của tập hợp $Small$. - Khi đó như cách ta sắp xếp hai tập hợp: - Nếu phần tử $x$ có giá trị **lớn hơn** $median$ $\Longrightarrow$ Ta thêm nó vào tập hợp $Big$. - Ngược lại $\Longrightarrow$ Ta thêm nó vào tập hợp $Small$. **Lưu ý : Nhớ xét riêng trường hợp $Small$ rỗng** :::success :::spoiler Code ```cpp = void add() { if(Small.empty()) Small.push(x), sizSmall++; else { int median = Small.top(); if(x > median) Big.push(x), sizBig++; else Small.push(x), sizSmall++; } Balance(); } ``` ::: #### c. Thao Tác Xóa - Ta có thể đơn giản sử dụng các lệnh tìm kiếm có sẵn ở trong **CTDL** đang sử dụng để tìm phần tử $x$ cần xóa xuất hiện ở đâu trong tập hợp sau đó xóa nó. #### d. Thao Tác Tìm Trung Vị - Ta chỉ cần gọi phần tử lớn nhất của tập hợp $Small$. ## II. Cải Tiến - Tưởng chừng như cách giải quyết trên đã đủ để **AC**, nhưng test lại đủ mạnh để khiến cho ta bị **TLE**, do đó ta cần các cải tiến. ### 1. Cải Tiến Thao Tác Xóa - Nhìn nhận điều đặc biệt mà bài toán cho ta, ta thấy số $x$ mà đề yêu cầu xóa luôn tồn tại trong tập hợp $\Longrightarrow$ **Ta có thể lưu lại và xóa đi các phần tử cần xóa sau**. - Thật vậy, khi ta đã xác định phần tử giá trị $x$ nằm ở tập hợp nào, thì ta chỉ cần **ghi nợ** phần tử đó vào tập hợp, và trong các quá trình thao tác trong tương lai, nếu ta phát hiện phần tử **đáng ra đã bị xóa** thì ta sẽ xóa nó đi mà không ảnh hưởng tới các thao tác khác. - Mục đích chính của việc **lưu nợ** này lại là vì ta sẽ đợi các phần tử cần xóa mà hiện tại đang ở giữa đâu đó trong $2$ tập hợp chính *(khiến việc xóa mất $O(log_2 \> n)$)* xuất hiện ở đầu $2$ tập hợp *(việc xóa chỉ mất $O(1)$)* - Ta sẽ **ghi nợ** bằng cách tạo một tập hợp với **thứ tự sắp xếp** tương tự tập hợp ta đang nợ, và các tập hợp này sẽ chỉ lưu đúng **các giá trị chưa được xóa**, và sau mọi thao tác thay đổi, ta sẽ gọi thao tác **trả nợ** này ra để xem xem có phần tử nào cần xóa đã xuất hiện ở hai đầu tập hợp hay chưa, và nếu có thì xóa chúng đi. **Lưu ý : Ta sẽ cần phải có một giá trị quản lý $size$ riêng ở mỗi tập hợp, vì nếu gọi thao tác $std::size()$ thì sẽ sai vì nó sẽ bao gồm cả phần tử "đáng ra đã bị xóa" bị tính vào.** :::success :::spoiler Code ```cpp = void Paid() { while(!OweBig.empty() && OweBig.top() == Big.top()) OweBig.pop(), Big.pop(); while(!OweSmall.empty() && OweSmall.top() == Small.top()) OweSmall.pop(), Small.pop(); } void rev() { if(x == Small.top()) Small.pop(), sizSmall--; else if(x < Small.top()) OweSmall.push(x), sizSmall--; else if(x == Big.top()) Big.pop(), sizBig--; else if(x > Big.top()) OweBig.push(x), sizBig--; Paid(); Balance(); } ``` ::: ### 2. Cải Tiến Xuất - Một mẹo nhỏ khiến thời gian giảm đi đôi chút là ta sẽ lưu kết quả vào một mảng $ans$ trước rồi mới xuất ra. :::success :::spoiler Code ```cpp = for(int i = 1 ; i <= q ; ++i) { cin >> c >> x; if(c == '+') add(); else rev(); ans[i] = Small.top(); } for(int i = 1 ; i <= q ; ++i) cout << ans[i] << '\n'; ::: # Code Minh Họa > **Time:** $O(n \log_2 n)$ > **Space:** $O(n)$ > [color=lightgreen] :::success :::spoiler CQTshadow Code ```cpp= #include <bits/stdc++.h> #pragma GCC optimize("O2") using namespace std; const int N = 1e5 + 7; const double eps = 1e-9; template <typename X> using V = vector<X>; template <typename X> bool minimize(X &a, const X b) { if(a > b + eps) return a = b, 1; else return 0; } template <typename X> bool maximize(X &a, const X b) { if(a + eps < b) return a = b, 1; else return 0; } int q, sizSmall, sizBig, x; char c; priority_queue<int, V<int>, greater<int>> Big, OweBig; priority_queue<int> Small, OweSmall; void Paid() { while(!OweBig.empty() && OweBig.top() == Big.top()) OweBig.pop(), Big.pop(); while(!OweSmall.empty() && OweSmall.top() == Small.top()) OweSmall.pop(), Small.pop(); } void Balance() { if(sizSmall > sizBig + 1) { Big.push(Small.top()), sizBig++; Small.pop(), sizSmall--; } else if(sizSmall < sizBig) { Small.push(Big.top()), sizSmall++; Big.pop(), sizBig--; } Paid(); } void add() { if(Small.empty()) Small.push(x), sizSmall++; else { int median = Small.top(); if(x > median) Big.push(x), sizBig++; else Small.push(x), sizSmall++; } Balance(); } void rev() { if(x == Small.top()) Small.pop(), sizSmall--; else if(x < Small.top()) OweSmall.push(x), sizSmall--; else if(x == Big.top()) Big.pop(), sizBig--; else if(x > Big.top()) OweBig.push(x), sizBig--; Paid(); Balance(); } int ans[1000002]; int main () { ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin >> q; for(int i = 1 ; i <= q ; ++i) { cin >> c >> x; if(c == '+') add(); else rev(); ans[i] = Small.top(); } for(int i = 1 ; i <= q ; ++i) cout << ans[i] << '\n'; } ``` ::: :::success :::spoiler SPyofgame Dynamic Median Template ```cpp= /* @Author: SPyofgame @License: Free to use @Tag: Dynamic Median Data Structure */ #include <iostream> #include <cassert> #include <vector> #include <queue> using namespace std; /// Data Structure that /// - Insert element in O(log size) /// - Erase element in O(log size) /// - Find median in O(log size) /// - Constant Factor: As high as C++std::priority_queue template<typename T> struct Dynamic_Median { /// Diffrent Size | Expected Ratio: |L| >= |R| & |L| - |R| <= 1 int diff = 0; /// Value can be duplicated /// Value Array priority_queue<T, vector<T>, less<T> > L; priority_queue<T, vector<T>, greater<T> > R; /// Erasing Queue priority_queue<T, vector<T>, less<T> > LEQ; priority_queue<T, vector<T>, greater<T> > REQ; /// Balance two array with expected ratio: |L| >= |R| & |L| - |R| <= 1 /// Return value: void /// Parameter: NULL /// Complexity: O(log size) /// Requirement: ALL OTHER FUNCTION use this on top /// Run-time-error: |L| = 0 or |R| = 0 while balancing /// Suspect (diff) behaved wrongly void balance() { if (diff > 1) { assert(L.size()); R.push(L.top()); L.pop(); diff -= 2; } else if (diff < 0) { L.push(R.top()); R.pop(); diff += 2; } while (REQ.size() && REQ.top() == R.top()) REQ.pop(), R.pop(); while (LEQ.size() && LEQ.top() == L.top()) LEQ.pop(), L.pop(); } /// Find median of array S = (L + R) /// Return value: S[n / 2] if n = |S| is even /// S[(n - 1) / 2] if n = |S| is odd /// /// Parameter: NULL /// Complexity: O(log size) /// Run-time-error: Balance() behaved wrongly /// |L| is empty after balancing T median() { balance(); assert(L.size()); return L.top(); } /// Find median of array S = (L + R) /// Return value: S[n / 2] if n = |S| is even /// S[(n + 1) / 2] if n = |S| is odd /// /// Parameter: NULL /// Complexity: O(log size) /// Run-time-error: Balance() behaved wrongly /// |R| is empty after balancing T median2() { balance(); assert(R.size()); return R.top(); } /// Insert (value) to array /// Return value: void /// Parameter: <T> value /// Complexity: O(log size) /// Run-time-error: Balance() behaved wrongly void insert(const T &value) { balance(); if (L.empty() || value <= median()) L.push(value), ++diff; else R.push(value), --diff; } /// Erase (value) from array /// Return value: void /// Parameter: <T> value /// Complexity: O(log size) /// Run-time-error: Balance() behaved wrongly /// |L| = 0 or |R| = 0 while erasing void erase(const T &value) { balance(); if (value <= median()) /// If median() cost is high { /// Then please precalculate it --diff; (value == median()) ? L.pop() : LEQ.push(value); } else { ++diff; (value == median2()) ? R.pop() : REQ.push(value); } } }; int main() { ios::sync_with_stdio(NULL); cin.tie(NULL); int q; cin >> q; Dynamic_Median<int> S; while (q-->0) { char operation; cin >> operation; int value; cin >> value; if (operation == '+') S.insert(value); else S.erase(value); cout << S.median() << "\n"; } return 0; } ``` :::