Time Complexity
Author: Pakin Olanraktham & Pasit Sangprachathanarak
| Source | Resources |
|---|---|
| USACO | Time Complexity |
| CPH | Time complexity |
เนื่องจากใน Competitive Programming เราต้องการโปรแกรมที่มีประสิทธิภาพดี นั่นคือโปรแกรมสามารถทำงานได้อย่างรวดเร็ว หากโปรแกรมทำงานช้าเกินไป อาจโดนตัดคะแนน หรือไม่ได้คะแนนเลยก็ได้
Time Complexity คือ การประมาณจำนวน "ขั้นตอน" ที่อัลกอริทึมใช้กับ input ขนาด \(n\) โดยไม่ต้องรันโค้ดจริง เราจะเขียนในรูปฟังก์ชันของ \(n\) แล้วประมาณค่าเฉพาะพฤติกรรมเมื่อ \(n\) มีค่ามากๆ โดยเครื่องมือมาตรฐานที่ใช้คือ Big O notation โดยใน 1 วินาที C++ จะสามารถทำได้ประมาณ \(5\times 10^8\) ขั้นตอน
Big O คืออะไร
Big O (บิ๊กโอ) ใช้บอกขอบเขตบนของจำนวนขั้นตอนในกรณีที่แย่ที่สุด (worst case) เราเขียนว่า \(\mathcal{O}(f(n))\) โดยละทิ้งค่าคงที่ และพจน์อันดับต่ำกว่า
ตัวอย่าง: ถ้าจำนวนขั้นตอนจริง ๆ คือ \(3n^2 + 5n + 100\) เราเขียนสั้น ๆ ว่า \(\mathcal{O}(n^2)\)
สรุป
Big O คือ ภาษาย่อที่ช่วยบอกความเร็วโดยไม่สนใจรายละเอียดเล็ก ๆ (ค่าคงที่) ทำให้เราประเมินจำนวน "ขั้นตอน" ได้เร็ว
ตัวอย่างพื้นฐาน
โค้ดต่อไปนี้เป็น \(\mathcal{O}(1)\) (จำนวนคำสั่งคงที่ ไม่ขึ้นกับ \(n\))
ลูปเดี่ยว
หากภายใน Loop มี Complexity \(\mathcal{O}(1)\) และ Loop ทำงาน \(n\) รอบ เราจะได้ Complexity รวมคือ \(n\times\mathcal{O}(1)\) หรือก็คือ \(\mathcal{O}(n)\)
ค่าคงที่ ไม่มีผลต่อการคำนวณ Complexity โดยโค้ดด้านล่าง ทั้งสอง ก็ยังคงมี Complexity เท่ากันคือ \(\mathcal{O}(n)\) เหมือนเดิม
for (int i = 1; i <= 5*n + 17; i++) { /* O(1) */ }
for (int i = 1; i <= n + 457737; i++) { /* O(1) */ }
ลูปซ้อน (Nested Loops)
เราจะใช้วิธีเดียวกับการคำนวณ Complexity ของ Loop ปกติ
จากโค้ดข้างต้น เรารู้ว่าลูปด้านใน มี Complexity เท่ากับ \(\mathcal{O}(m)\) แต่ลูปนี้ ทำงาน \(n\) รอบเนื่องจากมีลูปอีกอันครอบอยู่ ดังนั้น Complexity รวมจะเท่ากับ \(n\times\mathcal{O}(m)\) หรือก็คือ \(\mathcal{O}(nm)\) นั่นเอง
หลายลูป
Complexity รวมคือ max ของแต่ละลูป:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
/* O(1) */
}
}
for (int i = 1; i <= n + 58834; i++) {
/* O(1) */
}
ลูปแรก \(\mathcal{O}(n^2)\) ลูปสอง \(\mathcal{O}(n)\) ดังนั้นรวม = \(\mathcal{O}(n^2)\)
กรณีมี input หลายขนาด เช่น \(n\) และ \(m\)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
/* O(1) */
}
}
for (int i = 1; i <= m; i++) {
/* O(1) */
}
รวม = \(\mathcal{O}(n^2 + m)\) (ยังไม่สามารถตัดเหลือ \(\max(n^2, m)\) ถ้าไม่รู้ความสัมพันธ์ระหว่าง \(n\) กับ \(m\))
คำแนะนำ
ในทางปฏิบัติ เราไม่จำเป็นต้องย่อให้เหมือนเป๊ะกับ Big O notation ที่กล่าวมาข้างต้นทั้งหมดก็ได้ เอาแค่ให้รู้คร่าวๆ ว่าโปรแกรมทำงานได้เร็วหรือช้าแค่ไหน เช่น \(\mathcal{O}(n^2)\) ก็สามารถที่จะรู้ได้แล้ว ว่าโปรแกรม สามารถทำงานได้ทัน ตามขอบเขตข้อมูลหรือไม่
Quiz
จงหา Complexity (Big O) ของโค้ดดังต่อไปนี้
เฉลย
อย่าลืมว่าเราไม่นำค่าคงที่มาคิด ดังนั้น คำตอบต้องเป็นแค่ \(\mathcal{O}(1)\)
เฉลย
จะมีการทำงานทั้งหมด \(n + (n-1) + (n-2) + \dots + 1 = \frac{n(n+1)}{2}\) ครั้ง ซึ่งเทียบได้กับ \(\mathcal{O}(n^2)\)
ตารางประเมินความเป็นไปได้
โดยทั่วไป (ภายใต้ time limit ประมาณ 1 วินาที ในภาษา C++) จะรองรับได้ประมาณนี้:
| ขนาด \(n\) | Complexities ที่ยังพอเป็นไปได้ |
|---|---|
| \(\le 10\) | \(\mathcal{O}(n!)\), \(\mathcal{O}(n^7)\), \(\mathcal{O}(n^6)\) |
| \(\le 20\) | \(\mathcal{O}(2^n\times n)\), \(\mathcal{O}(n^5)\) |
| \(\le 80\) | \(\mathcal{O}(n^4)\) |
| \(\le 400\) | \(\mathcal{O}(n^3)\) |
| \(\le 7.5 \times 10^3\) | \(\mathcal{O}(n^2)\) |
| \(\le 7 \times 10^4\) | \(\mathcal{O}(n \sqrt n)\) |
| \(\le 5 \times 10^5\) | \(\mathcal{O}(n \log n)\) |
| \(\le 5 \times 10^6\) | \(\mathcal{O}(n)\) |
| \(\le 10^{18}\) | \(\mathcal{O}(\log^2 n)\), \(\mathcal{O}(\log n)\), \(\mathcal{O}(1)\) |
อ้างอิงจาก
Constant Factor คืออะไร
แม้สองอัลกอริทึมจะอยู่ในคลาสเดียวกัน (เช่น \(\mathcal{O}(n)\)) ตัวคูณข้างหน้าก็ต่างกันได้ เช่น ทำ 1 การบวก เร็วกว่าทำ 1 ล้านการบวก แม้ทั้งคู่เป็น \(\mathcal{O}(1)\) เพราะมี Constant Factor ที่ไม่เท่ากัน