Skip to content

Commit

Permalink
Merge branch 'master' into add_o59_mar_c2_binary
Browse files Browse the repository at this point in the history
  • Loading branch information
iammarkps authored Oct 27, 2023
2 parents 7f4d938 + e960bd6 commit a40c0f1
Show file tree
Hide file tree
Showing 19 changed files with 484 additions and 0 deletions.
96 changes: 96 additions & 0 deletions md/1021.md
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

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

ในการ 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 ตัวอย่างด้านบน

![](../media/1021/push_1.png)

84 มีค่ามากกว่า 15 จึงต้องสลับ

![](../media/1021/push_2.png)

84 มีค่ามากกว่า 78 จึงสลับอีกรอล

![](../media/1021/push_3.png)

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 ตัวอย่างด้านบน

![](../media/1021/push_3.png)

ค่าที่รากคือ 90 ซึ่งเป็นค่าที่ต้องการ return

![](../media/1021/pop_1.png)

สลับ Node ขวาสุดของชั้นสุดท้ายขึ้นมาเป็นรากใหม่

![](../media/1021/pop_2.png)

15 มีค่าน้อยกว่า 90 จึงสลับลงไป

![](../media/1021/pop_3.png)

15 มีค่าน้อยกว่า 85 จึงสลับลงไปอีก

![](../media/1021/pop_4.png)

15 มีค่าน้อยกว่า 30 จึงสลับลงไปอีก

![](../media/1021/pop_5.png)

จบขั้นตอนการ 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
61 changes: 61 additions & 0 deletions md/1120.md
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) $
39 changes: 39 additions & 0 deletions md/1122.md
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

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

สังเกตว่าเราสำหรับ 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

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

สำหรับ 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)$

Loading

0 comments on commit a40c0f1

Please sign in to comment.