-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into add_o59_mar_c2_binary
- Loading branch information
Showing
19 changed files
with
484 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
โจทย์ข้อนี้มีคำสั่ง $N$ คำสั่ง โดยคำสั่งจะมีสองแบบ | ||
|
||
1. ใส่สินค้ามูลค่า $i$ เข้าไปในเครื่อง | ||
2. ถามว่าสินค้าที่มีมูลค่ามากสุดในเครื่องมีมูลค่าเท่าใดหากมีและเอาออกจากเครื่อง | ||
|
||
โจทย์ข้อนี้แก้ได้ด้วย Priority Queue ซึ่งเป็นโครงสร้างข้อมูลที่มีประสิทธิภาพในการหาค่าสูงสุดของชุดข้อมูลที่อาจมีการเพิ่มเข้าหรือลบออก | ||
|
||
ทั้งนี้ Priority Queue เป็นโครงสร้างข้อมูลที่มีอยู่ใน STL ซึ่งทำให้ข้อนี้แก้ได้ง่ายมากหากใช้ `std::priority_queue` โดยเพียงต้องใช้ฟังก์ชัน `push` เพื่อคำสั่งประเภทแรก และ `top` กับ `pop` สำหรับคำสั่งประเภทสอง | ||
|
||
เนื่องจากโจทย์ข้อนี้เป็นโจทย์ที่ต้องการสอนให้รู้จักการใช้ Priority Queue เฉลยนี้จะอธิบายโครงสร้าง Binary Heap ซึ่งเป็นวิธีที่พื้นฐานที่สุดสำหรับการเขียน Priority Queue (Priority Queue มีความหมายที่กว้างกว่า Binary Heap นอกจาก Binary Heap ยังมีโครงสร้างข้อมูลอื่นที่สามารถใช้เขียน Priority Queue เช่น Fibonacci Heap หรือ Binomial Heap) | ||
|
||
## Binary Heap | ||
|
||
Binary Heap ที่ต้องการในข้อนี้จะต้องรองรับ Operation สองแบบคือ: | ||
1. Push($i$) ใส่ข้อมูลที่มีค่า $i$ เข้าไปใน Heap | ||
2. Pop() return ค่าสูงสุดใน Heap และเอาข้อมูลนั้นออกจาก Heap | ||
โดย Operation ทั้งสองมี Time Complexity $\mathcal{O}(\log{}N)$ และการเก็บ Heap มี Memory Complexity $\mathcal{O}(N)$ (เมื่อ $N$ คือขนาดของ Heap) | ||
|
||
### หลักการทำงานของ Heap | ||
|
||
Heap เป็นโครงสร้างข้อมูลที่เก็บในลักษณะ Complete Binary Tree ซึ่งเป็น Binary Tree ประเภทหนึ่ง | ||
|
||
Binary Tree เป็น Tree ที่แต่ละ Node มีลูกอย่างมาก 2 ตัว | ||
|
||
Complete Binary Tree คือ Binary Tree ที่แต่ละชั้นจะมีจำนวน Node มากสุดที่เป็นไปได้ยกเว้นชั้นสุดท้ายซึ่งจะเก็บ Node ไว้ด้านซ้ายสุดก่อน นั่นคือในชั้นที่ $h$ ของ Binary Tree ที่มีชั้น $H$ เป็นชั้นสุดท้าย จะมี Node $2^h$ ตัวหาก $h < H$ และในชั้น $H$ มีได้ตั้งแต่ $1$ ถึง $2^H$ ตัว | ||
|
||
นอกจากนี้ Heap จะรักษาคุณสมบัติว่าสำหรับ Node $x$ ใดๆ ลูกของ Node $x$ จะมีค่าไม่เกินค่าของ Node $x$ ซึ่งจะทำให้ Node รากที่อยู่สูงสุดใน Heap เป็นค่าสูงสุดในทั้ง Heap | ||
|
||
 | ||
