From af64c012e7803e28d54fb96e4fc5dd74bfa093aa Mon Sep 17 00:00:00 2001 From: vzsky Date: Sun, 31 Aug 2025 11:08:41 +0700 Subject: [PATCH] Link Prog.in.th to Toi Problems --- md/0011.md | 0 md/1000.md | 1 + md/1001.md | 24 +----- md/1002.md | 137 +------------------------------- md/1003.md | 16 +--- md/1004.md | 4 +- md/1005.md | 21 +---- md/1006.md | 1 + md/1007.md | 1 + md/1008.md | 1 + md/1009.md | 1 + md/1010.md | 1 + md/1011.md | 1 + md/1012.md | 1 + md/1013.md | 1 + md/1014.md | 1 + md/1015.md | 1 + md/1016.md | 1 + md/1059.md | 1 + md/1060.md | 1 + md/1061.md | 122 +--------------------------- md/1062.md | 1 + md/1063.md | 1 + md/1064.md | 1 + md/toi1_brick.md | 23 ++++++ md/toi1_nugget.md | 15 ++++ md/toi1_plate.md | 3 + md/toi1_roman.md | 136 +++++++++++++++++++++++++++++++ md/toi2_maxseq.md | 20 +++++ md/toi4_temp.md | 121 ++++++++++++++++++++++++++++ media/{1061 => toi4_temp}/0.png | Bin media/{1061 => toi4_temp}/1.png | Bin media/{1061 => toi4_temp}/2.png | Bin media/{1061 => toi4_temp}/3.png | Bin 34 files changed, 341 insertions(+), 318 deletions(-) mode change 100755 => 100644 md/0011.md create mode 100644 md/1000.md create mode 100644 md/1006.md create mode 100644 md/1007.md create mode 100644 md/1008.md create mode 100644 md/1009.md create mode 100644 md/1010.md create mode 100644 md/1011.md create mode 100644 md/1012.md create mode 100644 md/1013.md create mode 100644 md/1014.md create mode 100644 md/1015.md create mode 100644 md/1016.md create mode 100644 md/1059.md create mode 100644 md/1060.md create mode 100644 md/1062.md create mode 100644 md/1063.md create mode 100644 md/1064.md create mode 100644 md/toi1_brick.md create mode 100644 md/toi1_nugget.md create mode 100644 md/toi1_plate.md create mode 100644 md/toi1_roman.md create mode 100644 md/toi2_maxseq.md create mode 100644 md/toi4_temp.md rename media/{1061 => toi4_temp}/0.png (100%) rename media/{1061 => toi4_temp}/1.png (100%) rename media/{1061 => toi4_temp}/2.png (100%) rename media/{1061 => toi4_temp}/3.png (100%) diff --git a/md/0011.md b/md/0011.md old mode 100755 new mode 100644 diff --git a/md/1000.md b/md/1000.md new file mode 100644 index 0000000..39ac8f6 --- /dev/null +++ b/md/1000.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi1_chain/solution) diff --git a/md/1001.md b/md/1001.md index ef71cee..1ac6d60 100644 --- a/md/1001.md +++ b/md/1001.md @@ -1,23 +1 @@ -กำหนดให้ $input[i][j]$ เป็น array สองมิติที่บ่งบอกถึงสถานะเริ่มต้นของเกม ซึ่งมาจากการแบ่งข้อมูลนำเข้าเป็นแต่ละ char แล้วนำใส่ char[][] - -ในทางคล้ายๆกัน เราจะนิยาม $result[i][j]$ เป็น array สองมิติที่บ่งบอกทถึงสถานะตอนท้ายของเกมเมื่อโปรแกรมทำงานเสร็จ ฉนั้น $result$ จะเท่ากับ $input$ เมื่อโปรแกรมเริ่มทำงานแล้วเราจะเปลี่ยนแปลงค่าในนี้ให้กลายเป็นสถานะตอนท้ายของเกมในที่สุด - -กำหนดให้ $free[i][j]$ เป็น array สองมิติที่บ่งบอกว่าในช่องที่ $(i, j)$ ของตารางเกมมีสิ่งกีดขวางหรือไม่ (รวมไปถึงก้อนอิฐที่อยู่ในตำแหน่งนั้นอยู่แล้ว) - -Pseudocode ของอัลกอริทึมในการวางก้อนอิฐจะเป็นดังนี้: - -``` -for i in [1, n]: - for j in [1, m]: - result[i][j] = input[i][j] - free[i][j] = true - -for j in [1, m]: - let i = 1 - while i <= n and free[i][j]: - i += 1 - free[i][j] = false - result[i][j] = '#' - -print(result) -``` +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi1_brick/solution) diff --git a/md/1002.md b/md/1002.md index 6cc3c2d..cdf78f8 100644 --- a/md/1002.md +++ b/md/1002.md @@ -1,136 +1 @@ -### Original Solution - -ข้อนี้อาจดูเหมือนจะมีเฉลยที่วุ่นวายมากๆ แต่ถ้าหากเราสามารถแบ่งกรณีได้อย่างเป็นระบบระเบียบก็จะไม่วุ่นวายมาก - -สำหรับทุกจำนวนเต็มน้อยกว่าหรือเท่ากับ $d$ เราต้องการหาจำนวน i, v, x, l, c ที่ต้องใช้ สมมุติว่าเรากำลังพิจารณาตัวเลข $n$ อยู่ - -Pseudocode จะเป็นดังต่อไปนี้ -``` -while n >= 0: - if n >= 100: - c += 1 - n -= 100 - elif n >= 90: - c += 1 - x += 1 - n -= 90 - elif n >= 50: - l += 1 - n -= 50 - elif n >= 40: - l += 1 - x += 1 - n -= 40 - elif n >= 10: - x += 1 - n -= 10 - elif n >= 9: - x += 1 - i += 1 - n -= 9 - elif n >= 5: - v += 1 - n -= 5 - elif n >= 4: - v += 1 - i += 1 - n -= 4 - else: - i += 1 - n -= 1 -``` - -สังเกตุว่าในแต่ละรอบของ while loop $n$ จะเข้าเพียงหนึ่งกรณีเท่านั้น และอัลกอริทึมจะพิจารณากรณีที่ $n$ มากๆไปหา $n$ น้อยๆเสมอเพื่อรับประกันว่าสัญลักษณ์ที่มีค่ามากกว่าจะอยู่ทางซ้ายเสมอ (โดยมีข้อยกเว้นดังที่ระบุไว้ในตัวโจทย์) - -### Alternative Solution - -จริง ๆ ข้อนี้สามารถมองเหมือนเป็น pattern ได้เหมือนกัน ลองดูแบบนี้ครับ ลำดับของอักษรแทนเลขโรมัน คือ `I V X L C` ตามลำดับ - -พิจารณาเลข `1, 2, 3, ..., 9` - -``` -1 : I -2 : II -3 : III -4 : IV -5 : V -6 : VI -7 : VII -8 : VIII -9 : IX -``` - -พิจารณา `10, 20, 30, ..., 90` - -``` -10 : X -20 : XX -30 : XXX -40 : XL -50 : L -60 : LX -70 : LXX -80 : LXXX -90 : XC -``` - -พิจารณา `100, 200, 300` - -``` -100 : C -200 : CC -300 : CCC -``` - -จะสังเกตได้ว่าเลขแบ่งเป็นชุดตัวอักษร ชุดละ 3 ตัว เรียกจากมากไปน้อยจะเป็น `I V X` ของ 1 - 9, `X L C` ของ 10 - 90, และ 100 - 300 ทั้งหมดเป็นไปตามกฎการเพิ่มแบบเดียวกัน ทั้งนี้เพื่อให้เหมือนกับ 2 ชุดก่อนหน้า เราสามารถสมมุติให้มีตัวอักษรอีก 2 ตัวเพิ่มขึ้นมาข้างหลังได้ `C _ _` - -นอกจากนี้ ตัวเลขโรมันในแต่ละหลักจะต่อกันไปเรื่อยเลย เช่น `318` ก็คือ `300 (CCC) + 10 (X) + 8 (VIII) = CCCXVIII` ดังนั้น เราสามารถหาว่าแต่ละหลักเป็นเลขอะไร แล้วค่อย ๆ ไล่นับจำนวนตัวอักษร โดยเราจะสร้าง array มาเก็บจำนวนของแต่ละตัวอักษร เรียงจากน้อยไปมาก และบวกเพิ่มโดยใช้ array อีกตัวที่เก็บว่าเลขในหลักนั้น ๆ มีตัวอักษรตาม pattern ที่กล่าวไปข้างต้นอย่างไร - -```cpp -#include - -using namespace std; - -int factor[][3] = { - {0, 0, 0}, - {1, 0, 0}, - {2, 0, 0}, - {3, 0, 0}, - {1, 1, 0}, - {0, 1, 0}, - {1, 1, 0}, - {2, 1, 0}, /// เช่น XII ถ้าเป็นหลักหน่วย และ LXX ถ้าเป็นหลักสิบ - {3, 1, 0}, - {1, 0, 1} -}; - -int cnt[7]; /// [I, V, X, L, C, _, _] - -/// เพิ่มค่าใน cnt โดย start_pos จะขึ้นอยู่กับว่าเป็นเลขในหลักใด -/// หลักหน่วย -- 0 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [I, V, X] -/// หลักสิบ -- 2 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [X, L, C] -/// หลักร้อย -- 4 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [C, _, _] - -void add(int start_pos, int num) { - for(int i = 0; i < 3; i++) { - cnt[start_pos + i] += factor[num][i]; - } -} - -int main() { - int d; - cin >> d; - - for(int i = 1; i <= d; i++) { - add(0, i % 10); /// หลักหน่วย - add(2, (i / 10) % 10); /// หลักสิบ - add(4, i / 100); /// หลักร้อย - } - - for (int i = 0; i < 5; i++) { - cout << cnt[i] << " "; - } - - return 0; -} -``` \ No newline at end of file +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi1_roman/solution) diff --git a/md/1003.md b/md/1003.md index e77cdd6..d25ae8d 100644 --- a/md/1003.md +++ b/md/1003.md @@ -1,15 +1 @@ -นิยาม $DP[i] = 1$ เมื่อ $i$ เป็น nugget number และ $DP[i] = 0$ เมื่อ $i$ ไม่ใช่ nugget number - -Pseudocode ในการหา $DP$ จะเป็นดังต่อไปนี้ -``` -for i in [0, n]: - DP[i] = 0 -DP[6] = DP[9] = DP[12] = 1 -for i in [13, n]: - if DP[i-6] == 1 or DP[i-9] == 1 or DP[i-12] == 1: - DP[i] = 1 -``` - -จากนี้เลข $i$ ใดๆที่ $DP[i] = 1$ คือ nugget number - -ทั้งหมดนี้หาได้ในเวลา $O(n)$ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi1_nugget/solution) diff --git a/md/1004.md b/md/1004.md index 323c37b..755c10f 100644 --- a/md/1004.md +++ b/md/1004.md @@ -1,3 +1 @@ -ข้อนี้เป็นการจำลองสถานการณ์โดยใช้ queue เข้ามาช่วย เมื่อมีเด็กถูกเรียนกมาเข้าแถว เราก็จะใช้คำสั่ง push ของ queue และเมื่อต้องการนำนักเรียนที่อยู่หัวแถวออกจากแถว เราก็จะใช้คำสั่ง pop ของ queue - -ใน C++ STL สามารถใช้ function ดังกล่าวโดยตรงจาก API ของ queue +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi1_plate/solution) diff --git a/md/1005.md b/md/1005.md index 0422ca4..20457de 100644 --- a/md/1005.md +++ b/md/1005.md @@ -1,20 +1 @@ -ปัญหาที่ตั้งขึ้นมาในข้อนี้เป็นหนึ่งใน classical problem หรือว่าได้ว่าเป็นปัญหาที่เจอได้บ่อยครั้ง คือปัญหา maximum sum subsequence ปัญหา maximum sum subsequence มีหลายวิธีที่สามารถจะแก้ได้โดยใช้เวลาเพียง $O(n)$ เท่านั้น เช่นเราอาจใช้ Kadane's algorithm หรือ deque มาแก้ก็ได้ - -อย่างไรก็ตาม เทคนิคเหล่านี้ยากเกินกว่าที่จะเหมาะสมกับโจทย์ในระดับ 10 เลยขออธิบายอัลกอริทึมที่ใช้เวลา $O(n^2)$ เนื่องจากโจทย์กำหนดว่า $n \leq 2500$ ดังนั้น $n^2 = 6250000$ ซึ่งยังสามารถทำงานเสร็จได้ภายในหนึ่งวินาทีสำหรับภาษา C/C++ แน่นอน - -อัลกอริทึมที่ใช้ก็คำ for-loop สองชั้น โดยไล่ตัวแปร $i$ จาก $[0, n)$ และสำหรับแต่ละ $i$ จะไล่ตัวแปร $j$ ตั้งแต่ $[i, n)$ เราจะเก็บตัวแปร $S$ ที่เก็บผลบวกของช่วง $[i, j]$ เมื่อต้องการเลื่อนจาก $j$ ไป $j+1$ เราจะเพิ่มค่า $S$ เป็น $S = S + a[j]$ และเมื่อต้องการเปลี่ยนค่า $i$ เราจะตั้งค่า $S$ ใหม่ที่ $S = 0$ ฉนั้นในแต่ละขั้นตอนจะได้ช่วง $[i, j]$ และผลบวกของแต่ละช่วง ซึ่งสามารถนำมาหาว่าผลบวกของช่วงใหนมีค่ามากที่สุดได้ ด้านล่างคือโค้ดของอัลกอริทึมหลักนี้ - -```python -max_sum = 0 -left_bound = -1 -right_bound = -1 -for i in range(0, n): - S = 0 - for j in range(i, n): - S = S + a[j] - if S > max_sum: - max_sum = S - left_bound = i - right_bound = j -print(max_sum, left_bound, right_bound) -``` +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_maxseq/solution) diff --git a/md/1006.md b/md/1006.md new file mode 100644 index 0000000..4fe011d --- /dev/null +++ b/md/1006.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_dice/solution) diff --git a/md/1007.md b/md/1007.md new file mode 100644 index 0000000..e93b641 --- /dev/null +++ b/md/1007.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_bee/solution) diff --git a/md/1008.md b/md/1008.md new file mode 100644 index 0000000..ccfaf63 --- /dev/null +++ b/md/1008.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_skyline/solution) diff --git a/md/1009.md b/md/1009.md new file mode 100644 index 0000000..a884ebb --- /dev/null +++ b/md/1009.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_segment/solution) diff --git a/md/1010.md b/md/1010.md new file mode 100644 index 0000000..afc32c9 --- /dev/null +++ b/md/1010.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi2_word/solution) diff --git a/md/1011.md b/md/1011.md new file mode 100644 index 0000000..1620579 --- /dev/null +++ b/md/1011.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_block/solution) diff --git a/md/1012.md b/md/1012.md new file mode 100644 index 0000000..79939ae --- /dev/null +++ b/md/1012.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_cake/solution) diff --git a/md/1013.md b/md/1013.md new file mode 100644 index 0000000..98e1a20 --- /dev/null +++ b/md/1013.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_express/solution) diff --git a/md/1014.md b/md/1014.md new file mode 100644 index 0000000..1898461 --- /dev/null +++ b/md/1014.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_filter/solution) diff --git a/md/1015.md b/md/1015.md new file mode 100644 index 0000000..6394cb9 --- /dev/null +++ b/md/1015.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_tiling/solution) diff --git a/md/1016.md b/md/1016.md new file mode 100644 index 0000000..e70ef53 --- /dev/null +++ b/md/1016.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi3_treasure/solution) diff --git a/md/1059.md b/md/1059.md new file mode 100644 index 0000000..68f9cf8 --- /dev/null +++ b/md/1059.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_sms/solution) diff --git a/md/1060.md b/md/1060.md new file mode 100644 index 0000000..40ef77b --- /dev/null +++ b/md/1060.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_mountain/solution) diff --git a/md/1061.md b/md/1061.md index b7ea545..74eaef4 100644 --- a/md/1061.md +++ b/md/1061.md @@ -1,121 +1 @@ -ข้อนี้ให้ค่าอุณหภูมิในตาราง $N \times N$ $(N \leq 20)$ และกำหนดว่าจากช่องใดๆ จะสามารถขยับไปช่องรอบๆ ที่มีอุณหภูมิที่สูงกว่าเท่านั้น โดยมีบางช่องที่ค่าอุณหภูมิเป็น 100 ซึ่งจะเข้าไม่ได้เลย - -### แนวคิด - -สำหรับข้อนี้เราสามารถมองว่าแต่ละช่องของเป็นตารางที่ไม่ได้มีค่าอุณหภูมิเป็น 100 เป็น Node และระหว่างสองช่องที่ติดกันจะมี Edge ที่มีทิศทางจากช่องที่มีอุณหภูมิน้อยกว่าไปยังช่องที่มีอุณหภูมิมากกว่า - -จากนั้นหากทำ State Space Search เพื่อหาว่า Node ใดในตารางบ้างที่มี Path ที่เป็นไปได้จากช่องเริ่มต้นและเอาอันที่มีอุณหภูมิสูงสุดจะได้คำตอบที่ต้องการ - -เช่นในตัวอย่างประกอบในโจทย์สามารถวาด Edge ได้ดังในรูป - -![](../media/1061/0.png) - -### State Space Search - -State Space Search (การค้นหาในปริภูมิสถานะ) เป็นรูปแบบขั้นตอนวิธีที่จะแทน State (สถานะ) เป็น Node ใน Graph โดย Node $A$ จะมี Edge ไปยัง Node $B$ ก็ต่อเมื่อสามารถเปลี่ยนจาก State $A$ ไป $B$ ได้ในหนึ่งขั้น ใน State Space Search เราจะเริ่มจาก Node ใดๆ แล้วพิจารณา Node รอบข้างที่มี Edge เชื่อมเพื่อพิจารณาแต่ละ State ที่สามารถไปถึงจาก State เริ่ม สำหรับข้อนี้จะต้องหา State ที่มีค่าอุณหภูมิสูงสุด - -รูปประกอบต่อไปนี้เป็นตัวอย่างของ State Space - -![](../media/1061/1.png) - -ในตัวอย่างนี้จะเห็นได้ว่า State $B$ สามารถไป $D$ และ $D$ กับ $E$ สามารถไปมาซึ่งกันและกัน - -ในการทำ State Space Search จะมีวิธีพื้นฐานหลักๆ สองแบบคือ Depth First Search (DFS) กับ Breadth First Search (BFS) - -#### DFS - -ใน DFS จะทำการค้นหาในรูปแบบ Recursive โดยสำหรับแต่ละ State ที่พิจารณา จะ DFS ลงไปใน Node ที่มี Edge ที่ยังได้ไม่ถูกพิจารณา - -หากทำ DFS จาก State $A$ ในตัวอย่างก่อนหน้านี้จะได้ดังในรูป (ลูกศรสีเขียวแทนการเรียก DFS แบบ Recursive และลูกศรสีแดงแทนการ Return กลับมา) - -![](../media/1061/2.png) - -ในตัวอย่างนี้จะพิจารณา State ในลำดับ $A,B,D,E,C,F,G$ - -เมื่อนำมาใช้กับข้อนี้จะได้โค้ดดังนี้ - -```cpp -int M; -int T[22][22]; -int visited[22][22]; - -int dfs(int x, int y) { - visited[x][y] = 1; // visited[x][y] จะแทนว่าช่อง x,y ถูกพิจารณาแล้ว - - int ret = T[x][y]; - - int dx[4] = {0, 0, -1, 1}; - int dy[4] = {1, -1, 0, 0}; - for (int u = 0; u < 4; u++) { // พิจารณา 4 ทิศทาง - int ux = x + dx[u]; - int uy = y + dy[u]; - - if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !visited[ux][uy] && - T[ux][uy] != 100 && - T[ux][uy] > T[x][y]) // ช่องใหม่ต้องอยู่ในกรอบ ยังไม่ถูกพิจารณา และ - // ต้องมีอุณหภูมิที่มากกว่าช่องปัจจุบันและไม่ใช่ 100 - ret = max(dfs(ux, uy), ret); // ค่าที่มากที่สุดจะเป็นค่าที่มากสุดของช่องนี้หรือช่องที่สามารถไปจากช่องนี้ - } - - return ret; -} -``` - -ในโค้ดตัวอย่างนี้จะพิจารณาแต่ละช่องที่อยู่ด้าน บน ล่าง ซ้าย หรือ ขวา ว่าเข้าเงื่อนไขที่จะสามารถไปจากช่องปัจจุบันไหมและหากสามารถไปจะเรียก DFS แบบ Recursive - -DFS จะใช้ Memory ใน Stack ตามความยาวของ Path ที่ทำ Recursion ซึ่งสำหรับข้อนี้เป็นได้อย่างมาก $\mathcal{O}(M^2)$ และจะใช้ Time Complexity ตามจำนวน Node และ Edge ที่ถูกพิจารณาซึ่งสำหรับข้อนี้เป็น $\mathcal{O}(M^2)$ เช่นกัน - -#### BFS - -สำหรับ BFS จะต่างจาก DFS ตรงที่เมื่อเจอ Edge ที่ไปได้แล้วจะไม่นำมาพิจารณาทันทีแต่ละนำเข้า Queue ก่อน โดยการพิจารณาแต่ละ Node จะเรียงตามลำดับใน Queue - -ตัวอย่างดังภาพ - -![](../media/1061/3.png) - -ในตัวอย่างนี้จะพิจารณาเป็นลำดับ $A,B,C,D,E,F,G$ ตามลูกศรสีเขียว - -โค้ด BFS ตัวอย่างสำหรับข้อนี้ - -```cpp -int M; -int T[22][22]; - -int bfs(int x, int y) { - int queued[22][22]; - queued[x][y] = 1; - std::deque> q; // Queue สำหรับการเก็บแต่ละ Node - - q.push_back(std::make_pair(x, y)); // นำ Node เริ่มต้นเข้า Queue - - int ret = T[x][y]; - - while (!q.empty()) { - int qx = q[0].first, qy = q[0].second; - q.pop_front(); // นำ Node แรกมาพิจารณาและเอาออกจาก Queue - - ret = std::max(ret, T[qx][qy]); - - int dx[4] = {0, 0, -1, 1}; - int dy[4] = {1, -1, 0, 0}; - for (int u = 0; u < 4; u++) { - int ux = qx + dx[u]; - int uy = qy + dy[u]; - - if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !queued[ux][uy] && - T[ux][uy] != 100 && T[ux][uy] > T[qx][qy]) { - q.push_back(std::make_pair(ux, uy)); // ใส่ Node ใหม่ที่เจอเข้า Queue - queued[ux][uy] = 1; - } - } - } - - return ret; -} -``` - -สังเกตว่าในโค้ดนี้จะเรียก BFS เพียงรอบเดียวต่างจาก DFS โดยจะนำ Node มาใส่ Queue แทนการใช้ Recursion - -BFS จะใช้ Memory ตามจำนวน Node ที่อยู่ใน Queue และเวลาตามจำนวน Node / Edge ที่ถูกพิจารณา - -ดังนั้นทั้งคู่จะเป็น $\mathcal{O}(M^2)$ สำหรับข้อนี้ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_temp/solution) diff --git a/md/1062.md b/md/1062.md new file mode 100644 index 0000000..3d5e4c8 --- /dev/null +++ b/md/1062.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_key/solution) diff --git a/md/1063.md b/md/1063.md new file mode 100644 index 0000000..a13bedf --- /dev/null +++ b/md/1063.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_logistics/solution) diff --git a/md/1064.md b/md/1064.md new file mode 100644 index 0000000..cbad90b --- /dev/null +++ b/md/1064.md @@ -0,0 +1 @@ +ดูเฉลยได้ที่[นี่](https://programming.in.th/tasks/toi4_school/solution) diff --git a/md/toi1_brick.md b/md/toi1_brick.md new file mode 100644 index 0000000..ef71cee --- /dev/null +++ b/md/toi1_brick.md @@ -0,0 +1,23 @@ +กำหนดให้ $input[i][j]$ เป็น array สองมิติที่บ่งบอกถึงสถานะเริ่มต้นของเกม ซึ่งมาจากการแบ่งข้อมูลนำเข้าเป็นแต่ละ char แล้วนำใส่ char[][] + +ในทางคล้ายๆกัน เราจะนิยาม $result[i][j]$ เป็น array สองมิติที่บ่งบอกทถึงสถานะตอนท้ายของเกมเมื่อโปรแกรมทำงานเสร็จ ฉนั้น $result$ จะเท่ากับ $input$ เมื่อโปรแกรมเริ่มทำงานแล้วเราจะเปลี่ยนแปลงค่าในนี้ให้กลายเป็นสถานะตอนท้ายของเกมในที่สุด + +กำหนดให้ $free[i][j]$ เป็น array สองมิติที่บ่งบอกว่าในช่องที่ $(i, j)$ ของตารางเกมมีสิ่งกีดขวางหรือไม่ (รวมไปถึงก้อนอิฐที่อยู่ในตำแหน่งนั้นอยู่แล้ว) + +Pseudocode ของอัลกอริทึมในการวางก้อนอิฐจะเป็นดังนี้: + +``` +for i in [1, n]: + for j in [1, m]: + result[i][j] = input[i][j] + free[i][j] = true + +for j in [1, m]: + let i = 1 + while i <= n and free[i][j]: + i += 1 + free[i][j] = false + result[i][j] = '#' + +print(result) +``` diff --git a/md/toi1_nugget.md b/md/toi1_nugget.md new file mode 100644 index 0000000..e77cdd6 --- /dev/null +++ b/md/toi1_nugget.md @@ -0,0 +1,15 @@ +นิยาม $DP[i] = 1$ เมื่อ $i$ เป็น nugget number และ $DP[i] = 0$ เมื่อ $i$ ไม่ใช่ nugget number + +Pseudocode ในการหา $DP$ จะเป็นดังต่อไปนี้ +``` +for i in [0, n]: + DP[i] = 0 +DP[6] = DP[9] = DP[12] = 1 +for i in [13, n]: + if DP[i-6] == 1 or DP[i-9] == 1 or DP[i-12] == 1: + DP[i] = 1 +``` + +จากนี้เลข $i$ ใดๆที่ $DP[i] = 1$ คือ nugget number + +ทั้งหมดนี้หาได้ในเวลา $O(n)$ diff --git a/md/toi1_plate.md b/md/toi1_plate.md new file mode 100644 index 0000000..323c37b --- /dev/null +++ b/md/toi1_plate.md @@ -0,0 +1,3 @@ +ข้อนี้เป็นการจำลองสถานการณ์โดยใช้ queue เข้ามาช่วย เมื่อมีเด็กถูกเรียนกมาเข้าแถว เราก็จะใช้คำสั่ง push ของ queue และเมื่อต้องการนำนักเรียนที่อยู่หัวแถวออกจากแถว เราก็จะใช้คำสั่ง pop ของ queue + +ใน C++ STL สามารถใช้ function ดังกล่าวโดยตรงจาก API ของ queue diff --git a/md/toi1_roman.md b/md/toi1_roman.md new file mode 100644 index 0000000..6cc3c2d --- /dev/null +++ b/md/toi1_roman.md @@ -0,0 +1,136 @@ +### Original Solution + +ข้อนี้อาจดูเหมือนจะมีเฉลยที่วุ่นวายมากๆ แต่ถ้าหากเราสามารถแบ่งกรณีได้อย่างเป็นระบบระเบียบก็จะไม่วุ่นวายมาก + +สำหรับทุกจำนวนเต็มน้อยกว่าหรือเท่ากับ $d$ เราต้องการหาจำนวน i, v, x, l, c ที่ต้องใช้ สมมุติว่าเรากำลังพิจารณาตัวเลข $n$ อยู่ + +Pseudocode จะเป็นดังต่อไปนี้ +``` +while n >= 0: + if n >= 100: + c += 1 + n -= 100 + elif n >= 90: + c += 1 + x += 1 + n -= 90 + elif n >= 50: + l += 1 + n -= 50 + elif n >= 40: + l += 1 + x += 1 + n -= 40 + elif n >= 10: + x += 1 + n -= 10 + elif n >= 9: + x += 1 + i += 1 + n -= 9 + elif n >= 5: + v += 1 + n -= 5 + elif n >= 4: + v += 1 + i += 1 + n -= 4 + else: + i += 1 + n -= 1 +``` + +สังเกตุว่าในแต่ละรอบของ while loop $n$ จะเข้าเพียงหนึ่งกรณีเท่านั้น และอัลกอริทึมจะพิจารณากรณีที่ $n$ มากๆไปหา $n$ น้อยๆเสมอเพื่อรับประกันว่าสัญลักษณ์ที่มีค่ามากกว่าจะอยู่ทางซ้ายเสมอ (โดยมีข้อยกเว้นดังที่ระบุไว้ในตัวโจทย์) + +### Alternative Solution + +จริง ๆ ข้อนี้สามารถมองเหมือนเป็น pattern ได้เหมือนกัน ลองดูแบบนี้ครับ ลำดับของอักษรแทนเลขโรมัน คือ `I V X L C` ตามลำดับ + +พิจารณาเลข `1, 2, 3, ..., 9` + +``` +1 : I +2 : II +3 : III +4 : IV +5 : V +6 : VI +7 : VII +8 : VIII +9 : IX +``` + +พิจารณา `10, 20, 30, ..., 90` + +``` +10 : X +20 : XX +30 : XXX +40 : XL +50 : L +60 : LX +70 : LXX +80 : LXXX +90 : XC +``` + +พิจารณา `100, 200, 300` + +``` +100 : C +200 : CC +300 : CCC +``` + +จะสังเกตได้ว่าเลขแบ่งเป็นชุดตัวอักษร ชุดละ 3 ตัว เรียกจากมากไปน้อยจะเป็น `I V X` ของ 1 - 9, `X L C` ของ 10 - 90, และ 100 - 300 ทั้งหมดเป็นไปตามกฎการเพิ่มแบบเดียวกัน ทั้งนี้เพื่อให้เหมือนกับ 2 ชุดก่อนหน้า เราสามารถสมมุติให้มีตัวอักษรอีก 2 ตัวเพิ่มขึ้นมาข้างหลังได้ `C _ _` + +นอกจากนี้ ตัวเลขโรมันในแต่ละหลักจะต่อกันไปเรื่อยเลย เช่น `318` ก็คือ `300 (CCC) + 10 (X) + 8 (VIII) = CCCXVIII` ดังนั้น เราสามารถหาว่าแต่ละหลักเป็นเลขอะไร แล้วค่อย ๆ ไล่นับจำนวนตัวอักษร โดยเราจะสร้าง array มาเก็บจำนวนของแต่ละตัวอักษร เรียงจากน้อยไปมาก และบวกเพิ่มโดยใช้ array อีกตัวที่เก็บว่าเลขในหลักนั้น ๆ มีตัวอักษรตาม pattern ที่กล่าวไปข้างต้นอย่างไร + +```cpp +#include + +using namespace std; + +int factor[][3] = { + {0, 0, 0}, + {1, 0, 0}, + {2, 0, 0}, + {3, 0, 0}, + {1, 1, 0}, + {0, 1, 0}, + {1, 1, 0}, + {2, 1, 0}, /// เช่น XII ถ้าเป็นหลักหน่วย และ LXX ถ้าเป็นหลักสิบ + {3, 1, 0}, + {1, 0, 1} +}; + +int cnt[7]; /// [I, V, X, L, C, _, _] + +/// เพิ่มค่าใน cnt โดย start_pos จะขึ้นอยู่กับว่าเป็นเลขในหลักใด +/// หลักหน่วย -- 0 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [I, V, X] +/// หลักสิบ -- 2 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [X, L, C] +/// หลักร้อย -- 4 เพื่อให้ไปบวกเพิ่ม ณ ตำแหน่ง [C, _, _] + +void add(int start_pos, int num) { + for(int i = 0; i < 3; i++) { + cnt[start_pos + i] += factor[num][i]; + } +} + +int main() { + int d; + cin >> d; + + for(int i = 1; i <= d; i++) { + add(0, i % 10); /// หลักหน่วย + add(2, (i / 10) % 10); /// หลักสิบ + add(4, i / 100); /// หลักร้อย + } + + for (int i = 0; i < 5; i++) { + cout << cnt[i] << " "; + } + + return 0; +} +``` \ No newline at end of file diff --git a/md/toi2_maxseq.md b/md/toi2_maxseq.md new file mode 100644 index 0000000..0422ca4 --- /dev/null +++ b/md/toi2_maxseq.md @@ -0,0 +1,20 @@ +ปัญหาที่ตั้งขึ้นมาในข้อนี้เป็นหนึ่งใน classical problem หรือว่าได้ว่าเป็นปัญหาที่เจอได้บ่อยครั้ง คือปัญหา maximum sum subsequence ปัญหา maximum sum subsequence มีหลายวิธีที่สามารถจะแก้ได้โดยใช้เวลาเพียง $O(n)$ เท่านั้น เช่นเราอาจใช้ Kadane's algorithm หรือ deque มาแก้ก็ได้ + +อย่างไรก็ตาม เทคนิคเหล่านี้ยากเกินกว่าที่จะเหมาะสมกับโจทย์ในระดับ 10 เลยขออธิบายอัลกอริทึมที่ใช้เวลา $O(n^2)$ เนื่องจากโจทย์กำหนดว่า $n \leq 2500$ ดังนั้น $n^2 = 6250000$ ซึ่งยังสามารถทำงานเสร็จได้ภายในหนึ่งวินาทีสำหรับภาษา C/C++ แน่นอน + +อัลกอริทึมที่ใช้ก็คำ for-loop สองชั้น โดยไล่ตัวแปร $i$ จาก $[0, n)$ และสำหรับแต่ละ $i$ จะไล่ตัวแปร $j$ ตั้งแต่ $[i, n)$ เราจะเก็บตัวแปร $S$ ที่เก็บผลบวกของช่วง $[i, j]$ เมื่อต้องการเลื่อนจาก $j$ ไป $j+1$ เราจะเพิ่มค่า $S$ เป็น $S = S + a[j]$ และเมื่อต้องการเปลี่ยนค่า $i$ เราจะตั้งค่า $S$ ใหม่ที่ $S = 0$ ฉนั้นในแต่ละขั้นตอนจะได้ช่วง $[i, j]$ และผลบวกของแต่ละช่วง ซึ่งสามารถนำมาหาว่าผลบวกของช่วงใหนมีค่ามากที่สุดได้ ด้านล่างคือโค้ดของอัลกอริทึมหลักนี้ + +```python +max_sum = 0 +left_bound = -1 +right_bound = -1 +for i in range(0, n): + S = 0 + for j in range(i, n): + S = S + a[j] + if S > max_sum: + max_sum = S + left_bound = i + right_bound = j +print(max_sum, left_bound, right_bound) +``` diff --git a/md/toi4_temp.md b/md/toi4_temp.md new file mode 100644 index 0000000..0525434 --- /dev/null +++ b/md/toi4_temp.md @@ -0,0 +1,121 @@ +ข้อนี้ให้ค่าอุณหภูมิในตาราง $N \times N$ $(N \leq 20)$ และกำหนดว่าจากช่องใดๆ จะสามารถขยับไปช่องรอบๆ ที่มีอุณหภูมิที่สูงกว่าเท่านั้น โดยมีบางช่องที่ค่าอุณหภูมิเป็น 100 ซึ่งจะเข้าไม่ได้เลย + +### แนวคิด + +สำหรับข้อนี้เราสามารถมองว่าแต่ละช่องของเป็นตารางที่ไม่ได้มีค่าอุณหภูมิเป็น 100 เป็น Node และระหว่างสองช่องที่ติดกันจะมี Edge ที่มีทิศทางจากช่องที่มีอุณหภูมิน้อยกว่าไปยังช่องที่มีอุณหภูมิมากกว่า + +จากนั้นหากทำ State Space Search เพื่อหาว่า Node ใดในตารางบ้างที่มี Path ที่เป็นไปได้จากช่องเริ่มต้นและเอาอันที่มีอุณหภูมิสูงสุดจะได้คำตอบที่ต้องการ + +เช่นในตัวอย่างประกอบในโจทย์สามารถวาด Edge ได้ดังในรูป + +![](../media/toi4_temp/0.png) + +### State Space Search + +State Space Search (การค้นหาในปริภูมิสถานะ) เป็นรูปแบบขั้นตอนวิธีที่จะแทน State (สถานะ) เป็น Node ใน Graph โดย Node $A$ จะมี Edge ไปยัง Node $B$ ก็ต่อเมื่อสามารถเปลี่ยนจาก State $A$ ไป $B$ ได้ในหนึ่งขั้น ใน State Space Search เราจะเริ่มจาก Node ใดๆ แล้วพิจารณา Node รอบข้างที่มี Edge เชื่อมเพื่อพิจารณาแต่ละ State ที่สามารถไปถึงจาก State เริ่ม สำหรับข้อนี้จะต้องหา State ที่มีค่าอุณหภูมิสูงสุด + +รูปประกอบต่อไปนี้เป็นตัวอย่างของ State Space + +![](../media/toi4_temp/1.png) + +ในตัวอย่างนี้จะเห็นได้ว่า State $B$ สามารถไป $D$ และ $D$ กับ $E$ สามารถไปมาซึ่งกันและกัน + +ในการทำ State Space Search จะมีวิธีพื้นฐานหลักๆ สองแบบคือ Depth First Search (DFS) กับ Breadth First Search (BFS) + +#### DFS + +ใน DFS จะทำการค้นหาในรูปแบบ Recursive โดยสำหรับแต่ละ State ที่พิจารณา จะ DFS ลงไปใน Node ที่มี Edge ที่ยังได้ไม่ถูกพิจารณา + +หากทำ DFS จาก State $A$ ในตัวอย่างก่อนหน้านี้จะได้ดังในรูป (ลูกศรสีเขียวแทนการเรียก DFS แบบ Recursive และลูกศรสีแดงแทนการ Return กลับมา) + +![](../media/toi4_temp/2.png) + +ในตัวอย่างนี้จะพิจารณา State ในลำดับ $A,B,D,E,C,F,G$ + +เมื่อนำมาใช้กับข้อนี้จะได้โค้ดดังนี้ + +```cpp +int M; +int T[22][22]; +int visited[22][22]; + +int dfs(int x, int y) { + visited[x][y] = 1; // visited[x][y] จะแทนว่าช่อง x,y ถูกพิจารณาแล้ว + + int ret = T[x][y]; + + int dx[4] = {0, 0, -1, 1}; + int dy[4] = {1, -1, 0, 0}; + for (int u = 0; u < 4; u++) { // พิจารณา 4 ทิศทาง + int ux = x + dx[u]; + int uy = y + dy[u]; + + if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !visited[ux][uy] && + T[ux][uy] != 100 && + T[ux][uy] > T[x][y]) // ช่องใหม่ต้องอยู่ในกรอบ ยังไม่ถูกพิจารณา และ + // ต้องมีอุณหภูมิที่มากกว่าช่องปัจจุบันและไม่ใช่ 100 + ret = max(dfs(ux, uy), ret); // ค่าที่มากที่สุดจะเป็นค่าที่มากสุดของช่องนี้หรือช่องที่สามารถไปจากช่องนี้ + } + + return ret; +} +``` + +ในโค้ดตัวอย่างนี้จะพิจารณาแต่ละช่องที่อยู่ด้าน บน ล่าง ซ้าย หรือ ขวา ว่าเข้าเงื่อนไขที่จะสามารถไปจากช่องปัจจุบันไหมและหากสามารถไปจะเรียก DFS แบบ Recursive + +DFS จะใช้ Memory ใน Stack ตามความยาวของ Path ที่ทำ Recursion ซึ่งสำหรับข้อนี้เป็นได้อย่างมาก $\mathcal{O}(M^2)$ และจะใช้ Time Complexity ตามจำนวน Node และ Edge ที่ถูกพิจารณาซึ่งสำหรับข้อนี้เป็น $\mathcal{O}(M^2)$ เช่นกัน + +#### BFS + +สำหรับ BFS จะต่างจาก DFS ตรงที่เมื่อเจอ Edge ที่ไปได้แล้วจะไม่นำมาพิจารณาทันทีแต่ละนำเข้า Queue ก่อน โดยการพิจารณาแต่ละ Node จะเรียงตามลำดับใน Queue + +ตัวอย่างดังภาพ + +![](../media/toi4_temp/3.png) + +ในตัวอย่างนี้จะพิจารณาเป็นลำดับ $A,B,C,D,E,F,G$ ตามลูกศรสีเขียว + +โค้ด BFS ตัวอย่างสำหรับข้อนี้ + +```cpp +int M; +int T[22][22]; + +int bfs(int x, int y) { + int queued[22][22]; + queued[x][y] = 1; + std::deque> q; // Queue สำหรับการเก็บแต่ละ Node + + q.push_back(std::make_pair(x, y)); // นำ Node เริ่มต้นเข้า Queue + + int ret = T[x][y]; + + while (!q.empty()) { + int qx = q[0].first, qy = q[0].second; + q.pop_front(); // นำ Node แรกมาพิจารณาและเอาออกจาก Queue + + ret = std::max(ret, T[qx][qy]); + + int dx[4] = {0, 0, -1, 1}; + int dy[4] = {1, -1, 0, 0}; + for (int u = 0; u < 4; u++) { + int ux = qx + dx[u]; + int uy = qy + dy[u]; + + if (1 <= ux && ux <= M && 1 <= uy && uy <= M && !queued[ux][uy] && + T[ux][uy] != 100 && T[ux][uy] > T[qx][qy]) { + q.push_back(std::make_pair(ux, uy)); // ใส่ Node ใหม่ที่เจอเข้า Queue + queued[ux][uy] = 1; + } + } + } + + return ret; +} +``` + +สังเกตว่าในโค้ดนี้จะเรียก BFS เพียงรอบเดียวต่างจาก DFS โดยจะนำ Node มาใส่ Queue แทนการใช้ Recursion + +BFS จะใช้ Memory ตามจำนวน Node ที่อยู่ใน Queue และเวลาตามจำนวน Node / Edge ที่ถูกพิจารณา + +ดังนั้นทั้งคู่จะเป็น $\mathcal{O}(M^2)$ สำหรับข้อนี้ diff --git a/media/1061/0.png b/media/toi4_temp/0.png similarity index 100% rename from media/1061/0.png rename to media/toi4_temp/0.png diff --git a/media/1061/1.png b/media/toi4_temp/1.png similarity index 100% rename from media/1061/1.png rename to media/toi4_temp/1.png diff --git a/media/1061/2.png b/media/toi4_temp/2.png similarity index 100% rename from media/1061/2.png rename to media/toi4_temp/2.png diff --git a/media/1061/3.png b/media/toi4_temp/3.png similarity index 100% rename from media/1061/3.png rename to media/toi4_temp/3.png