Skip to content

Commit 6d9bf82

Browse files
authored
Merge branch 'master' into add_1079
2 parents 2c734d6 + 82517dd commit 6d9bf82

32 files changed

Lines changed: 703 additions & 0 deletions

md/1021.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
โจทย์ข้อนี้มีคำสั่ง $N$ คำสั่ง โดยคำสั่งจะมีสองแบบ
2+
3+
1. ใส่สินค้ามูลค่า $i$ เข้าไปในเครื่อง
4+
2. ถามว่าสินค้าที่มีมูลค่ามากสุดในเครื่องมีมูลค่าเท่าใดหากมีและเอาออกจากเครื่อง
5+
6+
โจทย์ข้อนี้แก้ได้ด้วย Priority Queue ซึ่งเป็นโครงสร้างข้อมูลที่มีประสิทธิภาพในการหาค่าสูงสุดของชุดข้อมูลที่อาจมีการเพิ่มเข้าหรือลบออก
7+
8+
ทั้งนี้ Priority Queue เป็นโครงสร้างข้อมูลที่มีอยู่ใน STL ซึ่งทำให้ข้อนี้แก้ได้ง่ายมากหากใช้ `std::priority_queue` โดยเพียงต้องใช้ฟังก์ชัน `push` เพื่อคำสั่งประเภทแรก และ `top` กับ `pop` สำหรับคำสั่งประเภทสอง
9+
10+
เนื่องจากโจทย์ข้อนี้เป็นโจทย์ที่ต้องการสอนให้รู้จักการใช้ Priority Queue เฉลยนี้จะอธิบายโครงสร้าง Binary Heap ซึ่งเป็นวิธีที่พื้นฐานที่สุดสำหรับการเขียน Priority Queue (Priority Queue มีความหมายที่กว้างกว่า Binary Heap นอกจาก Binary Heap ยังมีโครงสร้างข้อมูลอื่นที่สามารถใช้เขียน Priority Queue เช่น Fibonacci Heap หรือ Binomial Heap)
11+
12+
## Binary Heap
13+
14+
Binary Heap ที่ต้องการในข้อนี้จะต้องรองรับ Operation สองแบบคือ:
15+
1. Push($i$) ใส่ข้อมูลที่มีค่า $i$ เข้าไปใน Heap
16+
2. Pop() return ค่าสูงสุดใน Heap และเอาข้อมูลนั้นออกจาก Heap
17+
โดย Operation ทั้งสองมี Time Complexity $\mathcal{O}(\log{}N)$ และการเก็บ Heap มี Memory Complexity $\mathcal{O}(N)$ (เมื่อ $N$ คือขนาดของ Heap)
18+
19+
### หลักการทำงานของ Heap
20+
21+
Heap เป็นโครงสร้างข้อมูลที่เก็บในลักษณะ Complete Binary Tree ซึ่งเป็น Binary Tree ประเภทหนึ่ง
22+
23+
Binary Tree เป็น Tree ที่แต่ละ Node มีลูกอย่างมาก 2 ตัว
24+
25+
Complete Binary Tree คือ Binary Tree ที่แต่ละชั้นจะมีจำนวน Node มากสุดที่เป็นไปได้ยกเว้นชั้นสุดท้ายซึ่งจะเก็บ Node ไว้ด้านซ้ายสุดก่อน นั่นคือในชั้นที่ $h$ ของ Binary Tree ที่มีชั้น $H$ เป็นชั้นสุดท้าย จะมี Node $2^h$ ตัวหาก $h < H$ และในชั้น $H$ มีได้ตั้งแต่ $1$ ถึง $2^H$ ตัว
26+
27+
นอกจากนี้ Heap จะรักษาคุณสมบัติว่าสำหรับ Node $x$ ใดๆ ลูกของ Node $x$ จะมีค่าไม่เกินค่าของ Node $x$ ซึ่งจะทำให้ Node รากที่อยู่สูงสุดใน Heap เป็นค่าสูงสุดในทั้ง Heap
28+
29+
![](../media/1021/1.png)
30+
31+
ในการ Implement โครงสร้างข้อมูล Binary Heap โดยทั่วไปจะเก็บเป็น Array จากช่องที่ $1$ ถึง $N$ โดยรากอยู่ที่ $1$ และให้ลูกขวาของ Node ที่ช่อง $x$ ที่ช่อง $2x$ และลูกซ้ายอยู่ที่ $2x+1$ (สังเกตตัวเลขแดงในภาพประกอบซึ่งแทนตำแหน่งของแต่ละ Node ใน Array)
32+
33+
เนื่องจาก Binary Heap จัดเป็น Complete Binary Tree จะทำให้จำนวนชั้นของ Heap เป็น $\mathcal{O}(\log N)$ เมื่อ $N$ คือจำนวนสมาชิกใน Heap
34+
35+
#### Push
36+
37+
สำหรับการ Push ค่า $i$ จะใส่ค่า $i$ เข้าไปที่ตำแหน่งซ้ายสุดของชั้นสุดท้ายที่ยังว่าง หากชั้นสุดท้ายเต็มแล้วจะใส่ในช่องซ้ายสุดของชั้นใหม่ จากนั้นจะสลับค่าที่เพิ่งใส่ไปกับ Node พ่อของมันจนกว่ามันมีค่าไม่เกิน Node พ่อ
38+
39+
เนื่องจากมีเพียง $\mathcal{O}(\log N)$ ชั้น จะมีการสลับอย่างมาก $\mathcal{O}(\log N)$ ครั้ง ซึ่งหมายความว่า Push มี Time Complexity $\mathcal{O}(\log N)$
40+
41+
ตัวอย่างการใส่ 84 เข้าไปใน Heap ตัวอย่างด้านบน
42+
43+
![](../media/1021/push_1.png)
44+
45+
84 มีค่ามากกว่า 15 จึงต้องสลับ
46+
47+
![](../media/1021/push_2.png)
48+
49+
84 มีค่ามากกว่า 78 จึงสลับอีกรอล
50+
51+
![](../media/1021/push_3.png)
52+
53+
84 มีค่าไม่เกิน 90 จึงจบขั้นตอนการ Push
54+
55+
#### Pop
56+
57+
สำหรับการ Pop ค่าที่ต้องการ return คือค่าสูงสุดกล่าวคือค่าที่อยู่ที่ราก
58+
59+
สำหรับการเอาค่านั้นออกจาก Heap จะสลับ Node ในตำแหน่งขวาสุดของชั้นสุดท้ายมาแทนรากเก่า และสลับ Node ที่ถูกสลับขึ้นมากับลูกที่มีค่ามากสุดจน Node นั้นมีค่าไม่ต่ำกว่าลูกทั้งสอง
60+
61+
Pop มี Time Complexity $\mathcal{O}(\log N)$ เช่นเดียวกับ Push เนื่องจาก Heap มีเพียง $\mathcal{O}(\log N)$ ชั้น
62+
63+
64+
ตัวอย่างการ Pop จาก Heap ตัวอย่างด้านบน
65+
66+
![](../media/1021/push_3.png)
67+
68+
ค่าที่รากคือ 90 ซึ่งเป็นค่าที่ต้องการ return
69+
70+
![](../media/1021/pop_1.png)
71+
72+
สลับ Node ขวาสุดของชั้นสุดท้ายขึ้นมาเป็นรากใหม่
73+
74+
![](../media/1021/pop_2.png)
75+
76+
15 มีค่าน้อยกว่า 90 จึงสลับลงไป
77+
78+
![](../media/1021/pop_3.png)
79+
80+
15 มีค่าน้อยกว่า 85 จึงสลับลงไปอีก
81+
82+
![](../media/1021/pop_4.png)
83+
84+
15 มีค่าน้อยกว่า 30 จึงสลับลงไปอีก
85+
86+
![](../media/1021/pop_5.png)
87+
88+
จบขั้นตอนการ Pop เนื่องจากไม่มีลูกให้สลับลงไปอีก
89+
90+
## Solution
91+
92+
เมื่อมี Priority Queue แล้วสำหรับแต่ละคำสั่งใน $N$ คำสั่งที่ได้ เพียวต้อง Push สำหรับ P และ Pop สำหรับ Q
93+
94+
แต่ละคำสั่งใช้เวลา $\mathcal{O}(\log{}N)$ ไม่ว่าจะเป็น P หรือ Q ดังนั้น Time Complexity ของข้อนี้คือ $\mathcal{O}(N\log{}N)$
95+
96+
ภาพประกอบทำใน https://visualgo.net/en