|
||
ในการ Implement โครงสร้างข้อมูล Binary Heap โดยทั่วไปจะเก็บเป็น Array จากช่องที่ $1$ ถึง $N$ โดยรากอยู่ที่ $1$ และให้ลูกขวาของ Node ที่ช่อง $x$ ที่ช่อง $2x$ และลูกซ้ายอยู่ที่ $2x+1$ (สังเกตตัวเลขแดงในภาพประกอบซึ่งแทนตำแหน่งของแต่ละ Node ใน Array) | ||
|
||
เนื่องจาก Binary Heap จัดเป็น Complete Binary Tree จะทำให้จำนวนชั้นของ Heap เป็น $\mathcal{O}(\log N)$ เมื่อ $N$ คือจำนวนสมาชิกใน Heap | ||
|
||
#### Push | ||
|
||
สำหรับการ Push ค่า $i$ จะใส่ค่า $i$ เข้าไปที่ตำแหน่งซ้ายสุดของชั้นสุดท้ายที่ยังว่าง หากชั้นสุดท้ายเต็มแล้วจะใส่ในช่องซ้ายสุดของชั้นใหม่ จากนั้นจะสลับค่าที่เพิ่งใส่ไปกับ Node พ่อของมันจนกว่ามันมีค่าไม่เกิน Node พ่อ | ||
|
||
เนื่องจากมีเพียง $\mathcal{O}(\log N)$ ชั้น จะมีการสลับอย่างมาก $\mathcal{O}(\log N)$ ครั้ง ซึ่งหมายความว่า Push มี Time Complexity $\mathcal{O}(\log N)$ | ||
|
||
ตัวอย่างการใส่ 84 เข้าไปใน Heap ตัวอย่างด้านบน | ||
|
||
 | ||
|
||
84 มีค่ามากกว่า 15 จึงต้องสลับ | ||
|
||
 | ||
|
||
84 มีค่ามากกว่า 78 จึงสลับอีกรอล | ||
|
||
 | ||
|
||
84 มีค่าไม่เกิน 90 จึงจบขั้นตอนการ Push | ||
|
||
#### Pop | ||
|
||
สำหรับการ Pop ค่าที่ต้องการ return คือค่าสูงสุดกล่าวคือค่าที่อยู่ที่ราก | ||
|
||
สำหรับการเอาค่านั้นออกจาก Heap จะสลับ Node ในตำแหน่งขวาสุดของชั้นสุดท้ายมาแทนรากเก่า และสลับ Node ที่ถูกสลับขึ้นมากับลูกที่มีค่ามากสุดจน Node นั้นมีค่าไม่ต่ำกว่าลูกทั้งสอง | ||
|
||
Pop มี Time Complexity $\mathcal{O}(\log N)$ เช่นเดียวกับ Push เนื่องจาก Heap มีเพียง $\mathcal{O}(\log N)$ ชั้น | ||
|
||
|
||
ตัวอย่างการ Pop จาก Heap ตัวอย่างด้านบน | ||
|
||
 | ||
|
||
ค่าที่รากคือ 90 ซึ่งเป็นค่าที่ต้องการ return | ||
|
||
 | ||
|
||
สลับ Node ขวาสุดของชั้นสุดท้ายขึ้นมาเป็นรากใหม่ | ||
|
||
 | ||
|
||
15 มีค่าน้อยกว่า 90 จึงสลับลงไป | ||
|
||
 | ||
|
||
15 มีค่าน้อยกว่า 85 จึงสลับลงไปอีก | ||
|
||
 | ||
|
||
15 มีค่าน้อยกว่า 30 จึงสลับลงไปอีก | ||
|
||
 | ||
