diff --git a/md/1038.md b/md/1038.md new file mode 100644 index 0000000..453c314 --- /dev/null +++ b/md/1038.md @@ -0,0 +1,39 @@ +ข้อนี้มีภารกิจ $N \leq 20$ ภารกิจ โดยต้องทำทุกภารกิจเพียงแต่ต้องเลือกลำดับที่จะทำ + +หากทำภารกิจที่ $j$ เป็นลำดับที่ $i$ จะมีโอกาสสำเร็จ $a_{(j,i)}$ โจทย์ถามว่าผลคูณความน่าจะเป็นเหล่านี้ที่เป็นไปได้มากสุดคือเท่าไหร่ + +### แนวคิด + +ข้อนี้เป็นโจทย์ Bitmask Dynamic Programming นั่นคือเป็นโจทย์ Dynamic Programming ที่เก็บ State เป็น Bitmask + +สังเกตว่าเราสามารถเก็บคำตอบของแต่ละ State เป็น $DP[S]$ ซึ่งแทนผลคูณความน่าจะเป็นที่จะสำเร็จโดยที่ภารกิจที่สำเร็จแล้วคือ $S$ เมื่อ State $S=(b_{N}b_{N-1}\dots b_0)_2$ เป็นเลขฐานสองโดยที่ $b_j=1$ ถ้าเราทำภารกิจที่ $j$ แล้ว คำตอบจะเป็น $DP[2^N -1]$ เพราะ $2^N-1 = (11\dots1)_2$ (มี 1 $N$ ตัว) + +เช่นถ้า $S=1010_2$ แสดงว่าทำภารกิจที่ 2 กับ 4 แล้ว + +สังเกตว่าสำหรับ State $S$ จำนวนภารกิจที่ทำไปแล้วจะเท่ากับจำนวน bit ที่เป็น $1$ ให้จำนวนนี้เป็น $i_{S}$ + +สำหรับ $DP[0]$ สามารถตั้งเป็น 100 แทนโอกาส 100% + +ในการคำนวณ $DP[S]$ สังเกตว่าจะต้องมีงานอันภารกิจ $j$ ที่ $b_j=1$ ใน $S$ ดังนั้นสามารถพิจารณาทีละงาน $j$ ดังกล่าวว่าผลคูณที่ดีที่สุดที่เป็นไปได้คือเท่าไหร่ ซึ่งจะได้ว่าเป็น $a_{(j, i_{S})} DP[S - (1<2$ + +และให้หาความจุของเซ็ต $S$ ที่มีขนาด $|S|=N \leq 10000$ + +ข้อสังเกตหลักของข้อนี้คือเราสามารถคำนวณผลรวมของความจุของทุกสับเซ็ตที่มีขนาด $k$ ($k\geq 3$) ได้หากเราทราบผลรวมความจุของทุกสับเซตที่มีขนาด $k-1$ + +ให้ผลรวมความจุของสับเซ็ตขนาด $k$ ทุกอันเป็น $M_k$ + +สังเกตว่าทุกเซ็ตขนาด $k-1$ จะเป็นสับเซ็ตของสับเซ็ตขนาด $k$ จำนวน $N-(k-1)$ อันพอดี (เพราะเลือกอีก 1 สมาชิกของ $S$ ที่เหลืออยู่มาเพิ่ม จะมี $N-(k-1)$ ตัวเลือก) ดังนั้น $M_k = M_{k-1} (N-(k-1))$ + +สำหรับ $k=2$ เราสามารถคำนวณ $M_2$ ได้โดยตรงโดยการคำนวณค่ามากกว่าลบค่าน้อยกว่าสำหรับทุกคู่ของสมาชิกของ $S$ ในเวลา $\mathcal{O}(N^2)$ + +จากนั้นคำนวณ $M_3, M_4, \dots, M_N$ ได้ตามสูตร $M_k = M_{k-1} (N-(k-1))$ ซึ่งใช้เวลา $\mathcal{O}(N)$ + +ทั้งหมดจึงใช้เวลา $\mathcal{O}(N^2)$ ซึ่งเร็วเพียงพอสำหรับข้อนี้ + +(เพิ่มเติม: ขั้นตอนการหา $M_2$ สามารถลดเวลาเป็น $\mathcal{O}(N \log N)$ โดยการ sort แล้วนับว่าแต่ละสมาชิกมีกี่สมาชิกที่มากกว่าหรือน้อยกว่าแทนการพิจารณาทุกคู่โดยตรง แต่นั่นไม่จำเป็นสำหรับข้อนี้) \ No newline at end of file diff --git a/md/1142.md b/md/1142.md new file mode 100644 index 0000000..5cdfb84 --- /dev/null +++ b/md/1142.md @@ -0,0 +1,110 @@ +ข้อนี้ให้บริษัทมา $N$ $(N\leq100000)$ บริษัทที่ต้องการจองศูนย์ประชุมซึ่งรองรับได้อย่างมากวันละ $K$ $(K\leq100)$ บริษัท + +บริษัท $i$ ต้องการจองศูนย์นี้ระหว่างวันที่ $X_i$ ถึง $Y_i$ $(1\leq X_i\leq Y_i\leq 1000000000)$ โดยการจองจะเรียงจาก $i=1$ ไปถึง $i=N$ หากวันใดมีบริษัทจองครบแล้ว $K$ บริษัท ศูนย์จะเต็มและไม่รับการจองของบริษัทต่อมาที่ขอจองในช่วงที่ทับวันดังกล่าว (กล่าวคือถ้าในช่วง $[X_i,Y_i]$ มีวันใดที่ครบ $K$ แล้วบริษัทที่ $i$ จะไม่ได้จองเลยทั้งช่วง $[X_i,Y_i]$) + +### แนวคิด + +อย่างแรกสังเกตว่าเราต้องสนใจเพียงวันที่ปรากฎใน $X_i$ หรือ $Y_i$ ใดๆ เป็นพิกัดเพราะหากมีการครบ $K$ วันจะต้องครบที่พิกัดดังกล่าว ดังนั้นสามารถทำ Coordinate Compression (เช่นเดียวกับใน https://programming.in.th/tasks/1138/solution) + +ในการพิจารณาแต่ละคำขอจองจากแต่ละบริษัทเราจะต้อง Query ว่าในช่วง $[X_i,Y_i]$ (ที่ทำ Coordinate Compression แล้ว) มีจองไปแล้วกี่บริษัทในวันที่ถูกจองมากที่สุด + +จากนั้นหากยังมีจองไม่ถึง $K$ จะต้อง Update ช่วงนี้ว่ามีจองเพิ่มขึ้น 1 บริษัทสำหรับทุกวันในช่วง $[X_i,Y_i]$ + +สังเกตว่า Operation ที่เราต้องการมีเพียง Query กับ Update คล้ายกับ Segment Tree เพียงแต่การ Update จะต้องรองรับการ Update ช่วงอย่างมีประสิทธิภาพด้วย โครงสร้างข้อมูลที่รองรับ Operation ดังกล่าวคือ Lazy Segment Tree + +### Lazy Segment Tree + +Lazy Segment Tree เป็นโครงสร้าง Segment Tree (อ่านเพิ่มได้จาก https://programming.in.th/tasks/1147/solution) ที่เพิ่มประสิทธิ์ภาพในการ Update แบบช่วง โดยในการ Update แบบช่วงจะเก็บเพิ่มอีกค่าในแต่ละ Node ซึ่งเป็นค่า "Lazy" ที่แทนว่าในทั้งช่วงนี้ยังค้าง Update อะไรอยู่ + +รูปต่อไปนี้เป็น Lazy Segment Tree ที่เริ่มด้วยค่า Lazy เป็น 0 ทั้งหมด + +![](../media/1142/0.png) + +#### Update + +สำหรับการ `update` เพิ่มช่วง $[X_i,Y_i]$ ด้วยค่า $Z$ จะคล้ายๆ กับ Segment Tree ปกติ โดยต่างกันเพียงแค่ +- ถ้าช่วง $[l,r]$ ที่ Node นี้รับผิดชอบอยู่ใน $[X_i, Y_i]$ จะสามารถเพิ่มค่า Lazy ของ Node นี้ได้แล้ว return เลยโดยไม่ต้องลงไปใน Node ซ้ายหรือยวา +- ถ้า $[l,r]$ ตัดกับ $[X_i, Y_i]$ แต่ไม่ได้อยู่ข้างในทั้งหมดจะต้อง Push ค่า Lazy ไปยัง Node ซ้ายและขวาโดยใช้การ `update` ด้วยค่า Lazy ดังกล่าว + +ตัวอย่างเช่น + +![](../media/1142/1.png) + +Node สีฟ้าคือ Node ที่ช่วงทั้งช่วง $[l,r]$ อยู่ใน Update ซึ่งจะโดนแก้เพียงค่า Lazy เช่นสำหรับ Node ที่คุมช่วง $C[1..3]$ สังเกตว่าสำหรับ Node เหล่านี้ Node ด้านล่างไม่ถูก Visit เลย + +ตัวอย่างโค้ด + +```cpp +int update(int X, int Y, int Z, int n, int l, int r) { + if (X <= l && r <= Y) { // [l,r] is contained in [X,Y] + Lazy[n] += Z; + return ST[n] + Lazy[n]; + } + if (r < X || Y < l) // [X,Y] is not in the range + return ST[n] + Lazy[n]; + + // [l,r] intersects [X,Y] + push_lazy(n, l, r); + + int mid = (l + r) / 2; + int new_left_value = update(X, Y, Z, n * 2, l, mid); + int new_right_value = update(X, Y, Z, n * 2 + 1, mid + 1, r); + + ST[n] = max(new_left_value, new_right_value); + return ST[n]; +} +``` + +#### Push Lazy +สำหรับการ Push ค่า Lazy เราเพียงต้อง `update` Node ลูกซ้ายและขวาด้วยค่า Lazy ปัจจุบัน และแก้ค่า Lazy ให้เป็น $0$ เพื่อแสดงว่าไม่เหลือ Lazy Update ที่ค้างอยู่แล้ว เสร็จแล้วต้องแก้ค่า $\max$ ของช่วงที่เก็บไว้เพื่อให้ถูกต้องหลัง `update` ลูก + +ตัวอย่างโค้ด + +```cpp +void push_lazy(int n, int l, int r) { + if (Lazy[n] == 0) + return; + + int mid = (l + r) / 2; + int new_left_value = update(l, r, Lazy[n], n * 2, l, mid); + int new_right_value = update(l, r, Lazy[n], n * 2 + 1, mid + 1, r); + Lazy[n] = 0; + ST[n] = max(new_left_value, new_right_value); +} +``` + +สังเกตว่าในการ `update` แต่ละครั้งทุกขั้นตอนรวมถึงการ `push_lazy` จะใช้เวลา $\mathcal{O}(1)$ ดังนั้น Time Complexity จะเป็นไปตามจำนวน Node ที่ถูก Visit ซึ่งสามารถพิสูจน์ได้จำนวน Node ที่ต้อง Visit จะเป็น $\mathcal{O}(\log N)$ เช่นเดียวกับที่พิสูจน์ไว้แล้วสำหรับ `update` ของ Segment Tree ทั่วไปใน https://programming.in.th/tasks/1147/solution + +#### Query + +สำหรับการ `query` จะคล้าย `query` ของ Segment Tree ปกติ โดยเพียงต้องบวกค่า Lazy เข้าไปในค่าที่ Return หาก $[l,r]$ อยู่ในช่วง Query และต้อง `push_lazy` เช่นเดียวกับการ `update` หาก $[l,r]$ ตัดกันช่วง Query แต่ไม่ได้อยู่ข้างในช่วงทั้งหมด + +ตัวอย่างประกอบ + +![](../media/1142/2.png) + +ในตัวอย่างนี้ค่าที่ถูก Return คือค่าที่มากสุดของ Node ที่อยู่ในช่วง (Node สีฟ้า) สังเกตว่า Node สำหรับ $C[4]$ กับ $C[5]$ ถูก `push_lazy` จากค่า Lazy ของ Node $C[4..5]$ + +ตัวอย่างโค้ด +```cpp +int query(int A, int B, int n, int l, int r) { + if (A <= l && r <= B) // [l,r] is a subset of [a,b] + return ST[n] + Lazy[n]; + if (B < l || r < A) // [l,r] does not intersect [a,b] + return -1000000001; // -inf + + // [l,r] intersects [a,b] + push_lazy(n, l, r); + + 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); +} +``` +การ `query` จะใช้เวลา $\mathcal{O}(\log N)$ เช่นเดียวกับการ `update` + +### Time Complexity + +ตามที่อธิบายไว้ด้านบนเราจะทำ Coordinate Compression ที่ใช้เวลา $\mathcal{O}(N\log N)$ แล้วใช้ Lazy Segment Tree สำหรับการ `update` และ `query` โดยแต่ละ Operation ใช้เวลา $\mathcal{O}(\log N)$ (เพราะจำนวนพิกัดที่ใช้ใน Lazy Segment Tree หลัง Compression คืออย่างมาก $2N$) ซึ่งต้องทำ $N$ ครั้ง จึงใช้เวลาทั้งหมด $\mathcal{O}(N\log N)$ diff --git a/md/1148.md b/md/1148.md new file mode 100644 index 0000000..c2762a9 --- /dev/null +++ b/md/1148.md @@ -0,0 +1,63 @@ +ข้อนี้มี Sushi ยาว $n$ $(n\leq 1000000)$ ที่สามารถตัดได้ที่ $m$ $(m\leq 20000)$ ตำแหน่ง คือที่ระยะ $R_1,R_2,\dots,R_m$ จากด้านซ้าย โดยจะแบ่งให้เพื่อน $k$ $(k\leq 20000)$ คน คนละหนึ่งชิ้นหลังตัด โดยคนที่ $i$ จะได้รับส่วนที่ $i$ จากด้านซ้ายเรียงกันหลังการตัด + +เพื่อนคนที่ $i$ มีค่าความชอบ $P_i$ $(P_i \leq 1000)$ และจะได้รับความสุขเป็น $P_i$ คูณความยาวของชิ้นที่ได้รับ (ต้องได้ชิ้นที่มีความยาวมากกว่า $0$) + +โจทย์ถามว่าจะได้ความสุขรวมมากที่สุดเท่าไหร่ + +## Dynamic Programming + +เพื่อความสะดวกกำหนดให้ $R_0=0, R_{m+1}=n$ + +ข้อนนี้สามารถมองเป็นโจทย์ Dynamic Programming โดยให้ $DP[i][j]$ แทนค่ารวมมากสุดที่เป็นไปได้หากแบ่ง Sushi แล้วถึง $R_i$ โดยคนที่ได้รับชิ้น $i$ คือเพื่อนที่ $j$ + +เราจะเริ่มจาก $DP[0][0] = 0$ เพราะยังไม่มีความสุขและยังไม่ได้ให้ Sushi ชิ้นใดกับเพื่อนคนไหน และ $DP[0][j] = -\infty$ เพราะไม่สามารถจบที่เพื่อนคนที่ $j$ โดยยังไม่มีใครได้สักชิ้น + +จากนั้นสามารถพิจารณากรณี $i\geq 1$ + +สังเกตว่า $DP[i][1]$ จะเท่ากับ $R_i \times P_1$ เพราะเพื่อนถ้าคนแรกได้ถึง $R_i$ จะต้องได้ทั้งหมดตั้งแต่ $0$ ถึง $R_i$ + +สำหรับ $j>1$ จะสังเกตว่า $DP[i][j] = \max(DP[i-1][j-1], DP[i-1][j]) + (R_i - R_{i-1}) P_j$ เพราะถ้าจะจบการแบ่งถึง $R_i$ โดยให้เพื่อนคนที่ $j$ ชิ้นก่อนหน้าถึง $R_{i-1}$ จะต้องถูกแบ่งให้เพื่อนคนที่ $j$ หรือ $j-1$ เท่านั้น ซึ่งจะเป็น $\max(DP[i-1][j-1], DP[i-1][j])$ และการให้ช่วง $[R_{i-1},R_i]$ กับคนที่ $j$ จะได้ความสุข $(R_i - R_{i-1}) P_j$ + +เห็นได้ว่าการคำนวณแต่ละช่องของ $DP$ จะใช้เวลา $\mathcal{O}(1)$ มี $mk$ ช่องทั้งหมดจึงเป็น $\mathcal{O}(mk)$ + +อย่างไรก็ตามเนื่องจากข้อนี้ให้ Memory เพียง 16 Mb จะไม่สามารถประกาศ $DP$ ให้มี $mk$ ช่องโดยตรงจึงต้องหาวิธีลดการใช้ Memory + +สังเกตว่าในการคำนวณ $DP_i[j] = DP[i][j]$ สำหรับ $i$ ใดๆ เราจะต้องใช้เพียง Array $DP_{i-1}$ เพราะในสูตรที่ใช้จะใช้เพียง $DP_{i-1}$ โดยไม่ต้องใช้ $DP_{1}, DP_2, \dots, DP_{i-2}$ ดังนั้น ณ เวลาใดๆ จะต้องเก็บอย่างมาก $2k$ ค่า ทำให้ลดการใช้ Memory เป็น $\mathcal{O}(k)$ โดยไม่เพิ่ม Time Complexity + +ตัวอย่างโค้ด + +```cpp +#include + +using namespace std; + +int R[20010]; +int P[20010]; + +int DP[2][20010]; +int main() { + int n, m, k; + cin >> n >> m >> k; + + for (int i = 1; i <= m; i++) + cin >> R[i]; + R[m + 1] = n; + + for (int i = 1; i <= k; i++) + cin >> P[i]; + + DP[0][0] = 0; + for (int i = 1; i <= m; i++) + DP[0][i] = -1000000000; + + for (int i = 1; i <= m + 1; i++) { + DP[i % 2][1] = R[i] * P[1]; + for (int j = 2; j <= k; j++) + DP[i % 2][j] = max(DP[(i + 1) % 2][j], DP[(i + 1) % 2][j - 1]) + (R[i] - R[i - 1]) * P[j]; + } + + cout << DP[(m + 1) % 2][k]; +} +``` + +ในโค้ดนี้จะใช้ DP[0] กับ DP[1] สลับกันโดย DP[i%2] จะใช้แทน $DP_i$ ในแต่ละขั้น และ DP[(i+1)%2] จะแทน $DP_{i-1}$ \ No newline at end of file diff --git a/md/1151.md b/md/1151.md new file mode 100644 index 0000000..245f1c1 --- /dev/null +++ b/md/1151.md @@ -0,0 +1,50 @@ +ข้อนี้กำหนดให้มีต้นไม้ $N$ ต้น โดยต้นที่ $i$ สูง $H_i$ และจะเลือกตัดต้นไหนทิ้งก็ได้ + +ต้นที่ $j$ จะเห็นได้หากทุกต้น $i$ ที่ $i < j$ ที่ไม่โดนตัดมี $H_i < H_j$ (ไม่โดนบัง) + +โจทย์นี้ถามว่าหากเลือกตัดดีที่สุดจะเห็นได้มากสุดกี่ต้น + +### แนวคิด + +ข้อนี้เป็นโจทย์ Longest Increasing Subsequence (LIS) โดยตรงเพราะสามารถตัดทุกต้นที่อยู่นอก LIS ให้เหลือ LIS ที่เป็นต้นไม้ที่จะมองเห็นทั้งหมด + +### Longest Increasing Subsequence + +Longest Increasing Subsequence (ปัญหาลำดับย่อยเพิ่มยาวที่สุด) เป็นปัญหาที่ถามว่าหากมี Array $H_1, H_2, \dots, H_N$ จะสามารถเลือก Subsequence (ลำดับย่อย) $H_{a_1}, H_{a_2}, \dots, H_{a_c}$ โดยที่ $a_1 < a_2< \dots < a_c $ และ $H_{a_1} < H_{a_2}< \dots < H_{a_c}$ ที่ยาวสุดได้เท่าไหร่ + +สำหรับวิธีการหา Longest Increasing Subsequence จะสามารถใช้ Dynamic Programming โดยจะทำเป็นขั้นๆ หนึ่งขั้นสำหรับทุกค่า $H_i$ โดยเก็บค่า $DP[c]$ ที่แทนว่าหากเลือก Subsequence ใน $H_1, H_2, \dots, H_i$ ที่มีความยาว $c$ จะสามารถจบได้ด้วยค่าสุดท้ายต่ำสุดเท่าไหร่ + +สังเกตว่า $DP[0], DP[1] , \dots, DP[N] $ ควรเป็นลำดับไม่ลด $(DP[0] \leq DP[1] \leq \dots \leq DP[N])$ เพราะหากมีลำดับยาว $c$ ที่จบด้วยค่า $DP[c]$ จะเลือกตัดตัวสุดท้ายจากลำดับที่ได้ค่า $DP[c]$ ออกจะทำให้เหลือลำดับยาว $c-1$ ที่จบด้วยค่า $DP[c-1] < DP[c]$ + +ในตอนเริ่มจะมี $DP[c] = \infty$ สำหรับ $c\geq 1$ และ $DP[0]=-\infty$ แทน Subsequence ว่างที่มีความยาว $0$ และจบด้วยค่าต่ำสุด $-\infty$ เพราะจะเอาค่าอะไรมาต่อก็ได้โดยที่ Subsequence ที่ได้ยังเป็นลำดับที่เพิ่มอยู่ + +สมมิตว่า $DP[0], DP[1] , \dots, DP[N] $ เป็นลำดับไม่ลด สำหรับแต่ละ $i$ จะต้องหา $DP[x]$ ที่มี $x$ มากสุดที่ $DP[x] H_i$ สำหรับ $a > x$ (ถ้าเอาไปต่อจะไม่เป็นลำดับย่อยเพิ่ม) ดังนั้นการพิจารณาเพียง $DP[x]$ เพื่อแก้ค่า $DP[x+1]$ นั้นถูกต้องแล้ว + +คุณสมบัติไม่ลดของ $DP[0], DP[1] , \dots, DP[N] $ ทำให้เราสามารถใช้ Binary Search ในการหาค่า $x$ ดังกล่าวที่เป็นค่าที่มากสุดที่ $DP[x] < H_i$ ซึ่งใช้เวลา $\mathcal{O}(\log N)$ ในแต่ละครั้ง ทั้งขั้นตอนวิธีจึงใช้เวลา $\mathcal{O}(N \log N)$ + +คำตอบจะเป็นค่า $x+1$ มากที่สุดที่ได้จากแต่ละขั้น + +ตัวอย่างโค้ดประกอบคำอธิบาย + +```cpp + for (int i = 1; i <= n; i++) + DP[i] = 1000000000; + + int ans = -1000000000; + for (int i = 1; i <= n; i++) { + int b = 0, e = n, x = 0; + while (b <= e) { + int mid = (b + e) / 2; + if (DP[mid] < H[i]) { + x = max(x, mid); + b = mid + 1; + } else + e = mid - 1; + } + + DP[x + 1] = min(DP[x + 1], H[i]); + ans = max(ans, x + 1); + } +``` \ No newline at end of file diff --git a/md/2030.md b/md/2030.md new file mode 100644 index 0000000..39627f9 --- /dev/null +++ b/md/2030.md @@ -0,0 +1,60 @@ +ข้อนี้ให้ Array $a_1, a_2, \dots, a_N$ $(N \leq 1000000)$ และให้หาจำนวน Subarray ที่มีพิสัย (ค่ามากสุด - ค่าต่ำสุด) อยู่ในช่วง $[p,q]$ + +### แนวคิด + +ข้อนี้เป็นโจทย์ Sliding Window + +อย่างแรกสังเกตว่าเราสามารถคำนวณจำนวนลำดับย่อยที่มีพิสัยในช่วง $[p,q]$ เป็น (จำนวนลำดับย่อยที่มีพิสัยไม่เกิน $q$) - (จำนวนลำดับย่อยที่มีพิสัยไม่เกิน $p-1$) ดังนั้นสำหรับข้อนี้เราจะพิจารณาการหาจำนวนลำดับย่อยที่มีพิสัยไม่เกิน $q$ + +ในจำนวนลำดับย่อยที่มีพิสัยไม่เกิน $q$ เราสามารถพิจารณาจำนวนลำดับย่อยที่เข้าข่ายที่จบที่แต่ละ $a_i$ หากนำจำนวนนี้มาบวกกันสำหรับทุก $a_i$ จะได้คำตอบที่ต้องการ + +สังเกตว่าสำหรับ $m &a) { + deque d_max; + deque d_min; + + long long res = 0; + long long l = 0; + for (int i = 0; i < a.size(); i++) { + while (d_max.size() > 0 && a[d_max.back()] <= a[i]) + d_max.pop_back(); + d_max.push_back(i); + + while (d_min.size() > 0 && a[d_min.back()] >= a[i]) + d_min.pop_back(); + d_min.push_back(i); + + while (l <= i && a[d_max[0]] - a[d_min[0]] > q) { + l++; + + while (d_max.size() > 0 && d_max[0] < l) + d_max.pop_front(); + + while (d_min.size() > 0 && d_min[0] < l) + d_min.pop_front(); + } + res += (i - l + 1); + } + return res; +} +``` + +นี่คือโค้ดตัวอย่างสำหรับการหาจำนวน Subarray ที่มีพิสัยไม่เกิน $q$ + +เราจะเก็บ Sliding Window สองอันคือ `d_min` กับ `d_max` และเก็บค่า $l$ ที่อธิบายไดว้ + +ในแต่ละขั้นจะ push $i$ เข้า Sliding Window ทั้งสองโดยเอาข้อมูลที่สำคัญแล้วออก (ต่ำกว่าค่าปัจจุบันสำหรับ `d_max` หรือ สูงกว่าค่าปัจจุบันสำหรับ `d_min`) + +จากนั้นจะเพิ่มค่า $l$ และ pop ค่าที่อยู่ก่อน $l$ ในแต่ละ Window จนพิสัยไม่เกิน $q$ diff --git a/md/o59_mar_c2_binary.md b/md/o59_mar_c2_binary.md new file mode 100644 index 0000000..2b5c91a --- /dev/null +++ b/md/o59_mar_c2_binary.md @@ -0,0 +1,85 @@ +ข้อนี้ถามว่าหากแปลงต้นไม้ $T$ ให้เป็นต้นไม้ไบนารี $S$ จะทำให้ $S$ มีความลึกน้อยสุดได้เท่าไหร่ โดยในการแปลงจะจะต้องใช้วิธีการสร้างจุดยอดใหม่เป็น parent ของสองจุดยอดใดๆ ที่เคยเป็น sibling กัน + +สังเกตว่าสำหรับ Subtree ใดๆ ส่ามารถหาได้ว่าจะแปลง Subtree นี้ให้เป็นต้นไม้ไบนารีที่มีความลึกต่ำสุดได้เท่าใดโดยไม่ต้องพิจารณาจุดยอดนอก Subtree นั้นๆ + +เราจึงสามารถกำหนด $H[x]$ เป็นความลึกที่ต่ำที่สุดที่เป็นไปได้ของต้นไม้ไบนารีที่มีรากเป็น $x$ + +สมมิตว่าเราคำนวณ $H[c]$ สำหรับทุกจุดยอดลูก $c$ ของ $x$ ไปแล้ว หาก $x$ ไม่มีลูกหรือมีลูกไม่เกิน 2 จุดยอด ความลึกเป็นเพียง ค่า $H$ ของลูกที่สูงสุดบวก 1 แต่หากมีมากกว่า 2 จุดยอดจะต้องพิจารณาวิธีสร้างต้นไบนารีจากจุดยอดลูกเหล่านี้ที่จะทำให้ความลึกต่ำสุดที่จะเป็นไปได้ + + +### Greedy Algorithm + +เราจะพิสูจน์ว่าการในการสร้างต้นไม้ไบนารีที่ตื้นที่สุดที่มีรากเป็น $x$ หาก $x$ มีลูกเกิน 2 ลูก จะสามารถเริ่มโดยการสร้างจุดยอดพิเศษเป็น parent ของสองจุดยอดลูกที่มีค่า $H$ ต่ำสุดเสมอ + +นั่นคือหากจุดยอดลูก $a$ และ $b$ เป็นจุดยอดที่ $H[a]$ กับ $H[b]$ มีค่าที่น้อยที่สุดที่เป็นไปได้สองค่า เราจะพิสูจน์ว่ามีต้นไบนารีที่มีรากเป็น $x$ ที่มีความลึก $H[x]$ (กล่าวคือตื้นสุดที่เป็นไปได้) โดยมี $a$ และ $b$ เป็น sibling (มี parent เป็นจุดยอดพิเศษเดียวกัน) + +พิจารณาต้นไม้ไบนารี $O$ ใดๆ ที่มีความลึกน้อยที่สุด เราจะพิสูจน์ว่ามีต้นไม้ $O'$ ที่มีความลึกไม่เกิน $O$ โดยมี $a$ และ $b$ เป็น sibling + +ให้ $d_O(z)$ เป็นความลึกของ $z$ ใน $O$ + +ให้ $a'$ เป็นหนึ่งในสองจุดยอด $a$ หรือ $b$ ดังกล่าวที่มี $d_O(a')$ มากสุดและ $b'$ เป็นอีกจุดยอด นั่นคือ $d_O(a') \geq d_O(b')$ + +สังเกตได้ว่าทุกจุกยอดลูกจะมี sibling ในต้นไบนารี $O$ เพราะในขั้นตอนการเพิ่มจุดยอดพิเศษ เพราะหากจุดยอดพิเศษนั้นไม่มี sibling แสดงว่าก่อนการเพิ่ม parent ของทั้งสองลูกมีเพียงสองลูกอยู่แล้ว ดังนั้นการเพิ่มจุดยอดพิเศษนี้จึงไม่จำเป็นและเพิ่มความลึกของ $O$ ซึ่งขัดกับการที่ $O$ มีความลึกน้อยสุดที่เป็นไปได้ + +ดังนั้นเราจะสามารถจำแนกเป็น 2 กรณี + +1. $a'$ และ $b'$ เป็น sibling กัน +2. $a'$ มี sibling ที่ไม่ใช่ $b'$ + +หากเข้ากรณีแรกสามารถเลือก $O'=O$ ตามที่ต้องการพิสูจน์ + +หากเข้ากรณีที่สอง ให้ sibling ของ $a'$ และ $b'$ เป็น $c$ และ $d$ ตามลำดับ เราสามารถเลือก $O'$ เป็นต้น $O$ ที่สลับ $b'$ มาเป็น sibling $a'$ และ $c$ ไปเป็น sibling $d$ ทั้งนี้จะทำให้ความลึกจุดยอด descendent ลึกสุดของ $b'$ ในต้นไบนารีใหม่เป็น $d_O(a') + H[b']$ และของ $d$ เป็น $d_O(b') + H[d]$ จากเดิม $d_O(b') + H[b']$ และ $d_O(a') + H[d]$ ตามลำดับ +เนื่องจาก $H[b'] \leq H[d]$ และ $d_O(b') \leq d_O(a')$ (จากนิยามของ $a', b'$ และ $H$) จะได้ว่า $\max(d_O(a') + H[b'], d_O(b') + H[d]) \leq \max(d_O(b') + H[b'], d_O(a') + H[d]) = d_O(a') + H[d]$ กล่าวคือการสลับนี้จะไม่เพิ่มความลึกของ $O'$ เมื่อเทียบกับ $O$ เพราะความลึกที่มากสุดในบรรดาจุดยอดที่ถูกกระทบไม่เพิ่ม + +เพราะฉะนั้นจึงสรุปได้ว่าไม่ว่ากรณีใดๆ จะมีต้นไม้ไบนารี $O'$ ที่เลือก $a$ และ $b$ มาเป็น sibling กันและมีความลึกต่ำสุดที่เป็นไปได้ + +เมื่อเราเลือกสร้างจุดยอดพิเศษ $c$ มาเป็นลูกใหม่ของ $x$ ที่มีลูกเป็น $a$ กับ $b$ เราสามารถใช้เหตุผลแบบเดิมเลือกสองลูกที่มี $H$ ต่ำสุดมาเป็นลูกของจุดยอดพิเศษใหม่อีกรอบเรื่อยๆ จน $x$ เหลือลูกเพียงสองลูกและจะได้ว่า $H[x]$ คือ $\max(H[y],H[z])$ ของลูกที่เหลืออยู่ $y,z$ + +### Algorithm เต็ม + +1. อ่าน input โดยเก็บ Adjacency List ของแต่ละจุดยอดว่ามีจุดยอดไหนเป็นลูกบ้าง +2. คำนวณ $H[x]$ สำหรับทุก $x$ โดยใช้วิธี recursive แบบด้านบน + - หาก $x$ ไม่มีลูกจะได้ $H[x]=0$ และรีเทิร์น + - มิเช่นนั้น $x$ มีลูกอย่างน้อย 1 ลูก ให้คำนวณ $H[c]$ สำหรับทุกลูก $c$ ก่อน + - เอา $H[c]$ ทุกค่ามาไว้ใน minimum priority queue + - เลือกสองค่าต่ำสุดใน priority queue เพื่อเอาออกและนำมาเป็นลูกของจุดพิเศษใหม่ (หากค่าเดิมคือ $A$ และ $B$ จุดยอดพิเศษใหม่จะมีความลึก $max(A,B)+1$ เพราะเพิ่มความลึกที่มากสุดในนั้นไป 1) และใส่จุดยอดใหม่เข้าไปใน queue โดยทำซ้ำจนใน queue เหลือไม่เกิน 2 ค่า + - เลือกค่าที่มาก $M$ ที่สุดที่เหลืออยู่ใน queue และตั้ง $H[x]=M+1$ + +3. คำตอบคือ $H[1]$ + +การอ่านข้อนำเข้าและเก็บ Adjacency List ใช้เวลา $\mathcal{O}(N)$ + +การคำนวณ $H$ ใช้เวลา $\mathcal{O}(C_x \log C_x)$ สำหรับทุก $x$ เมื่อ $C_x$ คือจำนวนลูกของ $x$ เพราะเกิดการ push และ pop จาก priority queue $\mathcal{O}(C_x)$ รอบ + +ทั้งหมดจึงใช้เวลา $\mathcal{O}(N) + \Sigma_{x=1}^{N} \mathcal{O}(C_x \log C_x) = \mathcal{O}(N \log N)$ + +#### ตัวอย่างโค้ดสำหรับการคำนวณ $H$ ในขั้นตอนที่ 2 + +```cpp +int H(int x) { + if (child[x].size() == 0) { + return 0; + } + + std::priority_queue, std::greater> q; + + for (int i = 0; i < child[x].size(); i++) + q.push(H(child[x][i])); + + while (q.size() > 2) { + int A = q.top(); + q.pop(); + int B = q.top(); + q.pop(); + q.push(max(A, B) + 1); + } + + int H_x = 0; + while (q.size() > 0) { + H_x = max(H_x, q.top() + 1); + q.pop(); + } + + return H_x; +} +``` \ No newline at end of file diff --git a/md/toi15_cave.md b/md/toi15_cave.md new file mode 100644 index 0000000..a0a63e0 --- /dev/null +++ b/md/toi15_cave.md @@ -0,0 +1,118 @@ +ข้อนี้กำหนดให้มีถ้ำที่มี $N$ $(N\leq 2000)$ โถงและทางเชื่อมระหว่างโถง $E$ $(E\leq 10000)$ ทางเชื่อม โดยโจทย์นี้ต้องให้จำลองหาเวลาที่จะใช้เพื่อไปจากโถง $P$ ไปยังโถง $U$ ณ เวลาต่างๆ ในตอนแรกระดับน้ำจะอยู่ที่ $h=0$ แต่ระดับน้ำจะเพิ่มขึ้นเรื่อยๆ และทำให้ระยะเวลาการเดินทางในแต่ละทางเชื่อมเพิ่มขึ้น + +แต่ละทางเชื่อมจะไปจากโถง $Q$ ไปยังโถง $R$ และใช้เวลา $T_{Q,R}$ (จะไปจาก $Q$ ไปยัง $R$ เท่านั้น ไม่สามารถไปจาก $R$ ไป $Q$ ด้วยทางเชื่อมเดียวกัน) ในตอนเริ่มคือ $h=0$ เมื่อผ่านไปถึงเวลา $h$ จะกลายเป็น $T_{Q,R}+h$ ยกเว้นทางเชื่อมที่ติดกับ $P$ ซึ่งจะเป็น $T_{Q,R}$ ตลอด + +จากนั้นโจทย์จะถามผลการจำลอง $L$ $(L \leq 500000)$ คำถามว่าเวลาที่ใช้เดินทางจาก $P$ ไป $U$ ที่เป็นไปได้ต่ำสุดคือเท่าไหร่เมื่อระดับน้ำคือ $h_i$ + +## แนวคิด + +ข้อนี้เป็นโจทย์ Dijkstra + +อย่างแรกสังเกตว่าเราไม่จำเป็นต้องกลับมายังจุดเริ่มต้นเพราะเพียงแต่จะทำให้การไปถึง $U$ ช้าลง ดังนั้นจึงสามารถตัดทางเชื่อมใดๆ ที่มีจบออกมายัง $P$ + +สมมติว่าเส้นทางเดินที่เลือกคือ $P, X_1, X_2, \dots, X_{c-1}, U$ ณ ระดับความสูงน้ำ $h_i$ ระยะเวลาการเดินทางรวมคือ $T_{P, X_1} + (T_{X_1, X_2} + h_i) + (T_{X_2,X_3} + h_i) + \dots + (T_{X_{c-1}, U} + h_i) $ ดังนั้นหากทำ Dijkstra สำหรับทุกการจำลอง $L$ ครั้งจะได้ทำตอบที่ต้องการ แต่ะจะใช้เวลานานเกินไป $\mathcal{O}(L(N + E\log N))$ ซึ่งช้าเกินไป + +สังเกตได้ว่า $P, X_1, X_2, \dots, X_{c-1}, U$ จะผ่านทางเชื่อม $c$ ทางเชื่อมและ $T_{P, X_1} + (T_{X_1, X_2} + h_i) + (T_{X_2,X_3} + h_i) + \dots + (T_{X_{c-1}, U} + h_i) = (c-1) h_i + T_{P, X_1} + T_{X_1, X_2} + T_{X_2,X_3} + \dots + T_{X_{c-1}, U} $ ดังนั้นเวลาที่ใช้คือระยะทางที่เวลา $h=0$ บวกกับจำนวนทางเชื่อมที่ผ่านลบ $1$ คูณกับ $h_i$ + +เราสามารถแปลงกราฟเดิมเป็นกราฟใหม่โดยมองว่าแต่ละจุดยอดใน Graph แทน State ว่าอยู่ที่โถง $x$ และผ่านมาแล้ว $c$ ทางเชื่อม +เราไม่ต้องสนใจ State ที่ผ่านไปเกิน $N-1$ ทางเชื่อมเพราะเส้นทางดังกล่าวจะมี Cycle ที่สามารถตัดออกและลดระยะเวลาได้ ดังนั้นจะมี $N^2$ State คือจบที่โถง $x$ ระหว่าง $0$ ถึง $N-1$ และผ่านไปแล้ว $c$ ทางเชื่อมระหว่าง $0$ ถึง $N-1$ ทางเชื่อมในกราฟใหม่จะเพิ่มจากกราฟเก่าที่มี $E$ เป็น $EN$ เพราะจะต้องมีหนึ่งทางเชื่อมสำหรับทุก $c$ ตั้งแต่ะ $0$ ถึง $N-1$ + +การทำ Dijkstra บนกราฟใหม่นี้จะทำให้ได้ $dist[x][c]$ แทนผลรวม $T_{Q,R}$ ในเส้นทางจาก $P$ ไป $u$ โดยผ่าน $c$ ทางเชื่อมที่ต่ำสุดที่เป็นไปได้ โดย Dijkstra จะใช้เวลา $\mathcal{O}(N^2 + EN \log N))$ + +Dijkstra จะทำให้ได้คำตอบว่าหากเริ่มที่ $P$ และไปถึง $U$ โดยผ่านไปแล้ว $c$ ทางเชื่อมจะทำให้ผลรวม $dist[U][c] = T_{P, X_1} + T_{X_1, X_2} + T_{X_2,X_3} + \dots + T_{X_{c-1}, U}$ เป็นไปได้ต่ำสุดคือเท่าไหร่ + +เมื่อเรามี $dist[U][c]$ สำหรับทุก $c$ ตั้งแต่ $1$ ถึง $N-1$ แล้วเราจะสามารถหาคำตอบแต่ละ $h_i$ โดยการไล่ $c$ หาค่า $dist[U][c] + (c-1) h_i$ ที่ต่ำสุดเป็นคำตอบซึ่งจะใช้เวลา $\mathcal{O}(LN)$ ซึ่งพอสำหรับข้อนี้ + +## Dijkstra's Algorithm + +Dijkstra's Algorithm เป็นขั้นตอนวิธีที่ใช้หาระยะทางสั้นในกราฟสุดจากจุดยอดเริ่มต้น $S$ ไปยังทุกจุดยอด สมมิตว่ากราฟที่พิจารณามี $N$ จุดยอดและ $E$ ทางเชื่อม + +ให้ระยะของเส้นเชื่อมระหว่าง $a$ กับ $b$ เป็น $w_{a,b}$ (สังเกตว่าใน Dijkstra หากมีมากกว่าหนึ่งเส้นเชื่อมระหว่าง $a$ กับ $b$ จะสามารถเลือกอันที่สั้นสุดมาอันเดียวเราจึงสามารถพิจารณาแค่กรณีที่กราฟเป็นกราฟเชิงเดียว (Simple Graph) ซึ่งแปลว่าจาก $a$ ไป $b$ มีอย่างมากเส้นเชื่อมเดียว) + +หลักการทำงานของ Dijkstra คือจะเก็บระยะ $dist[i]$ สำหรับแต่ละจุดยอด $i$ ในกราฟซึ่งแทนระยะทางต่ำสุดจาก $S$ ไปยัง $i$ ที่พบแล้ว ในตอนเริ่มต้นจะตั้ง $dist[S]=0$ และ $dist[i]=\infty$ สำหรับ $i\neq S$ จากนั้นในแต่ละขั้นจะเลือกจุดยอด $a$ ที่ยังไม่ได้พิจารณาที่มี $dist[a]$ ต่ำสุด (โดยที่ $a$ ไปถึงได้นั่นคือ $dist[a] \neq \infty$) และพิจารณาแต่ละเส้นเชื่อมออกจาก $a$ ไปยัง $b$ ว่า $dist[a] + w_{a,b}$ ต่ำกว่า $dist[b]$ ในปัจจุบันหรือไม่ หากใช่จะแก้ $dist[b] = dist[a] + w_{a,b}$ (เพราะเป็นเส้นทางที่ผ่าน $a$ ไปถึง $b$ ที่ใช้เวลาดังกล่าว) + +ใน Implementation ทั่วไป จะใช้ Binary Heap เพื่อหา $a$ ที่มี $dist[a]$ ต่ำสึดในแต่ละขั้นจนกว่า Heap จะว่าง ซึ่งจะทำให้การแก้ค่า $dist[b]$ และใส่ใน Heap ใหม่ใช้เวลา $\mathcal{O}(\log E)$ และการหาค่าต่ำสุดจะใช้เวลา $\mathcal{O}(\log E)$ เช่นกัน (อ่านเรื่อง Binary Heap เพิ่มได้จาก https://programming.in.th/tasks/1021/solution) สำหรับกราฟเชิงเดียวจะได้ว่า $\mathcal{O}(\log E) = \mathcal{O}(\log N)$ เพราะ $E \leq N^2$ + +แต่ละทางเชื่อมจะถูกพิจารณาอย่างมากครั้งเดียวดังนั้นการใส่ค่าใหม่ใน Binary Heap จะเกิดขึ้นอย่างมาก $\mathcal{O}(E)$ ครั้ง ซึ่งแปลว่าเวลาที่ใช้กับขั้นตอนวิธีทั้งหมดรวมทั้งการนำเข้าและเอาออกจะเป็น $\mathcal{O}(E \log V)$ เมื่อรวมกับการตั้งค่าเริ่มต้นของ $dist[i]$ สำหรับทุก $i$ จะได้เป็น $\mathcal{O}(V + E\log V)$ สำหรับทั้งขั้นตอนวิธี + +โค้ดตัวอย่างสำหรับ Dijkstra ทั่วไป (ดัดแปลงจาก https://usaco.guide/CPH.pdf#page=136) + +```cpp +int dist[MAX]; +bool visited[MAX]; + +vector> edges[MAX]; + +void dijkstra(int N, int S) { + for (int i = 1; i <= N; i++) + dist[i] = INF; + dist[S] = 0; + + priority_queue> q; + + q.push({0, S}); + while (!q.empty()) { + int a = q.top().second; + q.pop(); + if (visited[a]) + continue; + visited[a] = true; + for (auto e : edges[a]) { + int b = e.first, w = e.second; + if (dist[a] + w < dist[b]) { + dist[b] = dist[a] + w; + q.push({-dist[b], b}); + } + } + } +} +``` +โค้ดนี้เก็บเส้นเชื่อมเป็น `edges[a]` สำหรับทางเชื่อมที่ออกจาก $a$ ด้วย `pair` โดยค่าแรกใน `pair` จะเป็นอีกปลาย $b$ ของแต่เส้นเชื่อม และค่าที่สองจะเป็นระยะของเส้น $w_{a,b}$ + +ในโค้ดนี้ใช้`std::priority_queue` เป็น Heap สังเกตว่าจะใช้ `-dist[b]` เป็นค่าแรกเพราะ `std::priority_queue` จะเอาค่ามาสุดมาก่อน การใช้ค่าติดลบจึงทำให้เอาค่า `dist` ที่ต่ำสุดมาก่อนตามที่ต้องการ + +### Dijkstra สำหรับข้อนี้ + +สำหรับข้อนี้จะต้องแปลงให้แต่ละจุดยอดในกราฟเก็บทั้งหมายเลขของโถง $x$ และจำนวน $c$ เพื่อให้เป็น State ตามที่อธบิายไว้ + +ดังนั้นจะต้องแก้ให้ `dist` และ `visited` ให้เป็น Array 2 มิติ และใน `priority_queue` จะต้องเป็น State เป็น `pair` ของค่าแทนที่จะเป็นค่าเดียว + +```cpp +long long dist[MAX][MAX]; +int visited[MAX][MAX]; + +vector> edges[MAX]; + +void dijkstra(int N, int S) { + priority_queue>> q; + + for (int i = 0; i <= N; i++) + for (int j = 0; j <= N; j++) + dist[i][j] = 1000000000000000000LL, visited[i][j] = false; + + dist[S][0] = 0; + q.push({-0, {S, 0}}); + + while (!q.empty()) { + int a = q.top().second.first; + int c = q.top().second.second; + q.pop(); + + if (visited[a][c]) + continue; + + visited[a][c] = true; + + if (c >= N) // ไม่ต้องพิจารณาไปต่อถ้า State ปัจจุบันผ่านมาแล้ว N ทางเชื่อม + continue; + + for (auto e : edges[a]) { + int b = e.first, w = e.second; + if (dist[b][c + 1] > dist[a][c] + w) { + dist[b][c + 1] = dist[a][c] + w; + q.push({-dist[b][c + 1], {b, c + 1}}); + } + } + } +} +``` \ No newline at end of file diff --git a/media/1142/0.png b/media/1142/0.png new file mode 100644 index 0000000..f000747 Binary files /dev/null and b/media/1142/0.png differ diff --git a/media/1142/1.png b/media/1142/1.png new file mode 100644 index 0000000..f7f8dc7 Binary files /dev/null and b/media/1142/1.png differ diff --git a/media/1142/2.png b/media/1142/2.png new file mode 100644 index 0000000..1696357 Binary files /dev/null and b/media/1142/2.png differ