โจทย์ข้อนี้ต้องการให้เขียน Segment Tree ซึ่งเป็นโครงสร้างข้อมูลที่มีประสิทธิภาพในการทำ Range Query (Query แบบช่วง) บน Array โดยเฉพาะในกรณีที่ข้อมูลที่ถูก Query อาจถูก Update
Segment Tree
สมมุติต้องการทำ Range Query บน Array โครงสร้างข้อมูล Segment Tree จะรองรับ Operation สองแบบคือ:
- Query() หาค่าสูงสุดใน
- Update() เปลี่ยนค่าของ ให้เป็น โดย Operation ทั้งสองมี Time Complexity และใช้ Memory Complexity
หลักการทำงานของ Segment Tree
Segment Tree เป็นโครงสร้างข้อมูลที่เก็บในลักษณะ Binary Tree กล่าวคือแต่ละ Node จะมี Child Node ได้อย่างมาก 2 ตัว
แต่ละ Node ของ Segment Tree จะรับผิดชอบข้อมูลในช่วง และจะเก็บค่าสูงสุดในช่วงนั้น โดย Root จะรับผิดชอบทั้ง Array
Node ที่รับผิดชอบ Subarray ขนาด 1 (กล่าวคือ ) จะเป็น Leaf Node (ไม่มี Child Node)
ส่วน Node ที่รับผิดชอบ Subarray ขนาด 2 ขึ้นไปจะมี Child Node 2 ตัว โดย Child Node ซ้ายจะรับผิดชอบช่วง และ Child Node ขวารับผิดชอบ เมื่อ (ปัดลง)
สังเกตได้ว่าช่วงที่รับผิดชอบของ Node ในแต่ละ Layer ของ Tree จะมีขนาดที่ลดลง (เกือบ) ครึ่งหนึ่งในทุก Layer จึงทำให้ Segment Tree มี Depth อย่างมาก นอกจากนี้จำนวน Node ทั้งหมดจะไม่เกิน เนื่องจากเป็น Binary Tree ที่มี Node ตัว
Query
ในขั้นตอน Query(, ) สามารถจำแนก Node ที่รับผิดชอบ เป็น 3 เคส คือ
- กล่าวคือช่วงที่รับผิดชอบของ Node นี้ทั้งหมดอยู่ใน Query
- หรือ กล่าวคือช่วงที่ถูก Query ไม่ตัดกับช่วงที่รับผิดชอบของ Node
- เคสนอกเหนือจาก 1 และ 2 กล่าวคือช่วงที่ถูก Query มีส่วนที่ตัดกับช่วงที่รับผิดชอบของ Node แต่ช่วงของ Node ไม่ได้อยู่ใน Query ทั้งช่วง
เราสามารถเขียน Query หาค่าสูงสุดใน ด้วย Recursive Function บน Node ที่ให้คำตอบว่าค่าสูงสุดในช่วง Query ที่อยู่ในช่วงที่รับผิดชอบของ Node ที่กำลัง Query (กล่าวคือในช่วง ) คือเท่าไหร่
หากเริ่ม Query จาก Root จะได้คำตอบของ Query ที่ต้องการว่าค่าสูงสุดใน คือเท่าไหร่เพราะ Root รับผิดชอบทั้ง Array
สำหรับเคส 1 หรือ 2 เมื่อเรียก Query เข้ามีที่ Node นี้จะ return ได้เลย โดยจะ return สำหรับเคส 1 และ return สำหรับเคส 2
สำหรับเคส 3 จะต้องเรียก Query อีกใน Child Node ทั้งสองฝั่ง และ return ค่ามากสุดที่ได้
ภาพประกอบการ Query: Node ที่เข้าเคส 1 เป็นสีฟ้า เคส 2 เป็นสีส้ม เคส 3 เป็นสีเขียว Node สีขาวที่เหลือคือ Node ที่ไม่ถูกเรียก Query
ในเรียก Query แต่ละครั้งจะใช้เวลา ในแต่ละ Node เพราะมีเพียงการ return ค่าหรือการทำ ซึ่งต่างใช้เวลา Time Complexity
Node ที่เข้าเคส 3 จะมีได้อย่างมาก Layer ละ 2 Node เพราะ Node ที่เข้าเคส 3 จะต้องมีจุดปลายของ Query ในช่วง ถึง และ Query มีเพียงสองจุดปลายคือ และ ดังนั้นจึงมีเพียง ตัวที่เข้าเคส 3 เพราะ Segment Tree มี Depth อย่างมาก
สังเกตว่าทุก Node ในเคส 1 หรือ 2 ที่ถูกเรียก Query จะต้องถูกเรียกมาจากเคส 3 (หรือเป็น Root) จึงได้ว่า การ Query จึงมี Time Complexity ตามจำนวน Node ที่เข้าเคสที่ 3
Update
สำหรับการ Update(, ) สามารถจำแนก Node ที่รับผิดชอบ เป็น 3 เคสเช่นเดียวกับ Query คือ
- กล่าวคือช่วงที่รับผิดชอบของ Node คือเพียง
- หรือ กล่าวคือ ไม่ได้อยุ่ในช่วงที่รับผิดชอบของ Node
- เคสนอกเหนือจาก 1 และ 2 กล่าวคือ อยู่ในช่วงที่รับผิดชอบของ Node แต่ Node ไม่ได้รับผิดชอบเพียง
เราสามารถเขียน Update เป็น Recursive Function ที่เริ่มจาก Root เช่นกัน โดย Function นี้จะแก้ไขค่าในแต่ละ Node ที่ถูกเรียกเมื่อแก้ เป็น
สำหรับเคส 1 เราจะตั้ง และ return
สำหรับเคส 2 เนื่องจากช่วงที่รับผิดชอบไม่ถูกกระทบโดยการเปลี่ยนแปลงค่าที่อยู่นอกช่วงจึงสามารถ return ค่าเดิมคือ โดยไม่ต้องแก้ไข
สำหรับเคส 3 จะต้องเรียก Update ใน Child Node ทั้งสองและตั้ง เป็น ของค่าที่ได้จาก Child Node ทั้งสอง
ภาพประกอบการ Update: Node ที่เข้าเคส 1 และ 3 ที่มีการเปลี่ยนแก้ไขเป็นสีเขียว เคส 2 ที่ไม่มีการแก้ไขเป็นสีส้ม
การ Update มี Time Complexity เช่นเดียวกับการ Query และสามารถวิเคราะห์ในรูปแบบเดียวกัน
ตัวอย่าง Implementation
ใน Implementation มาตรฐานของ Segment Tree เราจะให้การ Index ตัว Node คล้าย Heap กล่าวคือสำหรับ Node ที่มี Index จะให้ลูกซ้ายเป็น Node และลูกขวาเป็น (การ Index เช่นนี้จะทำให้ Index สูงสุดอาจถึง ไม่ใช่ ตามจำนวน Node จริง)
Root จะมี Index เป็น 1
สำหรับ Node ที่รับผิดชอบ เราจะเก็บค่า ใน
int S[262144 * 4 + 100]; int update(int i, int Z, int n, int l, int r) { if (l == i && i == r) { // เคส 1 S[n] = Z; return S[n]; } if (r < i || i < l) // เคส 2 return S[n]; // เคส 3 int mid = (l + r) / 2; int new_left_value = update(i, Z, n * 2, l, mid); int new_right_value = update(i, Z, n * 2 + 1, mid + 1, r); S[n] = max(new_left_value, new_right_value); return S[n]; } int query(int A, int B, int n, int l, int r) { if (A <= l && r <= B) // เคส 1 return S[n]; if (B < l || r < A) // เคส 2 return -1000000001; // -inf // เคส 3 int mid = (l + r) / 2; int left_query = query(A, B, n * 2, l, mid); int right_query = query(A, B, n * 2 + 1, mid + 1, r); return max(left_query, right_query); }
ในโค้ดนี้ คือ Index ของ Node ที่กำลัง Query / Update และ กับ คื่อช่วงที่ Node นั้นรับผิดชอบ
ในการเรียก update
หรือ query
เพื่อทำ Operation นั้นสำหรับทั้ง Array จะต้องระบุ เพื่อให้เริ่มที่ Root และระบุช่วง เช่น update(3, 5, 1, 1, N)
เป็นการตั้งค่าช่อง 3 เป็น 5
Solution
เมื่อเรามีตัว Segment Tree แล้ว สำหรับโจทย์ข้อนี้เพียงต้องเรียกใช้ update
สำหรับคำสั้ง U และ query
สำหรับคำสั่ง P
int main() { int N, Q; cin >> N >> Q; for (int x = 0; x < Q; x++) { char c; cin >> c; if (c == 'U') { int i, Z; cin >> i >> Z; update(i, Z, 1, 1, N); } else { int A, B; cin >> A >> B; cout << query(A, B, 1, 1, N) << "\n"; } } }
มีคำสั่ง คำสั่ง และแต่ละคำสั่งใช้เวลา ไม่ว่าจะเป็น U หรือ P ดังนั้น Time Complexity ของข้อนี้คือ