Skip to content

Commit

Permalink
Add 1147 (#137)
Browse files Browse the repository at this point in the history
* Add 1147

* fix typos

* Add Explanation

* Add Explanation2

* Add Explanation for code usage

* minor edit
  • Loading branch information
Phluenam authored Oct 24, 2023
1 parent b913019 commit a4362cc
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 0 deletions.
142 changes: 142 additions & 0 deletions md/1147.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
โจทย์ข้อนี้ต้องการให้เขียน Segment Tree ซึ่งเป็นโครงสร้างข้อมูลที่มีประสิทธิภาพในการทำ Range Query (Query แบบช่วง) บน Array โดยเฉพาะในกรณีที่ข้อมูลที่ถูก Query อาจถูก Update

## Segment Tree

สมมุติต้องการทำ Range Query บน Array $C[1..N]$ โครงสร้างข้อมูล Segment Tree จะรองรับ Operation สองแบบคือ:
1. Query($A,B$) หาค่าสูงสุดใน $C[A..B]$
2. Update($i,Z$) เปลี่ยนค่าของ $C[i]$ ให้เป็น $Z$
โดย Operation ทั้งสองมี Time Complexity $\mathcal{O}(\log{}N)$ และใช้ Memory Complexity $\mathcal{O}(N)$

### หลักการทำงานของ Segment Tree

Segment Tree เป็นโครงสร้างข้อมูลที่เก็บในลักษณะ Binary Tree กล่าวคือแต่ละ Node จะมี Child Node ได้อย่างมาก 2 ตัว

![](../media/1147/1.png)

แต่ละ Node ของ Segment Tree จะรับผิดชอบข้อมูลในช่วง $C[l..r]$ และจะเก็บค่าสูงสุดในช่วงนั้น โดย Root จะรับผิดชอบทั้ง Array $C[1..N]$

Node ที่รับผิดชอบ Subarray ขนาด 1 (กล่าวคือ $l=r$) จะเป็น Leaf Node (ไม่มี Child Node)

ส่วน Node ที่รับผิดชอบ Subarray ขนาด 2 ขึ้นไปจะมี Child Node 2 ตัว โดย Child Node ซ้ายจะรับผิดชอบช่วง $C[l..mid]$ และ Child Node ขวารับผิดชอบ $C[(mid+1)..r]$ เมื่อ $mid=(l+r)/2$ (ปัดลง)

สังเกตได้ว่าช่วงที่รับผิดชอบของ Node ในแต่ละ Layer ของ Tree จะมีขนาดที่ลดลง (เกือบ) ครึ่งหนึ่งในทุก Layer จึงทำให้ Segment Tree มี Depth อย่างมาก $O(\log{}N)$ นอกจากนี้จำนวน Node ทั้งหมดจะไม่เกิน $2N-1$ เนื่องจากเป็น Binary Tree ที่มี Node $N$ ตัว

#### Query

ในขั้นตอน Query($A$, $B$) สามารถจำแนก Node ที่รับผิดชอบ $C[l..r]$ เป็น 3 เคส คือ

1. $A\leq l \leq r \leq B$ กล่าวคือช่วงที่รับผิดชอบของ Node นี้ทั้งหมดอยู่ใน Query
2. $B < l$ หรือ $r < A$ กล่าวคือช่วงที่ถูก Query ไม่ตัดกับช่วงที่รับผิดชอบของ Node
3. เคสนอกเหนือจาก 1 และ 2 กล่าวคือช่วงที่ถูก Query มีส่วนที่ตัดกับช่วงที่รับผิดชอบของ Node แต่ช่วงของ Node ไม่ได้อยู่ใน Query ทั้งช่วง

เราสามารถเขียน Query หาค่าสูงสุดใน $C[A..B]$ ด้วย Recursive Function บน Node ที่ให้คำตอบว่าค่าสูงสุดในช่วง Query ที่อยู่ในช่วงที่รับผิดชอบของ Node ที่กำลัง Query (กล่าวคือในช่วง $C[max(l,A)..min(r,B)]$) คือเท่าไหร่

หากเริ่ม Query จาก Root จะได้คำตอบของ Query ที่ต้องการว่าค่าสูงสุดใน $C[A..B]$ คือเท่าไหร่เพราะ Root รับผิดชอบทั้ง Array

สำหรับเคส 1 หรือ 2 เมื่อเรียก Query เข้ามีที่ Node นี้จะ return ได้เลย โดยจะ return $max(C[l..r])$ สำหรับเคส 1 และ return $-\infty$ สำหรับเคส 2

สำหรับเคส 3 จะต้องเรียก Query อีกใน Child Node ทั้งสองฝั่ง และ return ค่ามากสุดที่ได้

![](../media/1147/2.png)

ภาพประกอบการ Query: Node ที่เข้าเคส 1 เป็นสีฟ้า เคส 2 เป็นสีส้ม เคส 3 เป็นสีเขียว Node สีขาวที่เหลือคือ Node ที่ไม่ถูกเรียก Query

ในเรียก Query แต่ละครั้งจะใช้เวลา $\mathcal{O}(1)$ ในแต่ละ Node เพราะมีเพียงการ return ค่าหรือการทำ $max$ ซึ่งต่างใช้เวลา $\mathcal{O}(1)$ Time Complexity

Node ที่เข้าเคส 3 จะมีได้อย่างมาก Layer ละ 2 Node เพราะ Node ที่เข้าเคส 3 จะต้องมีจุดปลายของ Query ในช่วง $l$ ถึง $r$ และ Query มีเพียงสองจุดปลายคือ $A$ และ $B$ ดังนั้นจึงมีเพียง $\mathcal{O}(\log{}N)$ ตัวที่เข้าเคส 3 เพราะ Segment Tree มี Depth อย่างมาก $O(\log{}N)$

สังเกตว่าทุก Node ในเคส 1 หรือ 2 ที่ถูกเรียก Query จะต้องถูกเรียกมาจากเคส 3 (หรือเป็น Root) จึงได้ว่า การ Query จึงมี Time Complexity $O(\log{}N)$ ตามจำนวน Node ที่เข้าเคสที่ 3

#### Update

สำหรับการ Update($i$, $Z$) สามารถจำแนก Node ที่รับผิดชอบ $C[l..r]$ เป็น 3 เคสเช่นเดียวกับ Query คือ

1. $ l = i = r $ กล่าวคือช่วงที่รับผิดชอบของ Node คือเพียง $C[i]$
2. $B < i$ หรือ $i < A$ กล่าวคือ $C[i]$ ไม่ได้อยุ่ในช่วงที่รับผิดชอบของ Node
3. เคสนอกเหนือจาก 1 และ 2 กล่าวคือ $C[i]$ อยู่ในช่วงที่รับผิดชอบของ Node แต่ Node ไม่ได้รับผิดชอบเพียง $C[i]$

เราสามารถเขียน Update เป็น Recursive Function ที่เริ่มจาก Root เช่นกัน โดย Function นี้จะแก้ไขค่าในแต่ละ Node ที่ถูกเรียกเมื่อแก้ $C[i]$ เป็น $Z$

สำหรับเคส 1 เราจะตั้ง $max(C[i]) = Z$ และ return $Z$

สำหรับเคส 2 เนื่องจากช่วงที่รับผิดชอบไม่ถูกกระทบโดยการเปลี่ยนแปลงค่าที่อยู่นอกช่วงจึงสามารถ return ค่าเดิมคือ $max(C[l..r])$ โดยไม่ต้องแก้ไข

สำหรับเคส 3 จะต้องเรียก Update ใน Child Node ทั้งสองและตั้ง $max(C[l..r])$ เป็น $max$ ของค่าที่ได้จาก Child Node ทั้งสอง

![](../media/1147/3.png)
ภาพประกอบการ Update: Node ที่เข้าเคส 1 และ 3 ที่มีการเปลี่ยนแก้ไขเป็นสีเขียว เคส 2 ที่ไม่มีการแก้ไขเป็นสีส้ม

การ Update มี Time Complexity $O(\log{}N)$ เช่นเดียวกับการ Query และสามารถวิเคราะห์ในรูปแบบเดียวกัน

### ตัวอย่าง Implementation

ใน Implementation มาตรฐานของ Segment Tree เราจะให้การ Index ตัว Node คล้าย Heap กล่าวคือสำหรับ Node ที่มี Index $n$ จะให้ลูกซ้ายเป็น Node $2n$ และลูกขวาเป็น $2n+1$ (การ Index เช่นนี้จะทำให้ Index สูงสุดอาจถึง $4N$ ไม่ใช่ $2N$ ตามจำนวน Node จริง)

Root จะมี Index เป็น 1

สำหรับ Node $n$ ที่รับผิดชอบ $C[l..r]$ เราจะเก็บค่า $max(C[l..r])$ ใน $S[n]$

```cpp
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);
}
```
ในโค้ดนี้ $n$ คือ Index ของ Node ที่กำลัง Query / Update และ $l$ กับ $r$ คื่อช่วงที่ Node นั้นรับผิดชอบ
ในการเรียก `update` หรือ `query` เพื่อทำ Operation นั้นสำหรับทั้ง Array $C[1..N]$ จะต้องระบุ $n=1$ เพื่อให้เริ่มที่ Root และระบุช่วง $l=1, r=N$ เช่น `update(3, 5, 1, 1, N)` เป็นการตั้งค่าช่อง 3 เป็น 5
## Solution
เมื่อเรามีตัว Segment Tree แล้ว สำหรับโจทย์ข้อนี้เพียงต้องเรียกใช้ `update` สำหรับคำสั้ง U และ `query` สำหรับคำสั่ง P
```cpp
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";
}
}
}
```

มีคำสั่ง $Q$ คำสั่ง และแต่ละคำสั่งใช้เวลา ไม่ว่าจะเป็น U หรือ P ดังนั้น Time Complexity ของข้อนี้คือ $\mathcal{O}(Q\log{}N)$
Binary file added media/1147/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/1147/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/1147/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a4362cc

Please sign in to comment.