|
||
จบขั้นตอนการ Pop เนื่องจากไม่มีลูกให้สลับลงไปอีก | ||
|
||
## Solution | ||
|
||
เมื่อมี Priority Queue แล้วสำหรับแต่ละคำสั่งใน $N$ คำสั่งที่ได้ เพียวต้อง Push สำหรับ P และ Pop สำหรับ Q | ||
|
||
แต่ละคำสั่งใช้เวลา $\mathcal{O}(\log{}N)$ ไม่ว่าจะเป็น P หรือ Q ดังนั้น Time Complexity ของข้อนี้คือ $\mathcal{O}(N\log{}N)$ | ||
|
||
ภาพประกอบทำใน https://visualgo.net/en |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
โจทย์ข้อนี้ต้องการให้เราตอบคำถามว่าในช่วง $X$ ถึง $ Y$ มีกี่ตัวที่มีตัวหาร $D$ ตัว โดยมี $Q$ คำถาม | ||
|
||
$(1\leq X \leq Y \leq 1000000, D \leq 500, Q \leq 100)$ | ||
|
||
### Preprocessing | ||
|
||
สำหรับข้อนี้ขั้นแรกคือ preprocess ว่าแต่ละตัวเลขตั้งแต่ $1$ ถึง $L=1000000$ มีตัวหารกี่ตัว เราสามารถใช้วิธีคล้ายตะแกรงของเอราทอสเทนีส (Sieve of Eratosthenes) | ||
|
||
สำหรับทุกจำนวนเต็ม $x$ เราจะ initialize จำนวนตัวหาร `num_divisors[x]` เป็น 0 | ||
|
||
จากนั้นสำหรับ $1 \leq i \leq L$ เราจะเพิ่ม `num_divisors[j]` สำหรับทุก $j = i, 2i, 3i, \dots (j \leq L)$ เพราะ $i$ เป็นตัวหารของ $j$ | ||
|
||
เห็นได้ว่าแต่ละ $i$ จะต้องเพิ่ม $j$ จำนวน $\frac{L}{i}$ ตัว | ||
|
||
```cpp | ||
int L = 1000000; | ||
int num_divisors[1000100]; | ||
|
||
for (int i = 1; i <= L; i++) | ||
for (int j = i; j <= L; j += i) | ||
num_divisors[j]++; | ||
``` | ||
|
||
ขั้นตอนนี้จึงใช้เวลา $\frac{L}{1} + \frac{L}{2} + \frac{L}{3} + \dots + \frac{L}{L} = \mathcal{O}(L \log{} L)$ | ||
|
||
จากนั้นเราจะเก็บทุกตัวใน `C[D]` โดย `C[D]` เป็น vector เก็บตัวเลขที่มีตัวหาร $D$ ตัว | ||
|
||
```cpp | ||
vector < int > C[510]; | ||
for (int i = 1; i <= L; i++) | ||
if (num_divisors[i] <= 500) | ||
C[num_divisors[i]].push_back(i); | ||
``` | ||
|
||
สังเกตว่าการ insert $i$ เข้าไปใน vector ในแบบนี้จะทำให้แต่ละ vector เรียงกันจากน้อยไปมาก | ||
|
||
ขั้นตอนนี้ใช้เวลา $\mathcal{O}(L)$ | ||
|
||
### การตอบ Query | ||
|
||
สำหรับ Query $X, Y, D$ เราต้อง Binary Search หาตำแหน่งของค่าน้อยสุดที่ไม่น้อยกว่า $X$ ใน `C[D]` และตำแหน่งของค่าที่น้อยที่สุดที่มากกว่า $Y$ จากนั้นเมื่อนำมาลบกันจะเป็นคำตอบของคำถาม เราสามารถใช้ Binary Search เพราะข้อมูลใน `C[D]` เรียงจากน้อยไปมากอยู่แล้ว | ||
|
||
การ Binary Search ในแต่ละคำถามใช้เวลา $\mathcal{O}(\log{} L)$ เพราะแต่ละ vector มีขนาดอย่างมาก $L$ | ||
|
||
หากใช้ภาษา C++ สามารถใช้ `std::lower_bound` และ `std::upper_bound` จาก `<algorithm>` ใน STL สำหรับการ Binary Search ดังกล่าว | ||
|
||
```cpp | ||
for (int i = 1; i <= Q; i++) { | ||
int X, Y, D; | ||
cin >> X >> Y >> D; | ||
|
||
auto lb = lower_bound(C[D].begin(), C[D].end(), X); | ||
auto ub = upper_bound(C[D].begin(), C[D].end(), Y); | ||
|
||
cout << int(ub - lb) << "\n"; | ||
} | ||
``` | ||
(`std::lower_bound(C[D].begin(), C[D].end(), X)` จะ return iterator ไปยังตำแหน่งแรกที่ไม่น้อยกว่า $X$ ส่วน `std::upper_bound(C[D].begin(), C[D].end(), Y)` จะ return iterator ไปยังตำแหน่งแรกที่มากกว่า $Y$) 7:51 AM 9/10/2023 | ||
ทั้งปัญหานี้จึงใช้เวลา $\mathcal{O}(L \log{} L + L + Q \log{} L) = \mathcal{O}((Q+L) \log{} L) $ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
หากแปลโจทย์ข้อนี้เป็นภาษา Graph Theory จะได้คำถามว่าหากมี Node $N$ ตัว (แทนนักเรียน) และ Edge $M$ อัน(แทนความเป็นเพื่อน) จะมีเซ็ตของ Node 4 ตัวกี่เซ็ตที่ใน Subgraph ของ Node 4 ตัวนั้นมี Edge อย่างน้อย 5 อัน | ||
|
||
วิธีคิดสำหรับข้อนี้คือพิจารณา Subgraph ขนาด 4 ที่มี 5 หรือ 6 Edge (4 Node จะมีได้อย่างมาก 6 Edge ใน Simple Graph) | ||
|
||
## Adjacency Matrix | ||
|
||
ในข้อนี้เราจะต้องการเช็คว่า Node $a$ และ $b$ มี Edge ที่เชื่อมต่อกันไหมในเวลา $\mathcal{O}(1)$ | ||
|
||
โครงสร้างข้อมูลที่เหมาะสมคือ Adjacency Matrix ซึ่งเป็นเป็น Array สองมิติ $A$ ขนาด $N \times N$ โดย $A[a][b]$ จะเป็น $1$ หากมี Edge ดังกล่าวและ $0$ หากไม่มี | ||
|
||
การสร้าง Matrix นี้ใช้เวลาและพื้นที่ความจำ $\mathcal{O}(N^2)$ (เพราะต้องสร้าง Memory ขนาด $N \times N$) | ||
|
||
การอ่านแต่ละ Edge และตั้งค่า $A[X_i][Y_i]$ ให้เป็น $1$ ใช้เวลา $\mathcal{O}(M)$ | ||
|
||
## Solution | ||
|
||
### เคส 6 Edge | ||
|
||
 | ||