md/1061.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
ข้อนี้ให้ค่าอุณหภูมิในตาราง $N \times N$ $(N \leq 20)$ และกำหนดว่าจากช่องใดๆ จะสามารถขยับไปช่องรอบๆ ที่มีอุณหภูมิที่สูงกว่าเท่านั้น โดยมีบางช่องที่ค่าอุณหภูมิเป็น 100 ซึ่งจะเข้าไม่ได้เลย
2+
3+
### แนวคิด
4+
5+
สำหรับข้อนี้เราสามารถมองว่าแต่ละช่องของเป็นตารางที่ไม่ได้มีค่าอุณหภูมิเป็น 100 เป็น Node และระหว่างสองช่องที่ติดกันจะมี Edge ที่มีทิศทางจากช่องที่มีอุณหภูมิน้อยกว่าไปยังช่องที่มีอุณหภูมิมากกว่า
6+
7+
จากนั้นหากทำ State Space Search เพื่อหาว่า Node ใดในตารางบ้างที่มี Path ที่เป็นไปได้จากช่องเริ่มต้นและเอาอันที่มีอุณหภูมิสูงสุดจะได้คำตอบที่ต้องการ
8+
9+
เช่นในตัวอย่างประกอบในโจทย์สามารถวาด Edge ได้ดังในรูป
10+
11+
![](../media/1061/0.png)
12+
13+
### State Space Search
14+
15+
State Space Search (การค้นหาในปริภูมิสถานะ) เป็นรูปแบบขั้นตอนวิธีที่จะแทน State (สถานะ) เป็น Node ใน Graph โดย Node $A$ จะมี Edge ไปยัง Node $B$ ก็ต่อเมื่อสามารถเปลี่ยนจาก State $A$ ไป $B$ ได้ในหนึ่งขั้น ใน State Space Search เราจะเริ่มจาก Node ใดๆ แล้วพิจารณา Node รอบข้างที่มี Edge เชื่อมเพื่อพิจารณาแต่ละ State ที่สามารถไปถึงจาก State เริ่ม สำหรับข้อนี้จะต้องหา State ที่มีค่าอุณหภูมิสูงสุด
16+
17+
รูปประกอบต่อไปนี้เป็นตัวอย่างของ State Space
18+
19+
![](../media/1061/1.png)
20+
21+
ในตัวอย่างนี้จะเห็นได้ว่า State $B$ สามารถไป $D$ และ $D$ กับ $E$ สามารถไปมาซึ่งกันและกัน
22+
23+
ในการทำ State Space Search จะมีวิธีพื้นฐานหลักๆ สองแบบคือ Depth First Search (DFS) กับ Breadth First Search (BFS)
24+
25+
#### DFS
26+
27+
ใน DFS จะทำการค้นหาในรูปแบบ Recursive โดยสำหรับแต่ละ State ที่พิจารณา จะ DFS ลงไปใน Node ที่มี Edge ที่ยังได้ไม่ถูกพิจารณา
28+
29+
หากทำ DFS จาก State $A$ ในตัวอย่างก่อนหน้านี้จะได้ดังในรูป (ลูกศรสีเขียวแทนการเรียก DFS แบบ Recursive และลูกศรสีแดงแทนการ Return กลับมา)
30+
31+
![](../media/1061/2.png)
32+
33+
ในตัวอย่างนี้จะพิจารณา State ในลำดับ $A,B,D,E,C,F,G$
34+
35+
เมื่อนำมาใช้กับข้อนี้จะได้โค้ดดังนี้
36+
37+
```cpp
38+
int M;
39+
int T[22][22];
40+
int visited[22][22];
41+
42+
int dfs(int x, int y) {
43+
visited[x][y] = 1; // visited[x][y] จะแทนว่าช่อง x,y ถูกพิจารณาแล้ว
44+
45+
int ret = T[x][y];
46+
47+
int dx[4] = {0, 0, -1, 1};
48+
int dy[4] = {1, -1, 0, 0};
49+
for (int u = 0; u < 4; u++) { // พิจารณา 4 ทิศทาง
50+
int ux = x + dx[u];
51+
int uy = y + dy[u];
52+
53+
if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !visited[ux][uy] &&
54+
T[ux][uy] != 100 &&
55+
T[ux][uy] > T[x][y]) // ช่องใหม่ต้องอยู่ในกรอบ ยังไม่ถูกพิจารณา และ
56+
// ต้องมีอุณหภูมิที่มากกว่าช่องปัจจุบันและไม่ใช่ 100
57+
ret = max(dfs(ux, uy), ret); // ค่าที่มากที่สุดจะเป็นค่าที่มากสุดของช่องนี้หรือช่องที่สามารถไปจากช่องนี้
58+
}
59+
60+
return ret;
61+
}
62+
```
63+
64+
ในโค้ดตัวอย่างนี้จะพิจารณาแต่ละช่องที่อยู่ด้าน บน ล่าง ซ้าย หรือ ขวา ว่าเข้าเงื่อนไขที่จะสามารถไปจากช่องปัจจุบันไหมและหากสามารถไปจะเรียก DFS แบบ Recursive
65+
66+
DFS จะใช้ Memory ใน Stack ตามความยาวของ Path ที่ทำ Recursion ซึ่งสำหรับข้อนี้เป็นได้อย่างมาก $\mathcal{O}(M^2)$ และจะใช้ Time Complexity ตามจำนวน Node และ Edge ที่ถูกพิจารณาซึ่งสำหรับข้อนี้เป็น $\mathcal{O}(M^2)$ เช่นกัน
67+
68+
#### BFS
69+
70+
สำหรับ BFS จะต่างจาก DFS ตรงที่เมื่อเจอ Edge ที่ไปได้แล้วจะไม่นำมาพิจารณาทันทีแต่ละนำเข้า Queue ก่อน โดยการพิจารณาแต่ละ Node จะเรียงตามลำดับใน Queue
71+
72+
ตัวอย่างดังภาพ
73+
74+
![](../media/1061/3.png)
75+
76+
ในตัวอย่างนี้จะพิจารณาเป็นลำดับ $A,B,C,D,E,F,G$ ตามลูกศรสีเขียว
77+
78+
โค้ด BFS ตัวอย่างสำหรับข้อนี้
79+
80+
```cpp
81+
int M;
82+
int T[22][22];
83+
84+
int bfs(int x, int y) {
85+
int queued[22][22];
86+
queued[x][y] = 1;
87+
std::deque<std::pair<int, int>> q; // Queue สำหรับการเก็บแต่ละ Node
88+
89+
q.push_back(std::make_pair(x, y)); // นำ Node เริ่มต้นเข้า Queue
90+
91+
int ret = T[x][y];
92+
93+
while (!q.empty()) {
94+
int qx = q[0].first, qy = q[0].second;
95+
q.pop_front(); // นำ Node แรกมาพิจารณาและเอาออกจาก Queue
96+
97+
ret = std::max(ret, T[qx][qy]);
98+
99+
int dx[4] = {0, 0, -1, 1};
100+
int dy[4] = {1, -1, 0, 0};
101+
for (int u = 0; u < 4; u++) {
102+
int ux = qx + dx[u];
103+
int uy = qy + dy[u];
104+
105+
if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !queued[ux][uy] &&
106+
T[ux][uy] != 100 && T[ux][uy] > T[qx][qy]) {
107+
q.push_back(std::make_pair(ux, uy)); // ใส่ Node ใหม่ที่เจอเข้า Queue
108+
queued[ux][uy] = 1;
109+
}
110+
}
111+
}
112+
113+
return ret;
114+
}
115+
```
116+
117+
สังเกตว่าในโค้ดนี้จะเรียก BFS เพียงรอบเดียวต่างจาก DFS โดยจะนำ Node มาใส่ Queue แทนการใช้ Recursion
118+
119+
BFS จะใช้ Memory ตามจำนวน Node ที่อยู่ใน Queue และเวลาตามจำนวน Node / Edge ที่ถูกพิจารณา
120+
121+
ดังนั้นทั้งคู่จะเป็น $\mathcal{O}(M^2)$ สำหรับข้อนี้

0 commit comments

Comments
 (0)