"การออกแบบอัลกอริทึมที่แค่ทำงานได้ถูกต้องนั้นง่าย แต่ความท้าทายที่แท้จริงคือการสร้างอัลกอริทึมที่ ทำงานได้รวดเร็ว ภายใต้ข้อจำกัด" [1, 7]
"การประมาณการว่าอัลกอริทึมจะใช้เวลาทำงานเท่าใด โดยมองเป็นฟังก์ชันของ ขนาดของข้อมูลนำเข้า (Input Size: n)" [5, 6]
• ขนาดของ Array [5, 6]
• ความยาวของ String [5, 6]
• จำนวน Node ใน Graph [8]
นิยมใช้ Big O Notation เช่น \(O(n)\), \(O(n^2)\) เพื่อบอกอันดับความเร็ว (Order of Magnitude) [5, 6]
Big O สนใจเพียงแนวโน้มการเติบโต ดังนั้น \(3n\), \(n+5\), หรือ \(n/2\) จะถูกนับเป็น \(O(n)\) ทั้งหมด [9, 10]
หากมีหลายขั้นตอน ให้นับขั้นตอนที่ช้าที่สุดเป็นตัวกำหนด เช่น \(O(n) + O(n^2) \rightarrow O(n^2)\) [11, 12]
Complexity: \(O(n)\)
คำอธิบาย: โค้ดภายในลูปทำงาน \(n\) ครั้งตามขนาดข้อมูล [9, 10]
Complexity: \(O(n^2)\)
คำอธิบาย: หากมีลูปซ้อนกัน \(k\) ชั้น ความซับซ้อนจะเป็น \(O(n^k)\) [9, 10]
Total Complexity: \(O(n^2)\)
เหตุผล: ขั้นตอนที่ช้าที่สุดคือ Bottleneck ของโปรแกรม [11, 12]
ขึ้นอยู่กับ จำนวนครั้งที่เรียกฟังก์ชัน \(\times\) ความซับซ้อนของการเรียก 1 ครั้ง [13, 14]
| Big O | นิยาม | ตัวอย่าง |
|---|---|---|
| \(O(1)\) | Constant Time | การคำนวณสูตรคณิตศาสตร์ [17, 19] |
| \(O(\log n)\) | Logarithmic | Binary Search [17, 19] |
| \(O(n)\) | Linear | วนลูป 1 รอบ [20, 21] |
| \(O(n \log n)\) | Sorting | Efficient Sorting (เช่น sort) [20, 21] |
| \(O(n^2)\) | Quadratic | Nested loops [22, 23] |
หมายเหตุ: ในการวิเคราะห์อัลกอริทึม ฐานของ Logarithm มักจะเป็น 2 [24]
การดำเนินการต่อวินาที
นอกจากเวลาแล้ว เรายังต้องคำนึงถึง หน่วยความจำ ที่ใช้อีกด้วย โดยใช้ Big O Notation เช่นเดียวกัน [30]
Big O ละทิ้ง ค่าคงที่ (Constant Factors) [31-33]
ทั้ง 3 ตัวคือ \(O(n)\) แต่ในโลกของ CP ค่าคงที่ที่ซ่อนอยู่ส่งผลต่อ Wall-clock time จริง! [33]
โจทย์เดียวกัน แต่อัลกอริทึมต่างกัน ผลลัพธ์ต่างกันมหาศาล! [31, 32]
Example: [-1, 2, 4, -3, 5, 2, -5, 2] \(\rightarrow\) Max is 10
ผลลัพธ์จากการทดสอบบนคอมพิวเตอร์สมัยใหม่ [38, 39]
เมื่อ \(O(n)\) ยังเร็วไม่พอ...
Binary Search
เปลี่ยนจาก Linear (\(O(n)\)) เป็น Logarithmic (\(O(\log n)\)) [26]
พัก 15 นาที ก่อนเข้าสู่หัวใจของการค้นหา
"The array must be SORTED!"
หากข้อมูลยังไม่เรียงลำดับ การทำ Binary Search จะให้คำตอบที่ ผิดพลาด [1, 3]
ดังนั้นในหลายโจทย์ เรามักต้อง std::sort ก่อนทำ Binary Search [4, 5]
low = 0, high = n-1 [6]
mid = (low + high) / 2 [6]
arr[mid] == target หยุดการค้นหา [6]
arr[mid] > target ให้หาต่อในครึ่ง "ซ้าย" (เลื่อน high) [6]
arr[mid] < target ให้หาต่อในครึ่ง "ขวา" (เลื่อน low) [6]
*หมายเหตุ: การใช้ low + (high - low) / 2 ช่วยป้องกันปัญหา Integer Overflow [7, 8]
อีกหนึ่งวิธีในการมอง Binary Search คือการ "กระโดด" ไปข้างหน้าด้วยขนาดที่ลดลงทีละครึ่ง [9, 10]
Step 1
Jump n/2
Step 2
Jump n/4
Step n
Jump 1
วิธีนี้ได้รับความนิยมมากในหมู่นักแข่งระดับโลก เพราะเขียนโค้ดได้กระชับและลดความผิดพลาดเรื่อง boundaries [11]
while จะทำงานสูงสุดไม่เกิน 2 ครั้งต่อหนึ่งขนาดการกระโดด [12, 13]
ไม่ต้องเขียนเองเสมอไป! C++ มีฟังก์ชันที่ปรับแต่งมาอย่างดีแล้ว [12, 14]
binary_search()คืนค่าเป็น boolean (true/false) ว่ามีข้อมูลอยู่ในช่วงที่ระบุหรือไม่ [14]
lower_bound()คืนค่า iterator ไปยังตัวแรกที่ "ไม่น้อยกว่า" (>=) x [15, 16]
upper_bound()คืนค่า iterator ไปยังตัวแรกที่ "มากกว่า" (>) x [15, 16]
equal_range()คืนค่าทั้งขอบเขตบนและล่างพร้อมกัน (เป็น pair) [15, 16]
"จะเกิดอะไรขึ้น... หากสิ่งที่เราต้องการค้นหาไม่ใช่ตำแหน่งข้อมูลใน Array
แต่คือ 'คำตอบ' ของโจทย์ที่เราคำนวณโดยตรงไม่ได้?"
Binary Search on Answer
"เมื่อเราไม่สามารถคำนวณคำตอบโดยตรงได้ แต่เราสามารถ ตรวจสอบ (Check) ได้ว่าค่านี้ 'ใช้ได้' หรือไม่" [7, 17]
เราจะสุ่มเดาคำตอบ (x) และใช้ฟังก์ชัน bool ok(x) เพื่อดูว่าคำตอบนั้นเป็นไปได้ไหม
แล้วใช้ Binary Search ค่อยๆ บีบขอบเขตเพื่อหาค่า x ที่ดีที่สุด [18, 19]
โจทย์ที่จะทำ Binary Search on Answer ได้ ต้องมีคำตอบที่ต่อเนื่องกันแบบนี้: [18, 20]
(F = ใช้ไม่ได้, T = ใช้ได้)
เมื่อมันเปลี่ยนจาก F เป็น T แล้ว ต้องเป็น T ไปตลอด (หรือสลับกัน) [21, 22]
ok(x) ทำงานใน \(O(n)\) เวลาทั้งหมดจะเป็น \(O(n \log z)\) [21, 22]
ปัญหา:
มีไส้กรอก \(N\) ชิ้นยาวต่างกัน ต้องการตัดไส้กรอกให้ผู้เข้าแข่ง \(M\) คน โดยทุกคนต้องได้ยาวเท่ากัน (ยาว \(L\)) ถามว่า \(L\) ที่ยาวที่สุดที่เป็นไปได้คือเท่าไหร่? [23]
วิธีนี้ใช้ Jump Strategy เพื่อหาจุดเปลี่ยนจาก False เป็น True ได้อย่างแม่นยำ [20, 25]
ในโจทย์พิกัดหรือเรขาคณิต เราต้องใช้ Binary Search กับจำนวนจริง (double/long double) [26]
while (hi - lo > 1e-9)
ระวัง: อาจเกิด Infinite Loop ถ้ากำหนดละเอียดเกินขีดจำกัดของ double [27]
for (int i=0; i<100; i++)
แนะนำ: การวนลูป 100 ครั้งให้ความละเอียดถึง \(10^{-30}\) และปลอดภัยกว่ามาก [28]
"Double-precision (double) มีความละเอียดเพียงประมาณ 15-17 หลัก เท่านั้น" [27]
หากขอบเขตการค้นหาคือ \(10^{12}\) และคุณตั้งความละเอียดที่ \(10^{-7}\)
ความแตกต่างระหว่าง lo และ hi อาจเหลือน้อยเกินกว่าที่คอมพิวเตอร์จะเก็บได้! [27]
วิธีแก้:
long long ในการคำนวณช่วงเสมอ [29]mid = low + (high - low) / 2 เพื่อเลี่ยงการบวกค่ามากปัญหาพบบ่อยที่สุดคือการจัดการ low และ high ที่ไม่สอดคล้องกับเงื่อนไขการหยุด [8]
while (low < high) {
int mid = (low+high)/2;
if (ok(mid)) low = mid;
else high = mid;
}
while (low <= high) {
...
low = mid + 1;
high = mid - 1;
}
"จงชัดเจนกับขอบเขต (Inclusive vs Exclusive) เสมอ" [8]
ถ้าข้อมูลมีขนาด \(10^9\) ตัว
Binary Search จะใช้เวลาตรวจสอบสูงสุดกี่ครั้ง?
คำใบ้: \(2^{10} \approx 10^3, 2^{20} \approx 10^6, 2^{30} \approx 10^9\)
หาค่าที่น้อยที่สุดของค่าสูงสุดที่เป็นไปได้ (เช่น โจทย์แบ่งงานให้คนทำงานพร้อมกัน)
หาค่าที่มากที่สุดของค่าต่ำสุดที่เป็นไปได้ (เช่น โจทย์วางเสาสัญญาณให้ห่างกันที่สุด)
ใช้พิสูจน์ความถูกต้องของ Greedy หรืออธิบายพฤติกรรมของระบบ [30]
จำไว้เสมอ:
ในโจทย์โอลิมปิก (IOI Style) หลายข้อ อัลกอริทึม \(O(n^2)\) หรือ \(O(n^3)\) อาจได้คะแนนเพียง 20-30 แต้ม
แต่การเพิ่ม Binary Search เข้าไปเพียงเล็กน้อย มักจะช่วยขยับคะแนนขึ้นไปถึง 50-70 แต้มได้ทันที! [31]
midlong long เมื่อค่าเกิน \(10^9\)it1 - v.begin() เมื่อใช้ STLlow ติดที่ค่าเดิมจนวนลูปไม่จบSorting & Searching section (Highly Recommended) [7]
Tag: 'binary search'
(Rating 800-1400) [7]
Silver level: Binary Search on Answer [7]
13.00 - 15.00 น.
Binary Search Implementation on Arrays & Answers
เตรียมเครื่องให้พร้อม เจอกันในช่วงบ่ายครับ!