|
||
สังเกตว่าเราสำหรับ Subgraph ที่เข้าเคส 6 Edge (ซึ่งเป็น Complete Graph) จะต้องมีคู่ Edge ที่ไม่มี Node ร่วมกัน 3 คู่ | ||
|
||
ดังนั้นเมื่อพิจารณาคู่ Edge $((X_i,Y_i), (X_j,Y_j))$ คู่หนึ่งจาก $M$ Edge ที่ได้มาจากข้อมูลนำเข้า โดยทั้งสอง Edge ไม่ได้มี Node ร่วมกัน ใน 4 Node $X_i, Y_i, X_j, Y_j$ หากใน 4 Node นี้มี Edge เชื่อมกันครบ 6 อัน จะสามารถนับว่าเจอ Subgraph เคส 6 Edge ได้ (โดยนับจำนวน Edge ได้โดยตรงด้วยการบวก $A[X_i][Y_i] + A[X_i][X_j] + A[X_i][Y_j] + A[Y_i][X_j] + A[Y_i][Y_j] + A[X_j][Y_j]$ จาก Adjacency Matrix ที่เก็บไว้) | ||
|
||
แต่เมื่อนับเช่นนี้จะเกิดการนับซ้ำ 3 รอบ เนื่องจากในแต่ละ Graph ที่เข้าเคสนี้จะมีคู่ Edge ที่ไม่มี Node ร่วมกัน 3 คู่ จึงต้องนำจำนวนที่นับได้มาหาร 3 เพื่อให้ได้จำนวนเคส 6 Edge ที่ต้องการ | ||
|
||
### เคส 5 Edge | ||
|
||
 | ||
|
||
สำหรับ Subgraph ที่เข้าเคส 5 Edge สังเกตว่าสามารถมองว่าเกิดจากการลบ 1 Edge จากเคส 6 Edge ด้านบน จะเหลือคู่ Edge ที่ไม่มี Node ร่วมกัน 2 คู่ | ||
|
||
เมื่อพิจารณาคู่ Edge $((X_i,Y_i), (X_j,Y_j))$ คู่หนึ่งที่ไม่มี Node ร่วมกันดังเช่นในเคสก่อนหน้า หากใน 4 Node นี้มี Edge เชื่อมกัน 5 อันพอดี จะสามารถนับว่าเจอ Subgraph เคส 5 Edge ได้ แต่ต้องหาร 2 เพราะจะถูกนับซ้ำสำหรับทั้ง 2 คู่ | ||
|
||
คำตอบที่ต้องการคือนำจำนวนที่นับได้จากทั้งสองเคสมาบวกกัน | ||
|
||
|
||
เนื่องจากมี $M$ Edge จะทำให้มี $\mathcal{O}(M^2)$ คู่ของ Edge ที่ต้องพิจารณา การนับ Edge ใน Subgraph 4 Node ใช้เวลา $\mathcal{O}(1)$ ดังนั้นทั้งปัญหาจึงใช้เวลา $\mathcal{O}(N^2+M^2)$ | ||
|
Oops, something went wrong.